]> git.decadent.org.uk Git - dak.git/blob - daklib/archive.py
e4140a1266d403138cd0ff9de5a454682474d484
[dak.git] / daklib / archive.py
1 # Copyright (C) 2012, Ansgar Burchardt <ansgar@debian.org>
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16
17 """module to manipulate the archive
18
19 This module provides classes to manipulate the archive.
20 """
21
22 from daklib.dbconn import *
23 import daklib.checks as checks
24 from daklib.config import Config
25 import daklib.upload as upload
26 import daklib.utils as utils
27 from daklib.fstransactions import FilesystemTransaction
28 from daklib.regexes import re_changelog_versions, re_bin_only_nmu
29 import daklib.daksubprocess
30
31 import apt_pkg
32 from datetime import datetime
33 import os
34 import shutil
35 from sqlalchemy.orm.exc import NoResultFound
36 from sqlalchemy.orm import object_session
37 import sqlalchemy.exc
38 import tempfile
39 import traceback
40
41 class ArchiveException(Exception):
42     pass
43
44 class HashMismatchException(ArchiveException):
45     pass
46
47 class ArchiveTransaction(object):
48     """manipulate the archive in a transaction
49     """
50     def __init__(self):
51         self.fs = FilesystemTransaction()
52         self.session = DBConn().session()
53
54     def get_file(self, hashed_file, source_name, check_hashes=True):
55         """Look for file C{hashed_file} in database
56
57         @type  hashed_file: L{daklib.upload.HashedFile}
58         @param hashed_file: file to look for in the database
59
60         @type  source_name: str
61         @param source_name: source package name
62
63         @type  check_hashes: bool
64         @param check_hashes: check size and hashes match
65
66         @raise KeyError: file was not found in the database
67         @raise HashMismatchException: hash mismatch
68
69         @rtype:  L{daklib.dbconn.PoolFile}
70         @return: database entry for the file
71         """
72         poolname = os.path.join(utils.poolify(source_name), hashed_file.filename)
73         try:
74             poolfile = self.session.query(PoolFile).filter_by(filename=poolname).one()
75             if check_hashes and (poolfile.filesize != hashed_file.size
76                                  or poolfile.md5sum != hashed_file.md5sum
77                                  or poolfile.sha1sum != hashed_file.sha1sum
78                                  or poolfile.sha256sum != hashed_file.sha256sum):
79                 raise HashMismatchException('{0}: Does not match file already existing in the pool.'.format(hashed_file.filename))
80             return poolfile
81         except NoResultFound:
82             raise KeyError('{0} not found in database.'.format(poolname))
83
84     def _install_file(self, directory, hashed_file, archive, component, source_name):
85         """Install a file
86
87         Will not give an error when the file is already present.
88
89         @rtype:  L{daklib.dbconn.PoolFile}
90         @return: database object for the new file
91         """
92         session = self.session
93
94         poolname = os.path.join(utils.poolify(source_name), hashed_file.filename)
95         try:
96             poolfile = self.get_file(hashed_file, source_name)
97         except KeyError:
98             poolfile = PoolFile(filename=poolname, filesize=hashed_file.size)
99             poolfile.md5sum = hashed_file.md5sum
100             poolfile.sha1sum = hashed_file.sha1sum
101             poolfile.sha256sum = hashed_file.sha256sum
102             session.add(poolfile)
103             session.flush()
104
105         try:
106             session.query(ArchiveFile).filter_by(archive=archive, component=component, file=poolfile).one()
107         except NoResultFound:
108             archive_file = ArchiveFile(archive, component, poolfile)
109             session.add(archive_file)
110             session.flush()
111
112             path = os.path.join(archive.path, 'pool', component.component_name, poolname)
113             hashed_file_path = os.path.join(directory, hashed_file.input_filename)
114             self.fs.copy(hashed_file_path, path, link=False, mode=archive.mode)
115
116         return poolfile
117
118     def install_binary(self, directory, binary, suite, component, allow_tainted=False, fingerprint=None, source_suites=None, extra_source_archives=None):
119         """Install a binary package
120
121         @type  directory: str
122         @param directory: directory the binary package is located in
123
124         @type  binary: L{daklib.upload.Binary}
125         @param binary: binary package to install
126
127         @type  suite: L{daklib.dbconn.Suite}
128         @param suite: target suite
129
130         @type  component: L{daklib.dbconn.Component}
131         @param component: target component
132
133         @type  allow_tainted: bool
134         @param allow_tainted: allow to copy additional files from tainted archives
135
136         @type  fingerprint: L{daklib.dbconn.Fingerprint}
137         @param fingerprint: optional fingerprint
138
139         @type  source_suites: SQLAlchemy subquery for C{daklib.dbconn.Suite} or C{True}
140         @param source_suites: suites to copy the source from if they are not
141                               in C{suite} or C{True} to allow copying from any
142                               suite.
143
144         @type  extra_source_archives: list of L{daklib.dbconn.Archive}
145         @param extra_source_archives: extra archives to copy Built-Using sources from
146
147         @rtype:  L{daklib.dbconn.DBBinary}
148         @return: databse object for the new package
149         """
150         session = self.session
151         control = binary.control
152         maintainer = get_or_set_maintainer(control['Maintainer'], session)
153         architecture = get_architecture(control['Architecture'], session)
154
155         (source_name, source_version) = binary.source
156         source_query = session.query(DBSource).filter_by(source=source_name, version=source_version)
157         source = source_query.filter(DBSource.suites.contains(suite)).first()
158         if source is None:
159             if source_suites != True:
160                 source_query = source_query.join(DBSource.suites) \
161                     .filter(Suite.suite_id == source_suites.c.id)
162             source = source_query.first()
163             if source is None:
164                 raise ArchiveException('{0}: trying to install to {1}, but could not find source'.format(binary.hashed_file.filename, suite.suite_name))
165             self.copy_source(source, suite, component)
166
167         db_file = self._install_file(directory, binary.hashed_file, suite.archive, component, source_name)
168
169         unique = dict(
170             package=control['Package'],
171             version=control['Version'],
172             architecture=architecture,
173             )
174         rest = dict(
175             source=source,
176             maintainer=maintainer,
177             poolfile=db_file,
178             binarytype=binary.type,
179             )
180         # Other attributes that are ignored for purposes of equality with
181         # an existing source
182         rest2 = dict(
183             fingerprint=fingerprint,
184             )
185
186         try:
187             db_binary = session.query(DBBinary).filter_by(**unique).one()
188             for key, value in rest.iteritems():
189                 if getattr(db_binary, key) != value:
190                     raise ArchiveException('{0}: Does not match binary in database.'.format(binary.hashed_file.filename))
191         except NoResultFound:
192             db_binary = DBBinary(**unique)
193             for key, value in rest.iteritems():
194                 setattr(db_binary, key, value)
195             for key, value in rest2.iteritems():
196                 setattr(db_binary, key, value)
197             session.add(db_binary)
198             session.flush()
199             import_metadata_into_db(db_binary, session)
200
201             self._add_built_using(db_binary, binary.hashed_file.filename, control, suite, extra_archives=extra_source_archives)
202
203         if suite not in db_binary.suites:
204             db_binary.suites.append(suite)
205
206         session.flush()
207
208         return db_binary
209
210     def _ensure_extra_source_exists(self, filename, source, archive, extra_archives=None):
211         """ensure source exists in the given archive
212
213         This is intended to be used to check that Built-Using sources exist.
214
215         @type  filename: str
216         @param filename: filename to use in error messages
217
218         @type  source: L{daklib.dbconn.DBSource}
219         @param source: source to look for
220
221         @type  archive: L{daklib.dbconn.Archive}
222         @param archive: archive to look in
223
224         @type  extra_archives: list of L{daklib.dbconn.Archive}
225         @param extra_archives: list of archives to copy the source package from
226                                if it is not yet present in C{archive}
227         """
228         session = self.session
229         db_file = session.query(ArchiveFile).filter_by(file=source.poolfile, archive=archive).first()
230         if db_file is not None:
231             return True
232
233         # Try to copy file from one extra archive
234         if extra_archives is None:
235             extra_archives = []
236         db_file = session.query(ArchiveFile).filter_by(file=source.poolfile).filter(ArchiveFile.archive_id.in_([ a.archive_id for a in extra_archives])).first()
237         if db_file is None:
238             raise ArchiveException('{0}: Built-Using refers to package {1} (= {2}) not in target archive {3}.'.format(filename, source.source, source.version, archive.archive_name))
239
240         source_archive = db_file.archive
241         for dsc_file in source.srcfiles:
242             af = session.query(ArchiveFile).filter_by(file=dsc_file.poolfile, archive=source_archive, component=db_file.component).one()
243             # We were given an explicit list of archives so it is okay to copy from tainted archives.
244             self._copy_file(af.file, archive, db_file.component, allow_tainted=True)
245
246     def _add_built_using(self, db_binary, filename, control, suite, extra_archives=None):
247         """Add Built-Using sources to C{db_binary.extra_sources}
248         """
249         session = self.session
250
251         for bu_source_name, bu_source_version in daklib.utils.parse_built_using(control):
252             bu_source = session.query(DBSource).filter_by(source=bu_source_name, version=bu_source_version).first()
253             if bu_source is None:
254                 raise ArchiveException('{0}: Built-Using refers to non-existing source package {1} (= {2})'.format(filename, bu_source_name, bu_source_version))
255
256             self._ensure_extra_source_exists(filename, bu_source, suite.archive, extra_archives=extra_archives)
257
258             db_binary.extra_sources.append(bu_source)
259
260     def install_source_to_archive(self, directory, source, archive, component, changed_by, allow_tainted=False, fingerprint=None):
261         session = self.session
262         control = source.dsc
263         maintainer = get_or_set_maintainer(control['Maintainer'], session)
264         source_name = control['Source']
265
266         ### Add source package to database
267
268         # We need to install the .dsc first as the DBSource object refers to it.
269         db_file_dsc = self._install_file(directory, source._dsc_file, archive, component, source_name)
270
271         unique = dict(
272             source=source_name,
273             version=control['Version'],
274             )
275         rest = dict(
276             maintainer=maintainer,
277             poolfile=db_file_dsc,
278             dm_upload_allowed=(control.get('DM-Upload-Allowed', 'no') == 'yes'),
279             )
280         # Other attributes that are ignored for purposes of equality with
281         # an existing source
282         rest2 = dict(
283             changedby=changed_by,
284             fingerprint=fingerprint,
285             )
286
287         created = False
288         try:
289             db_source = session.query(DBSource).filter_by(**unique).one()
290             for key, value in rest.iteritems():
291                 if getattr(db_source, key) != value:
292                     raise ArchiveException('{0}: Does not match source in database.'.format(source._dsc_file.filename))
293         except NoResultFound:
294             created = True
295             db_source = DBSource(**unique)
296             for key, value in rest.iteritems():
297                 setattr(db_source, key, value)
298             for key, value in rest2.iteritems():
299                 setattr(db_source, key, value)
300             session.add(db_source)
301             session.flush()
302
303             # Add .dsc file. Other files will be added later.
304             db_dsc_file = DSCFile()
305             db_dsc_file.source = db_source
306             db_dsc_file.poolfile = db_file_dsc
307             session.add(db_dsc_file)
308             session.flush()
309
310         if not created:
311             for f in db_source.srcfiles:
312                 self._copy_file(f.poolfile, archive, component, allow_tainted=allow_tainted)
313             return db_source
314
315         ### Now add remaining files and copy them to the archive.
316
317         for hashed_file in source.files.itervalues():
318             hashed_file_path = os.path.join(directory, hashed_file.input_filename)
319             if os.path.exists(hashed_file_path):
320                 db_file = self._install_file(directory, hashed_file, archive, component, source_name)
321                 session.add(db_file)
322             else:
323                 db_file = self.get_file(hashed_file, source_name)
324                 self._copy_file(db_file, archive, component, allow_tainted=allow_tainted)
325
326             db_dsc_file = DSCFile()
327             db_dsc_file.source = db_source
328             db_dsc_file.poolfile = db_file
329             session.add(db_dsc_file)
330
331         session.flush()
332
333         # Importing is safe as we only arrive here when we did not find the source already installed earlier.
334         import_metadata_into_db(db_source, session)
335
336         # Uploaders are the maintainer and co-maintainers from the Uploaders field
337         db_source.uploaders.append(maintainer)
338         if 'Uploaders' in control:
339             from daklib.textutils import split_uploaders
340             for u in split_uploaders(control['Uploaders']):
341                 db_source.uploaders.append(get_or_set_maintainer(u, session))
342         session.flush()
343
344         return db_source
345
346     def install_source(self, directory, source, suite, component, changed_by, allow_tainted=False, fingerprint=None):
347         """Install a source package
348
349         @type  directory: str
350         @param directory: directory the source package is located in
351
352         @type  source: L{daklib.upload.Source}
353         @param source: source package to install
354
355         @type  suite: L{daklib.dbconn.Suite}
356         @param suite: target suite
357
358         @type  component: L{daklib.dbconn.Component}
359         @param component: target component
360
361         @type  changed_by: L{daklib.dbconn.Maintainer}
362         @param changed_by: person who prepared this version of the package
363
364         @type  allow_tainted: bool
365         @param allow_tainted: allow to copy additional files from tainted archives
366
367         @type  fingerprint: L{daklib.dbconn.Fingerprint}
368         @param fingerprint: optional fingerprint
369
370         @rtype:  L{daklib.dbconn.DBSource}
371         @return: database object for the new source
372         """
373         db_source = self.install_source_to_archive(directory, source, suite.archive, component, changed_by, allow_tainted, fingerprint)
374
375         if suite in db_source.suites:
376             return db_source
377         db_source.suites.append(suite)
378         self.session.flush()
379
380         return db_source
381
382     def _copy_file(self, db_file, archive, component, allow_tainted=False):
383         """Copy a file to the given archive and component
384
385         @type  db_file: L{daklib.dbconn.PoolFile}
386         @param db_file: file to copy
387
388         @type  archive: L{daklib.dbconn.Archive}
389         @param archive: target archive
390
391         @type  component: L{daklib.dbconn.Archive}
392         @param component: target component
393
394         @type  allow_tainted: bool
395         @param allow_tainted: allow to copy from tainted archives (such as NEW)
396         """
397         session = self.session
398
399         if session.query(ArchiveFile).filter_by(archive=archive, component=component, file=db_file).first() is None:
400             query = session.query(ArchiveFile).filter_by(file=db_file)
401             if not allow_tainted:
402                 query = query.join(Archive).filter(Archive.tainted == False)
403
404             source_af = query.first()
405             if source_af is None:
406                 raise ArchiveException('cp: Could not find {0} in any archive.'.format(db_file.filename))
407             target_af = ArchiveFile(archive, component, db_file)
408             session.add(target_af)
409             session.flush()
410             self.fs.copy(source_af.path, target_af.path, link=False, mode=archive.mode)
411
412     def copy_binary(self, db_binary, suite, component, allow_tainted=False, extra_archives=None):
413         """Copy a binary package to the given suite and component
414
415         @type  db_binary: L{daklib.dbconn.DBBinary}
416         @param db_binary: binary to copy
417
418         @type  suite: L{daklib.dbconn.Suite}
419         @param suite: target suite
420
421         @type  component: L{daklib.dbconn.Component}
422         @param component: target component
423
424         @type  allow_tainted: bool
425         @param allow_tainted: allow to copy from tainted archives (such as NEW)
426
427         @type  extra_archives: list of L{daklib.dbconn.Archive}
428         @param extra_archives: extra archives to copy Built-Using sources from
429         """
430         session = self.session
431         archive = suite.archive
432         if archive.tainted:
433             allow_tainted = True
434
435         filename = db_binary.poolfile.filename
436
437         # make sure source is present in target archive
438         db_source = db_binary.source
439         if session.query(ArchiveFile).filter_by(archive=archive, file=db_source.poolfile).first() is None:
440             raise ArchiveException('{0}: cannot copy to {1}: source is not present in target archive'.format(filename, suite.suite_name))
441
442         # make sure built-using packages are present in target archive
443         for db_source in db_binary.extra_sources:
444             self._ensure_extra_source_exists(filename, db_source, archive, extra_archives=extra_archives)
445
446         # copy binary
447         db_file = db_binary.poolfile
448         self._copy_file(db_file, suite.archive, component, allow_tainted=allow_tainted)
449         if suite not in db_binary.suites:
450             db_binary.suites.append(suite)
451         self.session.flush()
452
453     def copy_source(self, db_source, suite, component, allow_tainted=False):
454         """Copy a source package to the given suite and component
455
456         @type  db_source: L{daklib.dbconn.DBSource}
457         @param db_source: source to copy
458
459         @type  suite: L{daklib.dbconn.Suite}
460         @param suite: target suite
461
462         @type  component: L{daklib.dbconn.Component}
463         @param component: target component
464
465         @type  allow_tainted: bool
466         @param allow_tainted: allow to copy from tainted archives (such as NEW)
467         """
468         archive = suite.archive
469         if archive.tainted:
470             allow_tainted = True
471         for db_dsc_file in db_source.srcfiles:
472             self._copy_file(db_dsc_file.poolfile, archive, component, allow_tainted=allow_tainted)
473         if suite not in db_source.suites:
474             db_source.suites.append(suite)
475         self.session.flush()
476
477     def remove_file(self, db_file, archive, component):
478         """Remove a file from a given archive and component
479
480         @type  db_file: L{daklib.dbconn.PoolFile}
481         @param db_file: file to remove
482
483         @type  archive: L{daklib.dbconn.Archive}
484         @param archive: archive to remove the file from
485
486         @type  component: L{daklib.dbconn.Component}
487         @param component: component to remove the file from
488         """
489         af = self.session.query(ArchiveFile).filter_by(file=db_file, archive=archive, component=component)
490         self.fs.unlink(af.path)
491         self.session.delete(af)
492
493     def remove_binary(self, binary, suite):
494         """Remove a binary from a given suite and component
495
496         @type  binary: L{daklib.dbconn.DBBinary}
497         @param binary: binary to remove
498
499         @type  suite: L{daklib.dbconn.Suite}
500         @param suite: suite to remove the package from
501         """
502         binary.suites.remove(suite)
503         self.session.flush()
504
505     def remove_source(self, source, suite):
506         """Remove a source from a given suite and component
507
508         @type  source: L{daklib.dbconn.DBSource}
509         @param source: source to remove
510
511         @type  suite: L{daklib.dbconn.Suite}
512         @param suite: suite to remove the package from
513
514         @raise ArchiveException: source package is still referenced by other
515                                  binaries in the suite
516         """
517         session = self.session
518
519         query = session.query(DBBinary).filter_by(source=source) \
520             .filter(DBBinary.suites.contains(suite))
521         if query.first() is not None:
522             raise ArchiveException('src:{0} is still used by binaries in suite {1}'.format(source.source, suite.suite_name))
523
524         source.suites.remove(suite)
525         session.flush()
526
527     def commit(self):
528         """commit changes"""
529         try:
530             self.session.commit()
531             self.fs.commit()
532         finally:
533             self.session.rollback()
534             self.fs.rollback()
535
536     def rollback(self):
537         """rollback changes"""
538         self.session.rollback()
539         self.fs.rollback()
540
541     def flush(self):
542         self.session.flush()
543
544     def __enter__(self):
545         return self
546
547     def __exit__(self, type, value, traceback):
548         if type is None:
549             self.commit()
550         else:
551             self.rollback()
552         return None
553
554 def source_component_from_package_list(package_list, suite):
555     """Get component for a source package
556
557     This function will look at the Package-List field to determine the
558     component the source package belongs to. This is the first component
559     the source package provides binaries for (first with respect to the
560     ordering of components).
561
562     It the source package has no Package-List field, None is returned.
563
564     @type  package_list: L{daklib.packagelist.PackageList}
565     @param package_list: package list of the source to get the override for
566
567     @type  suite: L{daklib.dbconn.Suite}
568     @param suite: suite to consider for binaries produced
569
570     @rtype:  L{daklib.dbconn.Component} or C{None}
571     @return: component for the given source or C{None}
572     """
573     if package_list.fallback:
574         return None
575     session = object_session(suite)
576     packages = package_list.packages_for_suite(suite)
577     components = set(p.component for p in packages)
578     query = session.query(Component).order_by(Component.ordering) \
579             .filter(Component.component_name.in_(components))
580     return query.first()
581
582 class ArchiveUpload(object):
583     """handle an upload
584
585     This class can be used in a with-statement::
586
587        with ArchiveUpload(...) as upload:
588           ...
589
590     Doing so will automatically run any required cleanup and also rollback the
591     transaction if it was not committed.
592     """
593     def __init__(self, directory, changes, keyrings):
594         self.transaction = ArchiveTransaction()
595         """transaction used to handle the upload
596         @type: L{daklib.archive.ArchiveTransaction}
597         """
598
599         self.session = self.transaction.session
600         """database session"""
601
602         self.original_directory = directory
603         self.original_changes = changes
604
605         self.changes = None
606         """upload to process
607         @type: L{daklib.upload.Changes}
608         """
609
610         self.directory = None
611         """directory with temporary copy of files. set by C{prepare}
612         @type: str
613         """
614
615         self.keyrings = keyrings
616
617         self.fingerprint = self.session.query(Fingerprint).filter_by(fingerprint=changes.primary_fingerprint).one()
618         """fingerprint of the key used to sign the upload
619         @type: L{daklib.dbconn.Fingerprint}
620         """
621
622         self.reject_reasons = []
623         """reasons why the upload cannot by accepted
624         @type: list of str
625         """
626
627         self.warnings = []
628         """warnings
629         @note: Not used yet.
630         @type: list of str
631         """
632
633         self.final_suites = None
634
635         self.new = False
636         """upload is NEW. set by C{check}
637         @type: bool
638         """
639
640         self._checked = False
641         """checks passes. set by C{check}
642         @type: bool
643         """
644
645         self._new_queue = self.session.query(PolicyQueue).filter_by(queue_name='new').one()
646         self._new = self._new_queue.suite
647
648     def warn(self, message):
649         """add a warning message
650
651         Adds a warning message that can later be seen in C{self.warnings}
652
653         @type  message: string
654         @param message: warning message
655         """
656         self.warnings.append(message)
657
658     def prepare(self):
659         """prepare upload for further processing
660
661         This copies the files involved to a temporary directory.  If you use
662         this method directly, you have to remove the directory given by the
663         C{directory} attribute later on your own.
664
665         Instead of using the method directly, you can also use a with-statement::
666
667            with ArchiveUpload(...) as upload:
668               ...
669
670         This will automatically handle any required cleanup.
671         """
672         assert self.directory is None
673         assert self.original_changes.valid_signature
674
675         cnf = Config()
676         session = self.transaction.session
677
678         group = cnf.get('Dinstall::UnprivGroup') or None
679         self.directory = utils.temp_dirname(parent=cnf.get('Dir::TempPath'),
680                                             mode=0o2750, group=group)
681         with FilesystemTransaction() as fs:
682             src = os.path.join(self.original_directory, self.original_changes.filename)
683             dst = os.path.join(self.directory, self.original_changes.filename)
684             fs.copy(src, dst, mode=0o640)
685
686             self.changes = upload.Changes(self.directory, self.original_changes.filename, self.keyrings)
687
688             for f in self.changes.files.itervalues():
689                 src = os.path.join(self.original_directory, f.filename)
690                 dst = os.path.join(self.directory, f.filename)
691                 if not os.path.exists(src):
692                     continue
693                 fs.copy(src, dst, mode=0o640)
694
695             source = None
696             try:
697                 source = self.changes.source
698             except Exception:
699                 # Do not raise an exception here if the .dsc is invalid.
700                 pass
701
702             if source is not None:
703                 for f in source.files.itervalues():
704                     src = os.path.join(self.original_directory, f.filename)
705                     dst = os.path.join(self.directory, f.filename)
706                     if not os.path.exists(dst):
707                         try:
708                             db_file = self.transaction.get_file(f, source.dsc['Source'], check_hashes=False)
709                             db_archive_file = session.query(ArchiveFile).filter_by(file=db_file).first()
710                             fs.copy(db_archive_file.path, dst, mode=0o640)
711                         except KeyError:
712                             # Ignore if get_file could not find it. Upload will
713                             # probably be rejected later.
714                             pass
715
716     def unpacked_source(self):
717         """Path to unpacked source
718
719         Get path to the unpacked source. This method does unpack the source
720         into a temporary directory under C{self.directory} if it has not
721         been done so already.
722
723         @rtype:  str or C{None}
724         @return: string giving the path to the unpacked source directory
725                  or C{None} if no source was included in the upload.
726         """
727         assert self.directory is not None
728
729         source = self.changes.source
730         if source is None:
731             return None
732         dsc_path = os.path.join(self.directory, source._dsc_file.filename)
733
734         sourcedir = os.path.join(self.directory, 'source')
735         if not os.path.exists(sourcedir):
736             devnull = open('/dev/null', 'w')
737             daklib.daksubprocess.check_call(["dpkg-source", "--no-copy", "--no-check", "-x", dsc_path, sourcedir], shell=False, stdout=devnull)
738         if not os.path.isdir(sourcedir):
739             raise Exception("{0} is not a directory after extracting source package".format(sourcedir))
740         return sourcedir
741
742     def _map_suite(self, suite_name):
743         for rule in Config().value_list("SuiteMappings"):
744             fields = rule.split()
745             rtype = fields[0]
746             if rtype == "map" or rtype == "silent-map":
747                 (src, dst) = fields[1:3]
748                 if src == suite_name:
749                     suite_name = dst
750                     if rtype != "silent-map":
751                         self.warnings.append('Mapping {0} to {1}.'.format(src, dst))
752             elif rtype == "ignore":
753                 ignored = fields[1]
754                 if suite_name == ignored:
755                     self.warnings.append('Ignoring target suite {0}.'.format(ignored))
756                     suite_name = None
757             elif rtype == "reject":
758                 rejected = fields[1]
759                 if suite_name == rejected:
760                     raise checks.Reject('Uploads to {0} are not accepted.'.format(rejected))
761             ## XXX: propup-version and map-unreleased not yet implemented
762         return suite_name
763
764     def _mapped_suites(self):
765         """Get target suites after mappings
766
767         @rtype:  list of L{daklib.dbconn.Suite}
768         @return: list giving the mapped target suites of this upload
769         """
770         session = self.session
771
772         suite_names = []
773         for dist in self.changes.distributions:
774             suite_name = self._map_suite(dist)
775             if suite_name is not None:
776                 suite_names.append(suite_name)
777
778         suites = session.query(Suite).filter(Suite.suite_name.in_(suite_names))
779         return suites
780
781     def _check_new_binary_overrides(self, suite, overridesuite):
782         new = False
783         source = self.changes.source
784
785         if source is not None and not source.package_list.fallback:
786             packages = source.package_list.packages_for_suite(suite)
787             binaries = [ entry for entry in packages ]
788             for b in binaries:
789                 override = self._binary_override(overridesuite, b)
790                 if override is None:
791                     self.warnings.append('binary:{0} is NEW.'.format(b.name))
792                     new = True
793         else:
794             binaries = self.changes.binaries
795             for b in binaries:
796                 if utils.is_in_debug_section(b.control) and suite.debug_suite is not None:
797                     continue
798                 override = self._binary_override(overridesuite, b)
799                 if override is None:
800                     self.warnings.append('binary:{0} is NEW.'.format(b.name))
801                     new = True
802
803         return new
804
805     def _check_new(self, suite, overridesuite):
806         """Check if upload is NEW
807
808         An upload is NEW if it has binary or source packages that do not have
809         an override in C{overridesuite} OR if it references files ONLY in a
810         tainted archive (eg. when it references files in NEW).
811
812         Debug packages (*-dbgsym in Section: debug) are not considered as NEW
813         if C{suite} has a seperate debug suite.
814
815         @rtype:  bool
816         @return: C{True} if the upload is NEW, C{False} otherwise
817         """
818         session = self.session
819         new = False
820
821         # Check for missing overrides
822         if self._check_new_binary_overrides(suite, overridesuite):
823             new = True
824         if self.changes.source is not None:
825             override = self._source_override(overridesuite, self.changes.source)
826             if override is None:
827                 self.warnings.append('source:{0} is NEW.'.format(self.changes.source.dsc['Source']))
828                 new = True
829
830         # Check if we reference a file only in a tainted archive
831         files = self.changes.files.values()
832         if self.changes.source is not None:
833             files.extend(self.changes.source.files.values())
834         for f in files:
835             query = session.query(ArchiveFile).join(PoolFile).filter(PoolFile.sha1sum == f.sha1sum)
836             query_untainted = query.join(Archive).filter(Archive.tainted == False)
837
838             in_archive = (query.first() is not None)
839             in_untainted_archive = (query_untainted.first() is not None)
840
841             if in_archive and not in_untainted_archive:
842                 self.warnings.append('{0} is only available in NEW.'.format(f.filename))
843                 new = True
844
845         return new
846
847     def _final_suites(self):
848         session = self.session
849
850         mapped_suites = self._mapped_suites()
851         final_suites = set()
852
853         for suite in mapped_suites:
854             overridesuite = suite
855             if suite.overridesuite is not None:
856                 overridesuite = session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
857             if self._check_new(suite, overridesuite):
858                 self.new = True
859             final_suites.add(suite)
860
861         return final_suites
862
863     def _binary_override(self, suite, binary):
864         """Get override entry for a binary
865
866         @type  suite: L{daklib.dbconn.Suite}
867         @param suite: suite to get override for
868
869         @type  binary: L{daklib.upload.Binary} or L{daklib.packagelist.PackageListEntry}
870         @param binary: binary to get override for
871
872         @rtype:  L{daklib.dbconn.Override} or C{None}
873         @return: override for the given binary or C{None}
874         """
875         if suite.overridesuite is not None:
876             suite = self.session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
877
878         mapped_component = get_mapped_component(binary.component)
879         if mapped_component is None:
880             return None
881
882         query = self.session.query(Override).filter_by(suite=suite, package=binary.name) \
883                 .join(Component).filter(Component.component_name == mapped_component.component_name) \
884                 .join(OverrideType).filter(OverrideType.overridetype == binary.type)
885
886         try:
887             return query.one()
888         except NoResultFound:
889             return None
890
891     def _source_override(self, suite, source):
892         """Get override entry for a source
893
894         @type  suite: L{daklib.dbconn.Suite}
895         @param suite: suite to get override for
896
897         @type  source: L{daklib.upload.Source}
898         @param source: source to get override for
899
900         @rtype:  L{daklib.dbconn.Override} or C{None}
901         @return: override for the given source or C{None}
902         """
903         if suite.overridesuite is not None:
904             suite = self.session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
905
906         query = self.session.query(Override).filter_by(suite=suite, package=source.dsc['Source']) \
907                 .join(OverrideType).filter(OverrideType.overridetype == 'dsc')
908
909         component = source_component_from_package_list(source.package_list, suite)
910         if component is not None:
911             query = query.filter(Override.component == component)
912
913         try:
914             return query.one()
915         except NoResultFound:
916             return None
917
918     def _binary_component(self, suite, binary, only_overrides=True):
919         """get component for a binary
920
921         By default this will only look at overrides to get the right component;
922         if C{only_overrides} is C{False} this method will also look at the
923         Section field.
924
925         @type  suite: L{daklib.dbconn.Suite}
926
927         @type  binary: L{daklib.upload.Binary}
928
929         @type  only_overrides: bool
930         @param only_overrides: only use overrides to get the right component
931
932         @rtype: L{daklib.dbconn.Component} or C{None}
933         """
934         override = self._binary_override(suite, binary)
935         if override is not None:
936             return override.component
937         if only_overrides:
938             return None
939         return get_mapped_component(binary.component, self.session)
940
941     def check(self, force=False):
942         """run checks against the upload
943
944         @type  force: bool
945         @param force: ignore failing forcable checks
946
947         @rtype:  bool
948         @return: C{True} if all checks passed, C{False} otherwise
949         """
950         # XXX: needs to be better structured.
951         assert self.changes.valid_signature
952
953         try:
954             # Validate signatures and hashes before we do any real work:
955             for chk in (
956                     checks.SignatureAndHashesCheck,
957                     checks.SignatureTimestampCheck,
958                     checks.ChangesCheck,
959                     checks.ExternalHashesCheck,
960                     checks.SourceCheck,
961                     checks.BinaryCheck,
962                     checks.BinaryTimestampCheck,
963                     checks.SingleDistributionCheck,
964                     ):
965                 chk().check(self)
966
967             final_suites = self._final_suites()
968             if len(final_suites) == 0:
969                 self.reject_reasons.append('No target suite found. Please check your target distribution and that you uploaded to the right archive.')
970                 return False
971
972             self.final_suites = final_suites
973
974             for chk in (
975                     checks.TransitionCheck,
976                     checks.ACLCheck,
977                     checks.NoSourceOnlyCheck,
978                     checks.LintianCheck,
979                     ):
980                 chk().check(self)
981
982             for chk in (
983                     checks.ACLCheck,
984                     checks.SourceFormatCheck,
985                     checks.SuiteArchitectureCheck,
986                     checks.VersionCheck,
987                     ):
988                 for suite in final_suites:
989                     chk().per_suite_check(self, suite)
990
991             if len(self.reject_reasons) != 0:
992                 return False
993
994             self._checked = True
995             return True
996         except checks.Reject as e:
997             self.reject_reasons.append(unicode(e))
998         except Exception as e:
999             self.reject_reasons.append("Processing raised an exception: {0}.\n{1}".format(e, traceback.format_exc()))
1000         return False
1001
1002     def _install_to_suite(self, suite, source_component_func, binary_component_func, source_suites=None, extra_source_archives=None):
1003         """Install upload to the given suite
1004
1005         @type  suite: L{daklib.dbconn.Suite}
1006         @param suite: suite to install the package into. This is the real suite,
1007                       ie. after any redirection to NEW or a policy queue
1008
1009         @param source_component_func: function to get the L{daklib.dbconn.Component}
1010                                       for a L{daklib.upload.Source} object
1011
1012         @param binary_component_func: function to get the L{daklib.dbconn.Component}
1013                                       for a L{daklib.upload.Binary} object
1014
1015         @param source_suites: see L{daklib.archive.ArchiveTransaction.install_binary}
1016
1017         @param extra_source_archives: see L{daklib.archive.ArchiveTransaction.install_binary}
1018
1019         @return: tuple with two elements. The first is a L{daklib.dbconn.DBSource}
1020                  object for the install source or C{None} if no source was
1021                  included. The second is a list of L{daklib.dbconn.DBBinary}
1022                  objects for the installed binary packages.
1023         """
1024         # XXX: move this function to ArchiveTransaction?
1025
1026         control = self.changes.changes
1027         changed_by = get_or_set_maintainer(control.get('Changed-By', control['Maintainer']), self.session)
1028
1029         if source_suites is None:
1030             source_suites = self.session.query(Suite).join((VersionCheck, VersionCheck.reference_id == Suite.suite_id)).filter(VersionCheck.check == 'Enhances').filter(VersionCheck.suite == suite).subquery()
1031
1032         source = self.changes.source
1033         if source is not None:
1034             component = source_component_func(source)
1035             db_source = self.transaction.install_source(
1036                 self.directory,
1037                 source,
1038                 suite,
1039                 component,
1040                 changed_by,
1041                 fingerprint=self.fingerprint
1042             )
1043         else:
1044             db_source = None
1045
1046         db_binaries = []
1047         for binary in self.changes.binaries:
1048             copy_to_suite = suite
1049             if utils.is_in_debug_section(binary.control) and suite.debug_suite is not None:
1050                 copy_to_suite = suite.debug_suite
1051
1052             component = binary_component_func(binary)
1053             db_binary = self.transaction.install_binary(
1054                 self.directory,
1055                 binary,
1056                 copy_to_suite,
1057                 component,
1058                 fingerprint=self.fingerprint,
1059                 source_suites=source_suites,
1060                 extra_source_archives=extra_source_archives
1061             )
1062             db_binaries.append(db_binary)
1063
1064         if suite.copychanges:
1065             src = os.path.join(self.directory, self.changes.filename)
1066             dst = os.path.join(suite.archive.path, 'dists', suite.suite_name, self.changes.filename)
1067             self.transaction.fs.copy(src, dst, mode=suite.archive.mode)
1068
1069         return (db_source, db_binaries)
1070
1071     def _install_changes(self):
1072         assert self.changes.valid_signature
1073         control = self.changes.changes
1074         session = self.transaction.session
1075         config = Config()
1076
1077         changelog_id = None
1078         # Only add changelog for sourceful uploads and binNMUs
1079         if 'source' in self.changes.architectures or re_bin_only_nmu.search(control['Version']):
1080             query = 'INSERT INTO changelogs_text (changelog) VALUES (:changelog) RETURNING id'
1081             changelog_id = session.execute(query, {'changelog': control['Changes']}).scalar()
1082             assert changelog_id is not None
1083
1084         db_changes = DBChange()
1085         db_changes.changesname = self.changes.filename
1086         db_changes.source = control['Source']
1087         db_changes.binaries = control.get('Binary', None)
1088         db_changes.architecture = control['Architecture']
1089         db_changes.version = control['Version']
1090         db_changes.distribution = control['Distribution']
1091         db_changes.urgency = control['Urgency']
1092         db_changes.maintainer = control['Maintainer']
1093         db_changes.changedby = control.get('Changed-By', control['Maintainer'])
1094         db_changes.date = control['Date']
1095         db_changes.fingerprint = self.fingerprint.fingerprint
1096         db_changes.changelog_id = changelog_id
1097         db_changes.closes = self.changes.closed_bugs
1098
1099         try:
1100             self.transaction.session.add(db_changes)
1101             self.transaction.session.flush()
1102         except sqlalchemy.exc.IntegrityError:
1103             raise ArchiveException('{0} is already known.'.format(self.changes.filename))
1104
1105         return db_changes
1106
1107     def _install_policy(self, policy_queue, target_suite, db_changes, db_source, db_binaries):
1108         u = PolicyQueueUpload()
1109         u.policy_queue = policy_queue
1110         u.target_suite = target_suite
1111         u.changes = db_changes
1112         u.source = db_source
1113         u.binaries = db_binaries
1114         self.transaction.session.add(u)
1115         self.transaction.session.flush()
1116
1117         dst = os.path.join(policy_queue.path, self.changes.filename)
1118         self.transaction.fs.copy(self.changes.path, dst, mode=policy_queue.change_perms)
1119
1120         return u
1121
1122     def try_autobyhand(self):
1123         """Try AUTOBYHAND
1124
1125         Try to handle byhand packages automatically.
1126
1127         @rtype:  list of L{daklib.upload.HashedFile}
1128         @return: list of remaining byhand files
1129         """
1130         assert len(self.reject_reasons) == 0
1131         assert self.changes.valid_signature
1132         assert self.final_suites is not None
1133         assert self._checked
1134
1135         byhand = self.changes.byhand_files
1136         if len(byhand) == 0:
1137             return True
1138
1139         suites = list(self.final_suites)
1140         assert len(suites) == 1, "BYHAND uploads must be to a single suite"
1141         suite = suites[0]
1142
1143         cnf = Config()
1144         control = self.changes.changes
1145         automatic_byhand_packages = cnf.subtree("AutomaticByHandPackages")
1146
1147         remaining = []
1148         for f in byhand:
1149             if '_' in f.filename:
1150                 parts = f.filename.split('_', 2)
1151                 if len(parts) != 3:
1152                     print "W: unexpected byhand filename {0}. No automatic processing.".format(f.filename)
1153                     remaining.append(f)
1154                     continue
1155
1156                 package, version, archext = parts
1157                 arch, ext = archext.split('.', 1)
1158             else:
1159                 parts = f.filename.split('.')
1160                 if len(parts) < 2:
1161                     print "W: unexpected byhand filename {0}. No automatic processing.".format(f.filename)
1162                     remaining.append(f)
1163                     continue
1164
1165                 package = parts[0]
1166                 version = '0'
1167                 arch = 'all'
1168                 ext = parts[-1]
1169
1170             try:
1171                 rule = automatic_byhand_packages.subtree(package)
1172             except KeyError:
1173                 remaining.append(f)
1174                 continue
1175
1176             if rule['Source'] != self.changes.source_name \
1177                     or rule['Section'] != f.section \
1178                     or ('Extension' in rule and rule['Extension'] != ext):
1179                 remaining.append(f)
1180                 continue
1181
1182             script = rule['Script']
1183             retcode = daklib.daksubprocess.call([script, os.path.join(self.directory, f.filename), control['Version'], arch, os.path.join(self.directory, self.changes.filename), suite.suite_name], shell=False)
1184             if retcode != 0:
1185                 print "W: error processing {0}.".format(f.filename)
1186                 remaining.append(f)
1187
1188         return len(remaining) == 0
1189
1190     def _install_byhand(self, policy_queue_upload, hashed_file):
1191         """install byhand file
1192
1193         @type  policy_queue_upload: L{daklib.dbconn.PolicyQueueUpload}
1194
1195         @type  hashed_file: L{daklib.upload.HashedFile}
1196         """
1197         fs = self.transaction.fs
1198         session = self.transaction.session
1199         policy_queue = policy_queue_upload.policy_queue
1200
1201         byhand_file = PolicyQueueByhandFile()
1202         byhand_file.upload = policy_queue_upload
1203         byhand_file.filename = hashed_file.filename
1204         session.add(byhand_file)
1205         session.flush()
1206
1207         src = os.path.join(self.directory, hashed_file.filename)
1208         dst = os.path.join(policy_queue.path, hashed_file.filename)
1209         fs.copy(src, dst, mode=policy_queue.change_perms)
1210
1211         return byhand_file
1212
1213     def _do_bts_versiontracking(self):
1214         cnf = Config()
1215         fs = self.transaction.fs
1216
1217         btsdir = cnf.get('Dir::BTSVersionTrack')
1218         if btsdir is None or btsdir == '':
1219             return
1220
1221         base = os.path.join(btsdir, self.changes.filename[:-8])
1222
1223         # version history
1224         sourcedir = self.unpacked_source()
1225         if sourcedir is not None:
1226             fh = open(os.path.join(sourcedir, 'debian', 'changelog'), 'r')
1227             versions = fs.create("{0}.versions".format(base), mode=0o644)
1228             for line in fh.readlines():
1229                 if re_changelog_versions.match(line):
1230                     versions.write(line)
1231             fh.close()
1232             versions.close()
1233
1234         # binary -> source mapping
1235         debinfo = fs.create("{0}.debinfo".format(base), mode=0o644)
1236         for binary in self.changes.binaries:
1237             control = binary.control
1238             source_package, source_version = binary.source
1239             line = " ".join([control['Package'], control['Version'], control['Architecture'], source_package, source_version])
1240             print >>debinfo, line
1241         debinfo.close()
1242
1243     def _policy_queue(self, suite):
1244         if suite.policy_queue is not None:
1245             return suite.policy_queue
1246         return None
1247
1248     def install(self):
1249         """install upload
1250
1251         Install upload to a suite or policy queue.  This method does B{not}
1252         handle uploads to NEW.
1253
1254         You need to have called the C{check} method before calling this method.
1255         """
1256         assert len(self.reject_reasons) == 0
1257         assert self.changes.valid_signature
1258         assert self.final_suites is not None
1259         assert self._checked
1260         assert not self.new
1261
1262         db_changes = self._install_changes()
1263
1264         for suite in self.final_suites:
1265             overridesuite = suite
1266             if suite.overridesuite is not None:
1267                 overridesuite = self.session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
1268
1269             policy_queue = self._policy_queue(suite)
1270
1271             redirected_suite = suite
1272             if policy_queue is not None:
1273                 redirected_suite = policy_queue.suite
1274
1275             # source can be in the suite we install to or any suite we enhance
1276             source_suite_ids = set([suite.suite_id, redirected_suite.suite_id])
1277             for enhanced_suite_id, in self.session.query(VersionCheck.reference_id) \
1278                     .filter(VersionCheck.suite_id.in_(source_suite_ids)) \
1279                     .filter(VersionCheck.check == 'Enhances'):
1280                 source_suite_ids.add(enhanced_suite_id)
1281
1282             source_suites = self.session.query(Suite).filter(Suite.suite_id.in_(source_suite_ids)).subquery()
1283
1284             source_component_func = lambda source: self._source_override(overridesuite, source).component
1285             binary_component_func = lambda binary: self._binary_component(overridesuite, binary, only_overrides=False)
1286
1287             (db_source, db_binaries) = self._install_to_suite(redirected_suite, source_component_func, binary_component_func, source_suites=source_suites, extra_source_archives=[suite.archive])
1288
1289             if policy_queue is not None:
1290                 self._install_policy(policy_queue, suite, db_changes, db_source, db_binaries)
1291
1292             # copy to build queues
1293             if policy_queue is None or policy_queue.send_to_build_queues:
1294                 for build_queue in suite.copy_queues:
1295                     self._install_to_suite(build_queue.suite, source_component_func, binary_component_func, source_suites=source_suites, extra_source_archives=[suite.archive])
1296
1297         self._do_bts_versiontracking()
1298
1299     def install_to_new(self):
1300         """install upload to NEW
1301
1302         Install upload to NEW.  This method does B{not} handle regular uploads
1303         to suites or policy queues.
1304
1305         You need to have called the C{check} method before calling this method.
1306         """
1307         # Uploads to NEW are special as we don't have overrides.
1308         assert len(self.reject_reasons) == 0
1309         assert self.changes.valid_signature
1310         assert self.final_suites is not None
1311
1312         source = self.changes.source
1313         binaries = self.changes.binaries
1314         byhand = self.changes.byhand_files
1315
1316         # we need a suite to guess components
1317         suites = list(self.final_suites)
1318         assert len(suites) == 1, "NEW uploads must be to a single suite"
1319         suite = suites[0]
1320
1321         # decide which NEW queue to use
1322         if suite.new_queue is None:
1323             new_queue = self.transaction.session.query(PolicyQueue).filter_by(queue_name='new').one()
1324         else:
1325             new_queue = suite.new_queue
1326         if len(byhand) > 0:
1327             # There is only one global BYHAND queue
1328             new_queue = self.transaction.session.query(PolicyQueue).filter_by(queue_name='byhand').one()
1329         new_suite = new_queue.suite
1330
1331
1332         def binary_component_func(binary):
1333             return self._binary_component(suite, binary, only_overrides=False)
1334
1335         # guess source component
1336         # XXX: should be moved into an extra method
1337         binary_component_names = set()
1338         for binary in binaries:
1339             component = binary_component_func(binary)
1340             binary_component_names.add(component.component_name)
1341         source_component_name = None
1342         for c in self.session.query(Component).order_by(Component.component_id):
1343             guess = c.component_name
1344             if guess in binary_component_names:
1345                 source_component_name = guess
1346                 break
1347         if source_component_name is None:
1348             source_component = self.session.query(Component).order_by(Component.component_id).first()
1349         else:
1350             source_component = self.session.query(Component).filter_by(component_name=source_component_name).one()
1351         source_component_func = lambda source: source_component
1352
1353         db_changes = self._install_changes()
1354         (db_source, db_binaries) = self._install_to_suite(new_suite, source_component_func, binary_component_func, source_suites=True, extra_source_archives=[suite.archive])
1355         policy_upload = self._install_policy(new_queue, suite, db_changes, db_source, db_binaries)
1356
1357         for f in byhand:
1358             self._install_byhand(policy_upload, f)
1359
1360         self._do_bts_versiontracking()
1361
1362     def commit(self):
1363         """commit changes"""
1364         self.transaction.commit()
1365
1366     def rollback(self):
1367         """rollback changes"""
1368         self.transaction.rollback()
1369
1370     def __enter__(self):
1371         self.prepare()
1372         return self
1373
1374     def __exit__(self, type, value, traceback):
1375         if self.directory is not None:
1376             shutil.rmtree(self.directory)
1377             self.directory = None
1378         self.changes = None
1379         self.transaction.rollback()
1380         return None