]> git.decadent.org.uk Git - dak.git/blob - daklib/checks.py
daklib/checks.py: Set "session" variable.
[dak.git] / daklib / checks.py
1 # Copyright (C) 2012, Ansgar Burchardt <ansgar@debian.org>
2 #
3 # Parts based on code that is
4 # Copyright (C) 2001-2006, James Troup <james@nocrew.org>
5 # Copyright (C) 2009-2010, Joerg Jaspert <joerg@debian.org>
6 #
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along
18 # with this program; if not, write to the Free Software Foundation, Inc.,
19 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20
21 """module provided pre-acceptance tests
22
23 Please read the documentation for the L{Check} class for the interface.
24 """
25
26 from daklib.config import Config
27 import daklib.daksubprocess
28 from daklib.dbconn import *
29 import daklib.dbconn as dbconn
30 from daklib.regexes import *
31 from daklib.textutils import fix_maintainer, ParseMaintError
32 import daklib.lintian as lintian
33 import daklib.utils as utils
34 from daklib.upload import InvalidHashException
35
36 import apt_inst
37 import apt_pkg
38 from apt_pkg import version_compare
39 import errno
40 import os
41 import subprocess
42 import time
43 import yaml
44
45 def check_fields_for_valid_utf8(filename, control):
46     """Check all fields of a control file for valid UTF-8"""
47     for field in control.keys():
48         try:
49             field.decode('utf-8')
50             control[field].decode('utf-8')
51         except UnicodeDecodeError:
52             raise Reject('{0}: The {1} field is not valid UTF-8'.format(filename, field))
53
54 class Reject(Exception):
55     """exception raised by failing checks"""
56     pass
57
58 class RejectStupidMaintainerException(Exception):
59     """exception raised by failing the external hashes check"""
60
61     def __str__(self):
62         return "'%s' has mismatching %s from the external files db ('%s' [current] vs '%s' [external])" % self.args[:4]
63
64 class RejectACL(Reject):
65     """exception raise by failing ACL checks"""
66     def __init__(self, acl, reason):
67         self.acl = acl
68         self.reason = reason
69
70     def __str__(self):
71         return "ACL {0}: {1}".format(self.acl.name, self.reason)
72
73 class Check(object):
74     """base class for checks
75
76     checks are called by L{daklib.archive.ArchiveUpload}. Failing tests should
77     raise a L{daklib.checks.Reject} exception including a human-readable
78     description why the upload should be rejected.
79     """
80     def check(self, upload):
81         """do checks
82
83         @type  upload: L{daklib.archive.ArchiveUpload}
84         @param upload: upload to check
85
86         @raise daklib.checks.Reject: upload should be rejected
87         """
88         raise NotImplemented
89     def per_suite_check(self, upload, suite):
90         """do per-suite checks
91
92         @type  upload: L{daklib.archive.ArchiveUpload}
93         @param upload: upload to check
94
95         @type  suite: L{daklib.dbconn.Suite}
96         @param suite: suite to check
97
98         @raise daklib.checks.Reject: upload should be rejected
99         """
100         raise NotImplemented
101     @property
102     def forcable(self):
103         """allow to force ignore failing test
104
105         C{True} if it is acceptable to force ignoring a failing test,
106         C{False} otherwise
107         """
108         return False
109
110 class SignatureAndHashesCheck(Check):
111     """Check signature of changes and dsc file (if included in upload)
112
113     Make sure the signature is valid and done by a known user.
114     """
115     def check(self, upload):
116         changes = upload.changes
117         if not changes.valid_signature:
118             raise Reject("Signature for .changes not valid.")
119         self._check_hashes(upload, changes.filename, changes.files.itervalues())
120
121         source = None
122         try:
123             source = changes.source
124         except Exception as e:
125             raise Reject("Invalid dsc file: {0}".format(e))
126         if source is not None:
127             if not source.valid_signature:
128                 raise Reject("Signature for .dsc not valid.")
129             if source.primary_fingerprint != changes.primary_fingerprint:
130                 raise Reject(".changes and .dsc not signed by the same key.")
131             self._check_hashes(upload, source.filename, source.files.itervalues())
132
133         if upload.fingerprint is None or upload.fingerprint.uid is None:
134             raise Reject(".changes signed by unknown key.")
135
136     """Make sure hashes match existing files
137
138     @type  upload: L{daklib.archive.ArchiveUpload}
139     @param upload: upload we are processing
140
141     @type  filename: str
142     @param filename: name of the file the expected hash values are taken from
143
144     @type  files: sequence of L{daklib.upload.HashedFile}
145     @param files: files to check the hashes for
146     """
147     def _check_hashes(self, upload, filename, files):
148         try:
149             for f in files:
150                 f.check(upload.directory)
151         except IOError as e:
152             if e.errno == errno.ENOENT:
153                 raise Reject('{0} refers to non-existing file: {1}\n'
154                              'Perhaps you need to include it in your upload?'
155                              .format(filename, os.path.basename(e.filename)))
156             raise
157         except InvalidHashException as e:
158             raise Reject('{0}: {1}'.format(filename, unicode(e)))
159
160 class ChangesCheck(Check):
161     """Check changes file for syntax errors."""
162     def check(self, upload):
163         changes = upload.changes
164         control = changes.changes
165         fn = changes.filename
166
167         for field in ('Distribution', 'Source', 'Binary', 'Architecture', 'Version', 'Maintainer', 'Files', 'Changes', 'Description'):
168             if field not in control:
169                 raise Reject('{0}: misses mandatory field {1}'.format(fn, field))
170
171         check_fields_for_valid_utf8(fn, control)
172
173         source_match = re_field_source.match(control['Source'])
174         if not source_match:
175             raise Reject('{0}: Invalid Source field'.format(fn))
176         version_match = re_field_version.match(control['Version'])
177         if not version_match:
178             raise Reject('{0}: Invalid Version field'.format(fn))
179         version_without_epoch = version_match.group('without_epoch')
180
181         match = re_file_changes.match(fn)
182         if not match:
183             raise Reject('{0}: Does not match re_file_changes'.format(fn))
184         if match.group('package') != source_match.group('package'):
185             raise Reject('{0}: Filename does not match Source field'.format(fn))
186         if match.group('version') != version_without_epoch:
187             raise Reject('{0}: Filename does not match Version field'.format(fn))
188
189         for bn in changes.binary_names:
190             if not re_field_package.match(bn):
191                 raise Reject('{0}: Invalid binary package name {1}'.format(fn, bn))
192
193         if 'source' in changes.architectures and changes.source is None:
194             raise Reject("Changes has architecture source, but no source found.")
195         if changes.source is not None and 'source' not in changes.architectures:
196             raise Reject("Upload includes source, but changes does not say so.")
197
198         try:
199             fix_maintainer(changes.changes['Maintainer'])
200         except ParseMaintError as e:
201             raise Reject('{0}: Failed to parse Maintainer field: {1}'.format(changes.filename, e))
202
203         try:
204             changed_by = changes.changes.get('Changed-By')
205             if changed_by is not None:
206                 fix_maintainer(changed_by)
207         except ParseMaintError as e:
208             raise Reject('{0}: Failed to parse Changed-By field: {1}'.format(changes.filename, e))
209
210         if len(changes.files) == 0:
211             raise Reject("Changes includes no files.")
212
213         for bugnum in changes.closed_bugs:
214             if not re_isanum.match(bugnum):
215                 raise Reject('{0}: "{1}" in Closes field is not a number'.format(changes.filename, bugnum))
216
217         return True
218
219 class ExternalHashesCheck(Check):
220     """Checks hashes in .changes and .dsc against an external database."""
221     def check_single(self, session, f):
222         q = session.execute("SELECT size, md5sum, sha1sum, sha256sum FROM external_files WHERE filename LIKE '%%/%s'" % f.filename)
223         (ext_size, ext_md5sum, ext_sha1sum, ext_sha256sum) = q.fetchone() or (None, None, None, None)
224
225         if not ext_size:
226             return
227
228         if ext_size != f.size:
229             raise RejectStupidMaintainerException(f.filename, 'size', f.size, ext_size)
230
231         if ext_md5sum != f.md5sum:
232             raise RejectStupidMaintainerException(f.filename, 'md5sum', f.md5sum, ext_md5sum)
233
234         if ext_sha1sum != f.sha1sum:
235             raise RejectStupidMaintainerException(f.filename, 'sha1sum', f.sha1sum, ext_sha1sum)
236
237         if ext_sha256sum != f.sha256sum:
238             raise RejectStupidMaintainerException(f.filename, 'sha256sum', f.sha256sum, ext_sha256sum)
239
240     def check(self, upload):
241         cnf = Config()
242
243         if not cnf.use_extfiles:
244             return
245
246         session = upload.session
247         changes = upload.changes
248
249         for f in changes.files.itervalues():
250             self.check_single(session, f)
251         source = changes.source
252         if source is not None:
253             for f in source.files.itervalues():
254                 self.check_single(session, f)
255
256 class BinaryCheck(Check):
257     """Check binary packages for syntax errors."""
258     def check(self, upload):
259         for binary in upload.changes.binaries:
260             self.check_binary(upload, binary)
261
262         binary_names = set([ binary.control['Package'] for binary in upload.changes.binaries ])
263         for bn in binary_names:
264             if bn not in upload.changes.binary_names:
265                 raise Reject('Package {0} is not mentioned in Binary field in changes'.format(bn))
266
267         return True
268
269     def check_binary(self, upload, binary):
270         fn = binary.hashed_file.filename
271         control = binary.control
272
273         for field in ('Package', 'Architecture', 'Version', 'Description'):
274             if field not in control:
275                 raise Reject('{0}: Missing mandatory field {0}.'.format(fn, field))
276
277         check_fields_for_valid_utf8(fn, control)
278
279         # check fields
280
281         package = control['Package']
282         if not re_field_package.match(package):
283             raise Reject('{0}: Invalid Package field'.format(fn))
284
285         version = control['Version']
286         version_match = re_field_version.match(version)
287         if not version_match:
288             raise Reject('{0}: Invalid Version field'.format(fn))
289         version_without_epoch = version_match.group('without_epoch')
290
291         architecture = control['Architecture']
292         if architecture not in upload.changes.architectures:
293             raise Reject('{0}: Architecture not in Architecture field in changes file'.format(fn))
294         if architecture == 'source':
295             raise Reject('{0}: Architecture "source" invalid for binary packages'.format(fn))
296
297         source = control.get('Source')
298         if source is not None and not re_field_source.match(source):
299             raise Reject('{0}: Invalid Source field'.format(fn))
300
301         # check filename
302
303         match = re_file_binary.match(fn)
304         if package != match.group('package'):
305             raise Reject('{0}: filename does not match Package field'.format(fn))
306         if version_without_epoch != match.group('version'):
307             raise Reject('{0}: filename does not match Version field'.format(fn))
308         if architecture != match.group('architecture'):
309             raise Reject('{0}: filename does not match Architecture field'.format(fn))
310
311         # check dependency field syntax
312
313         for field in ('Breaks', 'Conflicts', 'Depends', 'Enhances', 'Pre-Depends',
314                       'Provides', 'Recommends', 'Replaces', 'Suggests'):
315             value = control.get(field)
316             if value is not None:
317                 if value.strip() == '':
318                     raise Reject('{0}: empty {1} field'.format(fn, field))
319                 try:
320                     apt_pkg.parse_depends(value)
321                 except:
322                     raise Reject('{0}: APT could not parse {1} field'.format(fn, field))
323
324         for field in ('Built-Using',):
325             value = control.get(field)
326             if value is not None:
327                 if value.strip() == '':
328                     raise Reject('{0}: empty {1} field'.format(fn, field))
329                 try:
330                     apt_pkg.parse_src_depends(value)
331                 except:
332                     raise Reject('{0}: APT could not parse {1} field'.format(fn, field))
333
334 class BinaryTimestampCheck(Check):
335     """check timestamps of files in binary packages
336
337     Files in the near future cause ugly warnings and extreme time travel
338     can cause errors on extraction.
339     """
340     def check(self, upload):
341         cnf = Config()
342         future_cutoff = time.time() + cnf.find_i('Dinstall::FutureTimeTravelGrace', 24*3600)
343         past_cutoff = time.mktime(time.strptime(cnf.find('Dinstall::PastCutoffYear', '1975'), '%Y'))
344
345         class TarTime(object):
346             def __init__(self):
347                 self.future_files = dict()
348                 self.past_files = dict()
349             def callback(self, member, data):
350                 if member.mtime > future_cutoff:
351                     self.future_files[member.name] = member.mtime
352                 elif member.mtime < past_cutoff:
353                     self.past_files[member.name] = member.mtime
354
355         def format_reason(filename, direction, files):
356             reason = "{0}: has {1} file(s) with a timestamp too far in the {2}:\n".format(filename, len(files), direction)
357             for fn, ts in files.iteritems():
358                 reason += "  {0} ({1})".format(fn, time.ctime(ts))
359             return reason
360
361         for binary in upload.changes.binaries:
362             filename = binary.hashed_file.filename
363             path = os.path.join(upload.directory, filename)
364             deb = apt_inst.DebFile(path)
365             tar = TarTime()
366             deb.control.go(tar.callback)
367             if tar.future_files:
368                 raise Reject(format_reason(filename, 'future', tar.future_files))
369             if tar.past_files:
370                 raise Reject(format_reason(filename, 'past', tar.past_files))
371
372 class SourceCheck(Check):
373     """Check source package for syntax errors."""
374     def check_filename(self, control, filename, regex):
375         # In case we have an .orig.tar.*, we have to strip the Debian revison
376         # from the version number. So handle this special case first.
377         is_orig = True
378         match = re_file_orig.match(filename)
379         if not match:
380             is_orig = False
381             match = regex.match(filename)
382
383         if not match:
384             raise Reject('{0}: does not match regular expression for source filenames'.format(filename))
385         if match.group('package') != control['Source']:
386             raise Reject('{0}: filename does not match Source field'.format(filename))
387
388         version = control['Version']
389         if is_orig:
390             upstream_match = re_field_version_upstream.match(version)
391             if not upstream_match:
392                 raise Reject('{0}: Source package includes upstream tarball, but {0} has no Debian revision.'.format(filename, version))
393             version = upstream_match.group('upstream')
394         version_match =  re_field_version.match(version)
395         version_without_epoch = version_match.group('without_epoch')
396         if match.group('version') != version_without_epoch:
397             raise Reject('{0}: filename does not match Version field'.format(filename))
398
399     def check(self, upload):
400         if upload.changes.source is None:
401             return True
402
403         changes = upload.changes.changes
404         source = upload.changes.source
405         control = source.dsc
406         dsc_fn = source._dsc_file.filename
407
408         check_fields_for_valid_utf8(dsc_fn, control)
409
410         # check fields
411         if not re_field_package.match(control['Source']):
412             raise Reject('{0}: Invalid Source field'.format(dsc_fn))
413         if control['Source'] != changes['Source']:
414             raise Reject('{0}: Source field does not match Source field in changes'.format(dsc_fn))
415         if control['Version'] != changes['Version']:
416             raise Reject('{0}: Version field does not match Version field in changes'.format(dsc_fn))
417
418         # check filenames
419         self.check_filename(control, dsc_fn, re_file_dsc)
420         for f in source.files.itervalues():
421             self.check_filename(control, f.filename, re_file_source)
422
423         # check dependency field syntax
424         for field in ('Build-Conflicts', 'Build-Conflicts-Indep', 'Build-Depends', 'Build-Depends-Arch', 'Build-Depends-Indep'):
425             value = control.get(field)
426             if value is not None:
427                 if value.strip() == '':
428                     raise Reject('{0}: empty {1} field'.format(dsc_fn, field))
429                 try:
430                     apt_pkg.parse_src_depends(value)
431                 except Exception as e:
432                     raise Reject('{0}: APT could not parse {1} field: {2}'.format(dsc_fn, field, e))
433
434         rejects = utils.check_dsc_files(dsc_fn, control, source.files.keys())
435         if len(rejects) > 0:
436             raise Reject("\n".join(rejects))
437
438         return True
439
440 class SingleDistributionCheck(Check):
441     """Check that the .changes targets only a single distribution."""
442     def check(self, upload):
443         if len(upload.changes.distributions) != 1:
444             raise Reject("Only uploads to a single distribution are allowed.")
445
446 class ACLCheck(Check):
447     """Check the uploader is allowed to upload the packages in .changes"""
448
449     def _does_hijack(self, session, upload, suite):
450         # Try to catch hijacks.
451         # This doesn't work correctly. Uploads to experimental can still
452         # "hijack" binaries from unstable. Also one can hijack packages
453         # via buildds (but people who try this should not be DMs).
454         for binary_name in upload.changes.binary_names:
455             binaries = session.query(DBBinary).join(DBBinary.source) \
456                 .filter(DBBinary.suites.contains(suite)) \
457                 .filter(DBBinary.package == binary_name)
458             for binary in binaries:
459                 if binary.source.source != upload.changes.changes['Source']:
460                     return True, binary.package, binary.source.source
461         return False, None, None
462
463     def _check_acl(self, session, upload, acl):
464         source_name = upload.changes.source_name
465
466         if acl.match_fingerprint and upload.fingerprint not in acl.fingerprints:
467             return None, None
468         if acl.match_keyring is not None and upload.fingerprint.keyring != acl.match_keyring:
469             return None, None
470
471         if not acl.allow_new:
472             if upload.new:
473                 return False, "NEW uploads are not allowed"
474             for f in upload.changes.files.itervalues():
475                 if f.section == 'byhand' or f.section.startswith("raw-"):
476                     return False, "BYHAND uploads are not allowed"
477         if not acl.allow_source and upload.changes.source is not None:
478             return False, "sourceful uploads are not allowed"
479         binaries = upload.changes.binaries
480         if len(binaries) != 0:
481             if not acl.allow_binary:
482                 return False, "binary uploads are not allowed"
483             if upload.changes.source is None and not acl.allow_binary_only:
484                 return False, "binary-only uploads are not allowed"
485             if not acl.allow_binary_all:
486                 uploaded_arches = set(upload.changes.architectures)
487                 uploaded_arches.discard('source')
488                 allowed_arches = set(a.arch_string for a in acl.architectures)
489                 forbidden_arches = uploaded_arches - allowed_arches
490                 if len(forbidden_arches) != 0:
491                     return False, "uploads for architecture(s) {0} are not allowed".format(", ".join(forbidden_arches))
492         if not acl.allow_hijack:
493             for suite in upload.final_suites:
494                 does_hijack, hijacked_binary, hijacked_from = self._does_hijack(session, upload, suite)
495                 if does_hijack:
496                     return False, "hijacks are not allowed (binary={0}, other-source={1})".format(hijacked_binary, hijacked_from)
497
498         acl_per_source = session.query(ACLPerSource).filter_by(acl=acl, fingerprint=upload.fingerprint, source=source_name).first()
499         if acl.allow_per_source:
500             if acl_per_source is None:
501                 return False, "not allowed to upload source package '{0}'".format(source_name)
502         if acl.deny_per_source and acl_per_source is not None:
503             return False, acl_per_source.reason or "forbidden to upload source package '{0}'".format(source_name)
504
505         return True, None
506
507     def check(self, upload):
508         session = upload.session
509         fingerprint = upload.fingerprint
510         keyring = fingerprint.keyring
511
512         if keyring is None:
513             raise Reject('No keyring for fingerprint {0}'.format(fingerprint.fingerprint))
514         if not keyring.active:
515             raise Reject('Keyring {0} is not active'.format(keyring.name))
516
517         acl = fingerprint.acl or keyring.acl
518         if acl is None:
519             raise Reject('No ACL for fingerprint {0}'.format(fingerprint.fingerprint))
520         result, reason = self._check_acl(session, upload, acl)
521         if not result:
522             raise RejectACL(acl, reason)
523
524         for acl in session.query(ACL).filter_by(is_global=True):
525             result, reason = self._check_acl(session, upload, acl)
526             if result == False:
527                 raise RejectACL(acl, reason)
528
529         return True
530
531     def per_suite_check(self, upload, suite):
532         acls = suite.acls
533         if len(acls) != 0:
534             accept = False
535             for acl in acls:
536                 result, reason = self._check_acl(upload.session, upload, acl)
537                 if result == False:
538                     raise Reject(reason)
539                 accept = accept or result
540             if not accept:
541                 raise Reject('Not accepted by any per-suite acl (suite={0})'.format(suite.suite_name))
542         return True
543
544 class TransitionCheck(Check):
545     """check for a transition"""
546     def check(self, upload):
547         if 'source' not in upload.changes.architectures:
548             return True
549
550         transitions = self.get_transitions()
551         if transitions is None:
552             return True
553
554         session = upload.session
555
556         control = upload.changes.changes
557         source = re_field_source.match(control['Source']).group('package')
558
559         for trans in transitions:
560             t = transitions[trans]
561             source = t["source"]
562             expected = t["new"]
563
564             # Will be None if nothing is in testing.
565             current = get_source_in_suite(source, "testing", session)
566             if current is not None:
567                 compare = apt_pkg.version_compare(current.version, expected)
568
569             if current is None or compare < 0:
570                 # This is still valid, the current version in testing is older than
571                 # the new version we wait for, or there is none in testing yet
572
573                 # Check if the source we look at is affected by this.
574                 if source in t['packages']:
575                     # The source is affected, lets reject it.
576
577                     rejectmsg = "{0}: part of the {1} transition.\n\n".format(source, trans)
578
579                     if current is not None:
580                         currentlymsg = "at version {0}".format(current.version)
581                     else:
582                         currentlymsg = "not present in testing"
583
584                     rejectmsg += "Transition description: {0}\n\n".format(t["reason"])
585
586                     rejectmsg += "\n".join(textwrap.wrap("""Your package
587 is part of a testing transition designed to get {0} migrated (it is
588 currently {1}, we need version {2}).  This transition is managed by the
589 Release Team, and {3} is the Release-Team member responsible for it.
590 Please mail debian-release@lists.debian.org or contact {3} directly if you
591 need further assistance.  You might want to upload to experimental until this
592 transition is done.""".format(source, currentlymsg, expected,t["rm"])))
593
594                     raise Reject(rejectmsg)
595
596         return True
597
598     def get_transitions(self):
599         cnf = Config()
600         path = cnf.get('Dinstall::ReleaseTransitions', '')
601         if path == '' or not os.path.exists(path):
602             return None
603
604         contents = file(path, 'r').read()
605         try:
606             transitions = yaml.safe_load(contents)
607             return transitions
608         except yaml.YAMLError as msg:
609             utils.warn('Not checking transitions, the transitions file is broken: {0}'.format(msg))
610
611         return None
612
613 class NoSourceOnlyCheck(Check):
614     """Check for source-only upload
615
616     Source-only uploads are only allowed if Dinstall::AllowSourceOnlyUploads is
617     set. Otherwise they are rejected.
618     """
619     def check(self, upload):
620         if Config().find_b("Dinstall::AllowSourceOnlyUploads"):
621             return True
622         changes = upload.changes
623         if changes.source is not None and len(changes.binaries) == 0:
624             raise Reject('Source-only uploads are not allowed.')
625         return True
626
627 class LintianCheck(Check):
628     """Check package using lintian"""
629     def check(self, upload):
630         changes = upload.changes
631
632         # Only check sourceful uploads.
633         if changes.source is None:
634             return True
635         # Only check uploads to unstable or experimental.
636         if 'unstable' not in changes.distributions and 'experimental' not in changes.distributions:
637             return True
638
639         cnf = Config()
640         if 'Dinstall::LintianTags' not in cnf:
641             return True
642         tagfile = cnf['Dinstall::LintianTags']
643
644         with open(tagfile, 'r') as sourcefile:
645             sourcecontent = sourcefile.read()
646         try:
647             lintiantags = yaml.safe_load(sourcecontent)['lintian']
648         except yaml.YAMLError as msg:
649             raise Exception('Could not read lintian tags file {0}, YAML error: {1}'.format(tagfile, msg))
650
651         fd, temp_filename = utils.temp_filename(mode=0o644)
652         temptagfile = os.fdopen(fd, 'w')
653         for tags in lintiantags.itervalues():
654             for tag in tags:
655                 print >>temptagfile, tag
656         temptagfile.close()
657
658         changespath = os.path.join(upload.directory, changes.filename)
659         try:
660             cmd = []
661             result = 0
662
663             user = cnf.get('Dinstall::UnprivUser') or None
664             if user is not None:
665                 cmd.extend(['sudo', '-H', '-u', user])
666
667             cmd.extend(['/usr/bin/lintian', '--show-overrides', '--tags-from-file', temp_filename, changespath])
668             output = daklib.daksubprocess.check_output(cmd, stderr=subprocess.STDOUT)
669         except subprocess.CalledProcessError as e:
670             result = e.returncode
671             output = e.output
672         finally:
673             os.unlink(temp_filename)
674
675         if result == 2:
676             utils.warn("lintian failed for %s [return code: %s]." % \
677                 (changespath, result))
678             utils.warn(utils.prefix_multi_line_string(output, \
679                 " [possible output:] "))
680
681         parsed_tags = lintian.parse_lintian_output(output)
682         rejects = list(lintian.generate_reject_messages(parsed_tags, lintiantags))
683         if len(rejects) != 0:
684             raise Reject('\n'.join(rejects))
685
686         return True
687
688 class SourceFormatCheck(Check):
689     """Check source format is allowed in the target suite"""
690     def per_suite_check(self, upload, suite):
691         source = upload.changes.source
692         session = upload.session
693         if source is None:
694             return True
695
696         source_format = source.dsc['Format']
697         query = session.query(SrcFormat).filter_by(format_name=source_format).filter(SrcFormat.suites.contains(suite))
698         if query.first() is None:
699             raise Reject('source format {0} is not allowed in suite {1}'.format(source_format, suite.suite_name))
700
701 class SuiteArchitectureCheck(Check):
702     def per_suite_check(self, upload, suite):
703         session = upload.session
704         for arch in upload.changes.architectures:
705             query = session.query(Architecture).filter_by(arch_string=arch).filter(Architecture.suites.contains(suite))
706             if query.first() is None:
707                 raise Reject('Architecture {0} is not allowed in suite {1}'.format(arch, suite.suite_name))
708
709         return True
710
711 class VersionCheck(Check):
712     """Check version constraints"""
713     def _highest_source_version(self, session, source_name, suite):
714         db_source = session.query(DBSource).filter_by(source=source_name) \
715             .filter(DBSource.suites.contains(suite)).order_by(DBSource.version.desc()).first()
716         if db_source is None:
717             return None
718         else:
719             return db_source.version
720
721     def _highest_binary_version(self, session, binary_name, suite, architecture):
722         db_binary = session.query(DBBinary).filter_by(package=binary_name) \
723             .filter(DBBinary.suites.contains(suite)) \
724             .join(DBBinary.architecture) \
725             .filter(Architecture.arch_string.in_(['all', architecture])) \
726             .order_by(DBBinary.version.desc()).first()
727         if db_binary is None:
728             return None
729         else:
730             return db_binary.version
731
732     def _version_checks(self, upload, suite, other_suite, op, op_name):
733         session = upload.session
734
735         if upload.changes.source is not None:
736             source_name = upload.changes.source.dsc['Source']
737             source_version = upload.changes.source.dsc['Version']
738             v = self._highest_source_version(session, source_name, other_suite)
739             if v is not None and not op(version_compare(source_version, v)):
740                 raise Reject("Version check failed:\n"
741                              "Your upload included the source package {0}, version {1},\n"
742                              "however {3} already has version {2}.\n"
743                              "Uploads to {5} must have a {4} version than present in {3}."
744                              .format(source_name, source_version, v, other_suite.suite_name, op_name, suite.suite_name))
745
746         for binary in upload.changes.binaries:
747             binary_name = binary.control['Package']
748             binary_version = binary.control['Version']
749             architecture = binary.control['Architecture']
750             v = self._highest_binary_version(session, binary_name, other_suite, architecture)
751             if v is not None and not op(version_compare(binary_version, v)):
752                 raise Reject("Version check failed:\n"
753                              "Your upload included the binary package {0}, version {1}, for {2},\n"
754                              "however {4} already has version {3}.\n"
755                              "Uploads to {6} must have a {5} version than present in {4}."
756                              .format(binary_name, binary_version, architecture, v, other_suite.suite_name, op_name, suite.suite_name))
757
758     def per_suite_check(self, upload, suite):
759         session = upload.session
760
761         vc_newer = session.query(dbconn.VersionCheck).filter_by(suite=suite) \
762             .filter(dbconn.VersionCheck.check.in_(['MustBeNewerThan', 'Enhances']))
763         must_be_newer_than = [ vc.reference for vc in vc_newer ]
764         # Must be newer than old versions in `suite`
765         must_be_newer_than.append(suite)
766
767         for s in must_be_newer_than:
768             self._version_checks(upload, suite, s, lambda result: result > 0, 'higher')
769
770         vc_older = session.query(dbconn.VersionCheck).filter_by(suite=suite, check='MustBeOlderThan')
771         must_be_older_than = [ vc.reference for vc in vc_older ]
772
773         for s in must_be_older_than:
774             self._version_checks(upload, suite, s, lambda result: result < 0, 'lower')
775
776         return True
777
778     @property
779     def forcable(self):
780         return True