]> git.decadent.org.uk Git - dak.git/blob - dak/process_policy.py
0f2f22aab8a67d93e0d18b8610c1c5e1275f8648
[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 source_component_func(db_source):
145         package_list = PackageList(db_source.proxy)
146         component = source_component_from_package_list(package_list, upload.target_suite)
147         if component is not None:
148             return get_mapped_component(component.component_name, session=session)
149
150         # Fallback for packages without Package-List field
151         query = session.query(Override).filter_by(suite=overridesuite, package=db_source.source) \
152             .join(OverrideType).filter(OverrideType.overridetype == 'dsc') \
153             .join(Component)
154         return query.one().component
155
156     all_target_suites = [upload.target_suite]
157     all_target_suites.extend([q.suite for q in upload.target_suite.copy_queues])
158
159     for suite in all_target_suites:
160         if upload.source is not None:
161             transaction.copy_source(upload.source, suite, source_component_func(upload.source), allow_tainted=allow_tainted)
162         for db_binary in upload.binaries:
163             # build queues may miss the source package if this is a binary-only upload
164             if suite != upload.target_suite:
165                 transaction.copy_source(db_binary.source, suite, source_component_func(db_binary.source), allow_tainted=allow_tainted)
166             transaction.copy_binary(db_binary, suite, binary_component_func(db_binary), allow_tainted=allow_tainted, extra_archives=[upload.target_suite.archive])
167
168     # Copy .changes if needed
169     if upload.target_suite.copychanges:
170         src = os.path.join(upload.policy_queue.path, upload.changes.changesname)
171         dst = os.path.join(upload.target_suite.path, upload.changes.changesname)
172         fs.copy(src, dst, mode=upload.target_suite.archive.mode)
173
174     # Copy upload to Process-Policy::CopyDir
175     # Used on security.d.o to sync accepted packages to ftp-master, but this
176     # should eventually be replaced by something else.
177     copydir = cnf.get('Process-Policy::CopyDir') or None
178     if copydir is not None:
179         mode = upload.target_suite.archive.mode
180         if upload.source is not None:
181             for f in [ df.poolfile for df in upload.source.srcfiles ]:
182                 dst = os.path.join(copydir, f.basename)
183                 if not os.path.exists(dst):
184                     fs.copy(f.fullpath, dst, mode=mode)
185
186         for db_binary in upload.binaries:
187             f = db_binary.poolfile
188             dst = os.path.join(copydir, f.basename)
189             if not os.path.exists(dst):
190                 fs.copy(f.fullpath, dst, mode=mode)
191
192         src = os.path.join(upload.policy_queue.path, upload.changes.changesname)
193         dst = os.path.join(copydir, upload.changes.changesname)
194         if not os.path.exists(dst):
195             fs.copy(src, dst, mode=mode)
196
197     if upload.source is not None and not Options['No-Action']:
198         urgency = upload.changes.urgency
199         if urgency not in cnf.value_list('Urgency::Valid'):
200             urgency = cnf['Urgency::Default']
201         UrgencyLog().log(upload.source.source, upload.source.version, urgency)
202
203     print "  ACCEPT"
204     if not Options['No-Action']:
205         Logger.log(["Policy Queue ACCEPT", srcqueue.queue_name, changesname])
206
207     pu = get_processed_upload(upload)
208     daklib.announce.announce_accept(pu)
209
210     # TODO: code duplication. Similar code is in process-upload.
211     # Move .changes to done
212     src = os.path.join(upload.policy_queue.path, upload.changes.changesname)
213     now = datetime.datetime.now()
214     donedir = os.path.join(cnf['Dir::Done'], now.strftime('%Y/%m/%d'))
215     dst = os.path.join(donedir, upload.changes.changesname)
216     dst = utils.find_next_free(dst)
217     fs.copy(src, dst, mode=0o644)
218
219     remove_upload(upload, transaction)
220
221 ################################################################################
222
223 @try_or_reject
224 def comment_reject(*args):
225     real_comment_reject(*args, manual=True)
226
227 def real_comment_reject(upload, srcqueue, comments, transaction, notify=True, manual=False):
228     cnf = Config()
229
230     fs = transaction.fs
231     session = transaction.session
232     changesname = upload.changes.changesname
233     queuedir = upload.policy_queue.path
234     rejectdir = cnf['Dir::Reject']
235
236     ### Copy files to reject/
237
238     poolfiles = [b.poolfile for b in upload.binaries]
239     if upload.source is not None:
240         poolfiles.extend([df.poolfile for df in upload.source.srcfiles])
241     # Not beautiful...
242     files = [ af.path for af in session.query(ArchiveFile) \
243                   .filter_by(archive=upload.policy_queue.suite.archive) \
244                   .join(ArchiveFile.file) \
245                   .filter(PoolFile.file_id.in_([ f.file_id for f in poolfiles ])) ]
246     for byhand in upload.byhand:
247         path = os.path.join(queuedir, byhand.filename)
248         if os.path.exists(path):
249             files.append(path)
250     files.append(os.path.join(queuedir, changesname))
251
252     for fn in files:
253         dst = utils.find_next_free(os.path.join(rejectdir, os.path.basename(fn)))
254         fs.copy(fn, dst, link=True)
255
256     ### Write reason
257
258     dst = utils.find_next_free(os.path.join(rejectdir, '{0}.reason'.format(changesname)))
259     fh = fs.create(dst)
260     fh.write(comments)
261     fh.close()
262
263     ### Send mail notification
264
265     if notify:
266         rejected_by = None
267         reason = comments
268
269         # Try to use From: from comment file if there is one.
270         # This is not very elegant...
271         match = re.match(r"\AFrom: ([^\n]+)\n\n", comments)
272         if match:
273             rejected_by = match.group(1)
274             reason = '\n'.join(comments.splitlines()[2:])
275
276         pu = get_processed_upload(upload)
277         daklib.announce.announce_reject(pu, reason, rejected_by)
278
279     print "  REJECT"
280     if not Options["No-Action"]:
281         Logger.log(["Policy Queue REJECT", srcqueue.queue_name, upload.changes.changesname])
282
283     changes = upload.changes
284     remove_upload(upload, transaction)
285     session.delete(changes)
286
287 ################################################################################
288
289 def remove_upload(upload, transaction):
290     fs = transaction.fs
291     session = transaction.session
292     changes = upload.changes
293
294     # Remove byhand and changes files. Binary and source packages will be
295     # removed from {bin,src}_associations and eventually removed by clean-suites automatically.
296     queuedir = upload.policy_queue.path
297     for byhand in upload.byhand:
298         path = os.path.join(queuedir, byhand.filename)
299         if os.path.exists(path):
300             fs.unlink(path)
301         session.delete(byhand)
302     fs.unlink(os.path.join(queuedir, upload.changes.changesname))
303
304     session.delete(upload)
305     session.flush()
306
307 ################################################################################
308
309 def get_processed_upload(upload):
310     pu = daklib.announce.ProcessedUpload()
311
312     pu.maintainer = upload.changes.maintainer
313     pu.changed_by = upload.changes.changedby
314     pu.fingerprint = upload.changes.fingerprint
315
316     pu.suites = [ upload.target_suite ]
317     pu.from_policy_suites = [ upload.target_suite ]
318
319     changes_path = os.path.join(upload.policy_queue.path, upload.changes.changesname)
320     pu.changes = open(changes_path, 'r').read()
321     pu.changes_filename = upload.changes.changesname
322     pu.sourceful = upload.source is not None
323     pu.source = upload.changes.source
324     pu.version = upload.changes.version
325     pu.architecture = upload.changes.architecture
326     pu.bugs = upload.changes.closes
327
328     pu.program = "process-policy"
329
330     return pu
331
332 ################################################################################
333
334 def remove_unreferenced_binaries(policy_queue, transaction):
335     """Remove binaries that are no longer referenced by an upload
336
337     @type  policy_queue: L{daklib.dbconn.PolicyQueue}
338
339     @type  transaction: L{daklib.archive.ArchiveTransaction}
340     """
341     session = transaction.session
342     suite = policy_queue.suite
343
344     query = """
345        SELECT b.*
346          FROM binaries b
347          JOIN bin_associations ba ON b.id = ba.bin
348         WHERE ba.suite = :suite_id
349           AND NOT EXISTS (SELECT 1 FROM policy_queue_upload_binaries_map pqubm
350                                    JOIN policy_queue_upload pqu ON pqubm.policy_queue_upload_id = pqu.id
351                                   WHERE pqu.policy_queue_id = :policy_queue_id
352                                     AND pqubm.binary_id = b.id)"""
353     binaries = session.query(DBBinary).from_statement(query) \
354         .params({'suite_id': policy_queue.suite_id, 'policy_queue_id': policy_queue.policy_queue_id})
355
356     for binary in binaries:
357         Logger.log(["removed binary from policy queue", policy_queue.queue_name, binary.package, binary.version])
358         transaction.remove_binary(binary, suite)
359
360 def remove_unreferenced_sources(policy_queue, transaction):
361     """Remove sources that are no longer referenced by an upload or a binary
362
363     @type  policy_queue: L{daklib.dbconn.PolicyQueue}
364
365     @type  transaction: L{daklib.archive.ArchiveTransaction}
366     """
367     session = transaction.session
368     suite = policy_queue.suite
369
370     query = """
371        SELECT s.*
372          FROM source s
373          JOIN src_associations sa ON s.id = sa.source
374         WHERE sa.suite = :suite_id
375           AND NOT EXISTS (SELECT 1 FROM policy_queue_upload pqu
376                                   WHERE pqu.policy_queue_id = :policy_queue_id
377                                     AND pqu.source_id = s.id)
378           AND NOT EXISTS (SELECT 1 FROM binaries b
379                                    JOIN bin_associations ba ON b.id = ba.bin
380                                   WHERE b.source = s.id
381                                     AND ba.suite = :suite_id)"""
382     sources = session.query(DBSource).from_statement(query) \
383         .params({'suite_id': policy_queue.suite_id, 'policy_queue_id': policy_queue.policy_queue_id})
384
385     for source in sources:
386         Logger.log(["removed source from policy queue", policy_queue.queue_name, source.source, source.version])
387         transaction.remove_source(source, suite)
388
389 ################################################################################
390
391 def main():
392     global Options, Logger
393
394     cnf = Config()
395     session = DBConn().session()
396
397     Arguments = [('h',"help","Process-Policy::Options::Help"),
398                  ('n',"no-action","Process-Policy::Options::No-Action")]
399
400     for i in ["help", "no-action"]:
401         if not cnf.has_key("Process-Policy::Options::%s" % (i)):
402             cnf["Process-Policy::Options::%s" % (i)] = ""
403
404     queue_name = apt_pkg.parse_commandline(cnf.Cnf,Arguments,sys.argv)
405
406     if len(queue_name) != 1:
407         print "E: Specify exactly one policy queue"
408         sys.exit(1)
409
410     queue_name = queue_name[0]
411
412     Options = cnf.subtree("Process-Policy::Options")
413
414     if Options["Help"]:
415         usage()
416
417     Logger = daklog.Logger("process-policy")
418     if not Options["No-Action"]:
419         urgencylog = UrgencyLog()
420
421     with ArchiveTransaction() as transaction:
422         session = transaction.session
423         try:
424             pq = session.query(PolicyQueue).filter_by(queue_name=queue_name).one()
425         except NoResultFound:
426             print "E: Cannot find policy queue %s" % queue_name
427             sys.exit(1)
428
429         commentsdir = os.path.join(pq.path, 'COMMENTS')
430         # The comments stuff relies on being in the right directory
431         os.chdir(pq.path)
432
433         do_comments(commentsdir, pq, "REJECT.", "REJECTED.", "NOTOK", comment_reject, transaction)
434         do_comments(commentsdir, pq, "ACCEPT.", "ACCEPTED.", "OK", comment_accept, transaction)
435         do_comments(commentsdir, pq, "ACCEPTED.", "ACCEPTED.", "OK", comment_accept, transaction)
436
437         remove_unreferenced_binaries(pq, transaction)
438         remove_unreferenced_sources(pq, transaction)
439
440     if not Options['No-Action']:
441         urgencylog.close()
442
443 ################################################################################
444
445 if __name__ == '__main__':
446     main()