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