]> git.decadent.org.uk Git - dak.git/blob - dak/process_policy.py
Choose suite to redirect upload to before installing source
[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 from sqlalchemy.orm.exc import NoResultFound
41
42 from daklib.dbconn import *
43 from daklib import daklog
44 from daklib import utils
45 from daklib.dak_exceptions import CantOpenError, AlreadyLockedError, CantGetLockError
46 from daklib.config import Config
47 from daklib.archive import ArchiveTransaction, source_component_from_package_list
48 from daklib.urgencylog import UrgencyLog
49 from daklib.packagelist import PackageList
50
51 import daklib.announce
52
53 # Globals
54 Options = None
55 Logger = None
56
57 ################################################################################
58
59 def do_comments(dir, srcqueue, opref, npref, line, fn, transaction):
60     session = transaction.session
61     actions = []
62     for comm in [ x for x in os.listdir(dir) if x.startswith(opref) ]:
63         lines = open(os.path.join(dir, comm)).readlines()
64         if len(lines) == 0 or lines[0] != line + "\n": continue
65
66         # If the ACCEPT includes a _<arch> we only accept that .changes.
67         # Otherwise we accept all .changes that start with the given prefix
68         changes_prefix = comm[len(opref):]
69         if changes_prefix.count('_') < 2:
70             changes_prefix = changes_prefix + '_'
71         else:
72             changes_prefix = changes_prefix + '.changes'
73
74         # We need to escape "_" as we use it with the LIKE operator (via the
75         # SQLA startwith) later.
76         changes_prefix = changes_prefix.replace("_", r"\_")
77
78         uploads = session.query(PolicyQueueUpload).filter_by(policy_queue=srcqueue) \
79             .join(PolicyQueueUpload.changes).filter(DBChange.changesname.startswith(changes_prefix)) \
80             .order_by(PolicyQueueUpload.source_id)
81         reason = "".join(lines[1:])
82         actions.extend((u, reason) for u in uploads)
83
84         if opref != npref:
85             newcomm = npref + comm[len(opref):]
86             newcomm = utils.find_next_free(os.path.join(dir, newcomm))
87             transaction.fs.move(os.path.join(dir, comm), newcomm)
88
89     actions.sort()
90
91     for u, reason in actions:
92         print("Processing changes file: {0}".format(u.changes.changesname))
93         fn(u, srcqueue, reason, transaction)
94
95 ################################################################################
96
97 def try_or_reject(function):
98     def wrapper(upload, srcqueue, comments, transaction):
99         try:
100             function(upload, srcqueue, comments, transaction)
101         except Exception as e:
102             comments = 'An exception was raised while processing the package:\n{0}\nOriginal comments:\n{1}'.format(traceback.format_exc(), comments)
103             try:
104                 transaction.rollback()
105                 real_comment_reject(upload, srcqueue, comments, transaction)
106             except Exception as e:
107                 comments = 'In addition an exception was raised while trying to reject the upload:\n{0}\nOriginal rejection:\n{1}'.format(traceback.format_exc(), comments)
108                 transaction.rollback()
109                 real_comment_reject(upload, srcqueue, comments, transaction, notify=False)
110         if not Options['No-Action']:
111             transaction.commit()
112         else:
113             transaction.rollback()
114     return wrapper
115
116 ################################################################################
117
118 @try_or_reject
119 def comment_accept(upload, srcqueue, comments, transaction):
120     for byhand in upload.byhand:
121         path = os.path.join(srcqueue.path, byhand.filename)
122         if os.path.exists(path):
123             raise Exception('E: cannot ACCEPT upload with unprocessed byhand file {0}'.format(byhand.filename))
124
125     cnf = Config()
126
127     fs = transaction.fs
128     session = transaction.session
129     changesname = upload.changes.changesname
130     allow_tainted = srcqueue.suite.archive.tainted
131
132     # We need overrides to get the target component
133     overridesuite = upload.target_suite
134     if overridesuite.overridesuite is not None:
135         overridesuite = session.query(Suite).filter_by(suite_name=overridesuite.overridesuite).one()
136
137     def binary_component_func(db_binary):
138         section = db_binary.proxy['Section']
139         component_name = 'main'
140         if section.find('/') != -1:
141             component_name = section.split('/', 1)[0]
142         return get_mapped_component(component_name, session=session)
143
144     def is_debug_binary(db_binary):
145         return db_binary.proxy['Section'] == "debug"
146
147     def has_debug_binaries(upload):
148         return any((is_debug_binary(x) for x in upload.binaries))
149
150     def source_component_func(db_source):
151         package_list = PackageList(db_source.proxy)
152         component = source_component_from_package_list(package_list, upload.target_suite)
153         if component is not None:
154             return get_mapped_component(component.component_name, session=session)
155
156         # Fallback for packages without Package-List field
157         query = session.query(Override).filter_by(suite=overridesuite, package=db_source.source) \
158             .join(OverrideType).filter(OverrideType.overridetype == 'dsc') \
159             .join(Component)
160         return query.one().component
161
162     all_target_suites = [upload.target_suite]
163     all_target_suites.extend([q.suite for q in upload.target_suite.copy_queues])
164
165     for suite in all_target_suites:
166         debug_suite = suite.debug_suite
167
168         if upload.source is not None:
169             # If we have Source in this upload, let's include it into
170             # upload suite.
171             transaction.copy_source(
172                 upload.source,
173                 suite,
174                 source_component_func(upload.source),
175                 allow_tainted=allow_tainted,
176             )
177
178             if debug_suite is not None and has_debug_binaries(upload):
179                 # If we're handing a debug package, we also need to include the
180                 # source in the debug suite as well.
181                 transaction.copy_source(
182                     upload.source,
183                     debug_suite,
184                     source_component_func(upload.source),
185                     allow_tainted=allow_tainted,
186                 )
187
188         for db_binary in upload.binaries:
189             # Now, let's work out where to copy this guy to -- if it's
190             # a debug binary, and the suite has a debug suite, let's go
191             # ahead and target the debug suite rather then the stock
192             # suite.
193             copy_to_suite = suite
194             if debug_suite is not None and is_debug_binary(db_binary):
195                 copy_to_suite = debug_suite
196
197             # build queues may miss the source package if this is a
198             # binary-only upload.
199             if suite != upload.target_suite:
200                 transaction.copy_source(
201                     db_binary.source,
202                     copy_to_suite,
203                     source_component_func(db_binary.source),
204                     allow_tainted=allow_tainted,
205                 )
206
207             transaction.copy_binary(
208                 db_binary,
209                 copy_to_suite,
210                 binary_component_func(db_binary),
211                 allow_tainted=allow_tainted,
212                 extra_archives=[upload.target_suite.archive],
213             )
214
215     # Copy .changes if needed
216     if upload.target_suite.copychanges:
217         src = os.path.join(upload.policy_queue.path, upload.changes.changesname)
218         dst = os.path.join(upload.target_suite.path, upload.changes.changesname)
219         fs.copy(src, dst, mode=upload.target_suite.archive.mode)
220
221     # Copy upload to Process-Policy::CopyDir
222     # Used on security.d.o to sync accepted packages to ftp-master, but this
223     # should eventually be replaced by something else.
224     copydir = cnf.get('Process-Policy::CopyDir') or None
225     if copydir is not None:
226         mode = upload.target_suite.archive.mode
227         if upload.source is not None:
228             for f in [ df.poolfile for df in upload.source.srcfiles ]:
229                 dst = os.path.join(copydir, f.basename)
230                 if not os.path.exists(dst):
231                     fs.copy(f.fullpath, dst, mode=mode)
232
233         for db_binary in upload.binaries:
234             f = db_binary.poolfile
235             dst = os.path.join(copydir, f.basename)
236             if not os.path.exists(dst):
237                 fs.copy(f.fullpath, dst, mode=mode)
238
239         src = os.path.join(upload.policy_queue.path, upload.changes.changesname)
240         dst = os.path.join(copydir, upload.changes.changesname)
241         if not os.path.exists(dst):
242             fs.copy(src, dst, mode=mode)
243
244     if upload.source is not None and not Options['No-Action']:
245         urgency = upload.changes.urgency
246         if urgency not in cnf.value_list('Urgency::Valid'):
247             urgency = cnf['Urgency::Default']
248         UrgencyLog().log(upload.source.source, upload.source.version, urgency)
249
250     print "  ACCEPT"
251     if not Options['No-Action']:
252         Logger.log(["Policy Queue ACCEPT", srcqueue.queue_name, changesname])
253
254     pu = get_processed_upload(upload)
255     daklib.announce.announce_accept(pu)
256
257     # TODO: code duplication. Similar code is in process-upload.
258     # Move .changes to done
259     src = os.path.join(upload.policy_queue.path, upload.changes.changesname)
260     now = datetime.datetime.now()
261     donedir = os.path.join(cnf['Dir::Done'], now.strftime('%Y/%m/%d'))
262     dst = os.path.join(donedir, upload.changes.changesname)
263     dst = utils.find_next_free(dst)
264     fs.copy(src, dst, mode=0o644)
265
266     remove_upload(upload, transaction)
267
268 ################################################################################
269
270 @try_or_reject
271 def comment_reject(*args):
272     real_comment_reject(*args, manual=True)
273
274 def real_comment_reject(upload, srcqueue, comments, transaction, notify=True, manual=False):
275     cnf = Config()
276
277     fs = transaction.fs
278     session = transaction.session
279     changesname = upload.changes.changesname
280     queuedir = upload.policy_queue.path
281     rejectdir = cnf['Dir::Reject']
282
283     ### Copy files to reject/
284
285     poolfiles = [b.poolfile for b in upload.binaries]
286     if upload.source is not None:
287         poolfiles.extend([df.poolfile for df in upload.source.srcfiles])
288     # Not beautiful...
289     files = [ af.path for af in session.query(ArchiveFile) \
290                   .filter_by(archive=upload.policy_queue.suite.archive) \
291                   .join(ArchiveFile.file) \
292                   .filter(PoolFile.file_id.in_([ f.file_id for f in poolfiles ])) ]
293     for byhand in upload.byhand:
294         path = os.path.join(queuedir, byhand.filename)
295         if os.path.exists(path):
296             files.append(path)
297     files.append(os.path.join(queuedir, changesname))
298
299     for fn in files:
300         dst = utils.find_next_free(os.path.join(rejectdir, os.path.basename(fn)))
301         fs.copy(fn, dst, link=True)
302
303     ### Write reason
304
305     dst = utils.find_next_free(os.path.join(rejectdir, '{0}.reason'.format(changesname)))
306     fh = fs.create(dst)
307     fh.write(comments)
308     fh.close()
309
310     ### Send mail notification
311
312     if notify:
313         rejected_by = None
314         reason = comments
315
316         # Try to use From: from comment file if there is one.
317         # This is not very elegant...
318         match = re.match(r"\AFrom: ([^\n]+)\n\n", comments)
319         if match:
320             rejected_by = match.group(1)
321             reason = '\n'.join(comments.splitlines()[2:])
322
323         pu = get_processed_upload(upload)
324         daklib.announce.announce_reject(pu, reason, rejected_by)
325
326     print "  REJECT"
327     if not Options["No-Action"]:
328         Logger.log(["Policy Queue REJECT", srcqueue.queue_name, upload.changes.changesname])
329
330     changes = upload.changes
331     remove_upload(upload, transaction)
332     session.delete(changes)
333
334 ################################################################################
335
336 def remove_upload(upload, transaction):
337     fs = transaction.fs
338     session = transaction.session
339     changes = upload.changes
340
341     # Remove byhand and changes files. Binary and source packages will be
342     # removed from {bin,src}_associations and eventually removed by clean-suites automatically.
343     queuedir = upload.policy_queue.path
344     for byhand in upload.byhand:
345         path = os.path.join(queuedir, byhand.filename)
346         if os.path.exists(path):
347             fs.unlink(path)
348         session.delete(byhand)
349     fs.unlink(os.path.join(queuedir, upload.changes.changesname))
350
351     session.delete(upload)
352     session.flush()
353
354 ################################################################################
355
356 def get_processed_upload(upload):
357     pu = daklib.announce.ProcessedUpload()
358
359     pu.maintainer = upload.changes.maintainer
360     pu.changed_by = upload.changes.changedby
361     pu.fingerprint = upload.changes.fingerprint
362
363     pu.suites = [ upload.target_suite ]
364     pu.from_policy_suites = [ upload.target_suite ]
365
366     changes_path = os.path.join(upload.policy_queue.path, upload.changes.changesname)
367     pu.changes = open(changes_path, 'r').read()
368     pu.changes_filename = upload.changes.changesname
369     pu.sourceful = upload.source is not None
370     pu.source = upload.changes.source
371     pu.version = upload.changes.version
372     pu.architecture = upload.changes.architecture
373     pu.bugs = upload.changes.closes
374
375     pu.program = "process-policy"
376
377     return pu
378
379 ################################################################################
380
381 def remove_unreferenced_binaries(policy_queue, transaction):
382     """Remove binaries that are no longer referenced by an upload
383
384     @type  policy_queue: L{daklib.dbconn.PolicyQueue}
385
386     @type  transaction: L{daklib.archive.ArchiveTransaction}
387     """
388     session = transaction.session
389     suite = policy_queue.suite
390
391     query = """
392        SELECT b.*
393          FROM binaries b
394          JOIN bin_associations ba ON b.id = ba.bin
395         WHERE ba.suite = :suite_id
396           AND NOT EXISTS (SELECT 1 FROM policy_queue_upload_binaries_map pqubm
397                                    JOIN policy_queue_upload pqu ON pqubm.policy_queue_upload_id = pqu.id
398                                   WHERE pqu.policy_queue_id = :policy_queue_id
399                                     AND pqubm.binary_id = b.id)"""
400     binaries = session.query(DBBinary).from_statement(query) \
401         .params({'suite_id': policy_queue.suite_id, 'policy_queue_id': policy_queue.policy_queue_id})
402
403     for binary in binaries:
404         Logger.log(["removed binary from policy queue", policy_queue.queue_name, binary.package, binary.version])
405         transaction.remove_binary(binary, suite)
406
407 def remove_unreferenced_sources(policy_queue, transaction):
408     """Remove sources that are no longer referenced by an upload or a binary
409
410     @type  policy_queue: L{daklib.dbconn.PolicyQueue}
411
412     @type  transaction: L{daklib.archive.ArchiveTransaction}
413     """
414     session = transaction.session
415     suite = policy_queue.suite
416
417     query = """
418        SELECT s.*
419          FROM source s
420          JOIN src_associations sa ON s.id = sa.source
421         WHERE sa.suite = :suite_id
422           AND NOT EXISTS (SELECT 1 FROM policy_queue_upload pqu
423                                   WHERE pqu.policy_queue_id = :policy_queue_id
424                                     AND pqu.source_id = s.id)
425           AND NOT EXISTS (SELECT 1 FROM binaries b
426                                    JOIN bin_associations ba ON b.id = ba.bin
427                                   WHERE b.source = s.id
428                                     AND ba.suite = :suite_id)"""
429     sources = session.query(DBSource).from_statement(query) \
430         .params({'suite_id': policy_queue.suite_id, 'policy_queue_id': policy_queue.policy_queue_id})
431
432     for source in sources:
433         Logger.log(["removed source from policy queue", policy_queue.queue_name, source.source, source.version])
434         transaction.remove_source(source, suite)
435
436 ################################################################################
437
438 def main():
439     global Options, Logger
440
441     cnf = Config()
442     session = DBConn().session()
443
444     Arguments = [('h',"help","Process-Policy::Options::Help"),
445                  ('n',"no-action","Process-Policy::Options::No-Action")]
446
447     for i in ["help", "no-action"]:
448         if not cnf.has_key("Process-Policy::Options::%s" % (i)):
449             cnf["Process-Policy::Options::%s" % (i)] = ""
450
451     queue_name = apt_pkg.parse_commandline(cnf.Cnf,Arguments,sys.argv)
452
453     if len(queue_name) != 1:
454         print "E: Specify exactly one policy queue"
455         sys.exit(1)
456
457     queue_name = queue_name[0]
458
459     Options = cnf.subtree("Process-Policy::Options")
460
461     if Options["Help"]:
462         usage()
463
464     Logger = daklog.Logger("process-policy")
465     if not Options["No-Action"]:
466         urgencylog = UrgencyLog()
467
468     with ArchiveTransaction() as transaction:
469         session = transaction.session
470         try:
471             pq = session.query(PolicyQueue).filter_by(queue_name=queue_name).one()
472         except NoResultFound:
473             print "E: Cannot find policy queue %s" % queue_name
474             sys.exit(1)
475
476         commentsdir = os.path.join(pq.path, 'COMMENTS')
477         # The comments stuff relies on being in the right directory
478         os.chdir(pq.path)
479
480         do_comments(commentsdir, pq, "REJECT.", "REJECTED.", "NOTOK", comment_reject, transaction)
481         do_comments(commentsdir, pq, "ACCEPT.", "ACCEPTED.", "OK", comment_accept, transaction)
482         do_comments(commentsdir, pq, "ACCEPTED.", "ACCEPTED.", "OK", comment_accept, transaction)
483
484         remove_unreferenced_binaries(pq, transaction)
485         remove_unreferenced_sources(pq, transaction)
486
487     if not Options['No-Action']:
488         urgencylog.close()
489
490 ################################################################################
491
492 if __name__ == '__main__':
493     main()