]> git.decadent.org.uk Git - dak.git/blob - daklib/archive.py
010fbf7eba5814f914b525abd5b7788d97096415
[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):
782         new = False
783
784         binaries = self.changes.binaries
785         source = self.changes.source
786         if source is not None and not source.package_list.fallback:
787             packages = source.package_list.packages_for_suite(suite)
788             binaries = [ entry for entry in packages ]
789
790         for b in binaries:
791             if utils.is_in_debug_section(b.control) and suite.debug_suite is not None:
792                 continue
793             override = self._binary_override(suite, b)
794             if override is None:
795                 self.warnings.append('binary:{0} is NEW.'.format(b.name))
796                 new = True
797
798         return new
799
800     def _check_new(self, suite):
801         """Check if upload is NEW
802
803         An upload is NEW if it has binary or source packages that do not have
804         an override in C{suite} OR if it references files ONLY in a tainted
805         archive (eg. when it references files in NEW).
806
807         @rtype:  bool
808         @return: C{True} if the upload is NEW, C{False} otherwise
809         """
810         session = self.session
811         new = False
812
813         # Check for missing overrides
814         if self._check_new_binary_overrides(suite):
815             new = True
816         if self.changes.source is not None:
817             override = self._source_override(suite, self.changes.source)
818             if override is None:
819                 self.warnings.append('source:{0} is NEW.'.format(self.changes.source.dsc['Source']))
820                 new = True
821
822         # Check if we reference a file only in a tainted archive
823         files = self.changes.files.values()
824         if self.changes.source is not None:
825             files.extend(self.changes.source.files.values())
826         for f in files:
827             query = session.query(ArchiveFile).join(PoolFile).filter(PoolFile.sha1sum == f.sha1sum)
828             query_untainted = query.join(Archive).filter(Archive.tainted == False)
829
830             in_archive = (query.first() is not None)
831             in_untainted_archive = (query_untainted.first() is not None)
832
833             if in_archive and not in_untainted_archive:
834                 self.warnings.append('{0} is only available in NEW.'.format(f.filename))
835                 new = True
836
837         return new
838
839     def _final_suites(self):
840         session = self.session
841
842         mapped_suites = self._mapped_suites()
843         final_suites = set()
844
845         for suite in mapped_suites:
846             overridesuite = suite
847             if suite.overridesuite is not None:
848                 overridesuite = session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
849             if self._check_new(overridesuite):
850                 self.new = True
851             final_suites.add(suite)
852
853         return final_suites
854
855     def _binary_override(self, suite, binary):
856         """Get override entry for a binary
857
858         @type  suite: L{daklib.dbconn.Suite}
859         @param suite: suite to get override for
860
861         @type  binary: L{daklib.upload.Binary} or L{daklib.packagelist.PackageListEntry}
862         @param binary: binary to get override for
863
864         @rtype:  L{daklib.dbconn.Override} or C{None}
865         @return: override for the given binary or C{None}
866         """
867         if suite.overridesuite is not None:
868             suite = self.session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
869
870         mapped_component = get_mapped_component(binary.component)
871         if mapped_component is None:
872             return None
873
874         query = self.session.query(Override).filter_by(suite=suite, package=binary.name) \
875                 .join(Component).filter(Component.component_name == mapped_component.component_name) \
876                 .join(OverrideType).filter(OverrideType.overridetype == binary.type)
877
878         try:
879             return query.one()
880         except NoResultFound:
881             return None
882
883     def _source_override(self, suite, source):
884         """Get override entry for a source
885
886         @type  suite: L{daklib.dbconn.Suite}
887         @param suite: suite to get override for
888
889         @type  source: L{daklib.upload.Source}
890         @param source: source to get override for
891
892         @rtype:  L{daklib.dbconn.Override} or C{None}
893         @return: override for the given source or C{None}
894         """
895         if suite.overridesuite is not None:
896             suite = self.session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
897
898         query = self.session.query(Override).filter_by(suite=suite, package=source.dsc['Source']) \
899                 .join(OverrideType).filter(OverrideType.overridetype == 'dsc')
900
901         component = source_component_from_package_list(source.package_list, suite)
902         if component is not None:
903             query = query.filter(Override.component == component)
904
905         try:
906             return query.one()
907         except NoResultFound:
908             return None
909
910     def _binary_component(self, suite, binary, only_overrides=True):
911         """get component for a binary
912
913         By default this will only look at overrides to get the right component;
914         if C{only_overrides} is C{False} this method will also look at the
915         Section field.
916
917         @type  suite: L{daklib.dbconn.Suite}
918
919         @type  binary: L{daklib.upload.Binary}
920
921         @type  only_overrides: bool
922         @param only_overrides: only use overrides to get the right component
923
924         @rtype: L{daklib.dbconn.Component} or C{None}
925         """
926         override = self._binary_override(suite, binary)
927         if override is not None:
928             return override.component
929         if only_overrides:
930             return None
931         return get_mapped_component(binary.component, self.session)
932
933     def check(self, force=False):
934         """run checks against the upload
935
936         @type  force: bool
937         @param force: ignore failing forcable checks
938
939         @rtype:  bool
940         @return: C{True} if all checks passed, C{False} otherwise
941         """
942         # XXX: needs to be better structured.
943         assert self.changes.valid_signature
944
945         try:
946             # Validate signatures and hashes before we do any real work:
947             for chk in (
948                     checks.SignatureAndHashesCheck,
949                     checks.SignatureTimestampCheck,
950                     checks.ChangesCheck,
951                     checks.ExternalHashesCheck,
952                     checks.SourceCheck,
953                     checks.BinaryCheck,
954                     checks.BinaryTimestampCheck,
955                     checks.SingleDistributionCheck,
956                     ):
957                 chk().check(self)
958
959             final_suites = self._final_suites()
960             if len(final_suites) == 0:
961                 self.reject_reasons.append('No target suite found. Please check your target distribution and that you uploaded to the right archive.')
962                 return False
963
964             self.final_suites = final_suites
965
966             for chk in (
967                     checks.TransitionCheck,
968                     checks.ACLCheck,
969                     checks.NoSourceOnlyCheck,
970                     checks.LintianCheck,
971                     ):
972                 chk().check(self)
973
974             for chk in (
975                     checks.ACLCheck,
976                     checks.SourceFormatCheck,
977                     checks.SuiteArchitectureCheck,
978                     checks.VersionCheck,
979                     ):
980                 for suite in final_suites:
981                     chk().per_suite_check(self, suite)
982
983             if len(self.reject_reasons) != 0:
984                 return False
985
986             self._checked = True
987             return True
988         except checks.Reject as e:
989             self.reject_reasons.append(unicode(e))
990         except Exception as e:
991             self.reject_reasons.append("Processing raised an exception: {0}.\n{1}".format(e, traceback.format_exc()))
992         return False
993
994     def _install_to_suite(self, suite, source_component_func, binary_component_func, source_suites=None, extra_source_archives=None):
995         """Install upload to the given suite
996
997         @type  suite: L{daklib.dbconn.Suite}
998         @param suite: suite to install the package into. This is the real suite,
999                       ie. after any redirection to NEW or a policy queue
1000
1001         @param source_component_func: function to get the L{daklib.dbconn.Component}
1002                                       for a L{daklib.upload.Source} object
1003
1004         @param binary_component_func: function to get the L{daklib.dbconn.Component}
1005                                       for a L{daklib.upload.Binary} object
1006
1007         @param source_suites: see L{daklib.archive.ArchiveTransaction.install_binary}
1008
1009         @param extra_source_archives: see L{daklib.archive.ArchiveTransaction.install_binary}
1010
1011         @return: tuple with two elements. The first is a L{daklib.dbconn.DBSource}
1012                  object for the install source or C{None} if no source was
1013                  included. The second is a list of L{daklib.dbconn.DBBinary}
1014                  objects for the installed binary packages.
1015         """
1016         # XXX: move this function to ArchiveTransaction?
1017
1018         control = self.changes.changes
1019         changed_by = get_or_set_maintainer(control.get('Changed-By', control['Maintainer']), self.session)
1020
1021         if source_suites is None:
1022             source_suites = self.session.query(Suite).join((VersionCheck, VersionCheck.reference_id == Suite.suite_id)).filter(VersionCheck.check == 'Enhances').filter(VersionCheck.suite == suite).subquery()
1023
1024         source = self.changes.source
1025         if source is not None:
1026             component = source_component_func(source)
1027             db_source = self.transaction.install_source(
1028                 self.directory,
1029                 source,
1030                 suite,
1031                 component,
1032                 changed_by,
1033                 fingerprint=self.fingerprint
1034             )
1035         else:
1036             db_source = None
1037
1038         db_binaries = []
1039         for binary in self.changes.binaries:
1040             copy_to_suite = suite
1041             if utils.is_in_debug_section(binary.control) and suite.debug_suite is not None:
1042                 copy_to_suite = suite.debug_suite
1043
1044             component = binary_component_func(binary)
1045             db_binary = self.transaction.install_binary(
1046                 self.directory,
1047                 binary,
1048                 copy_to_suite,
1049                 component,
1050                 fingerprint=self.fingerprint,
1051                 source_suites=source_suites,
1052                 extra_source_archives=extra_source_archives
1053             )
1054             db_binaries.append(db_binary)
1055
1056         if suite.copychanges:
1057             src = os.path.join(self.directory, self.changes.filename)
1058             dst = os.path.join(suite.archive.path, 'dists', suite.suite_name, self.changes.filename)
1059             self.transaction.fs.copy(src, dst, mode=suite.archive.mode)
1060
1061         return (db_source, db_binaries)
1062
1063     def _install_changes(self):
1064         assert self.changes.valid_signature
1065         control = self.changes.changes
1066         session = self.transaction.session
1067         config = Config()
1068
1069         changelog_id = None
1070         # Only add changelog for sourceful uploads and binNMUs
1071         if 'source' in self.changes.architectures or re_bin_only_nmu.search(control['Version']):
1072             query = 'INSERT INTO changelogs_text (changelog) VALUES (:changelog) RETURNING id'
1073             changelog_id = session.execute(query, {'changelog': control['Changes']}).scalar()
1074             assert changelog_id is not None
1075
1076         db_changes = DBChange()
1077         db_changes.changesname = self.changes.filename
1078         db_changes.source = control['Source']
1079         db_changes.binaries = control.get('Binary', None)
1080         db_changes.architecture = control['Architecture']
1081         db_changes.version = control['Version']
1082         db_changes.distribution = control['Distribution']
1083         db_changes.urgency = control['Urgency']
1084         db_changes.maintainer = control['Maintainer']
1085         db_changes.changedby = control.get('Changed-By', control['Maintainer'])
1086         db_changes.date = control['Date']
1087         db_changes.fingerprint = self.fingerprint.fingerprint
1088         db_changes.changelog_id = changelog_id
1089         db_changes.closes = self.changes.closed_bugs
1090
1091         try:
1092             self.transaction.session.add(db_changes)
1093             self.transaction.session.flush()
1094         except sqlalchemy.exc.IntegrityError:
1095             raise ArchiveException('{0} is already known.'.format(self.changes.filename))
1096
1097         return db_changes
1098
1099     def _install_policy(self, policy_queue, target_suite, db_changes, db_source, db_binaries):
1100         u = PolicyQueueUpload()
1101         u.policy_queue = policy_queue
1102         u.target_suite = target_suite
1103         u.changes = db_changes
1104         u.source = db_source
1105         u.binaries = db_binaries
1106         self.transaction.session.add(u)
1107         self.transaction.session.flush()
1108
1109         dst = os.path.join(policy_queue.path, self.changes.filename)
1110         self.transaction.fs.copy(self.changes.path, dst, mode=policy_queue.change_perms)
1111
1112         return u
1113
1114     def try_autobyhand(self):
1115         """Try AUTOBYHAND
1116
1117         Try to handle byhand packages automatically.
1118
1119         @rtype:  list of L{daklib.upload.HashedFile}
1120         @return: list of remaining byhand files
1121         """
1122         assert len(self.reject_reasons) == 0
1123         assert self.changes.valid_signature
1124         assert self.final_suites is not None
1125         assert self._checked
1126
1127         byhand = self.changes.byhand_files
1128         if len(byhand) == 0:
1129             return True
1130
1131         suites = list(self.final_suites)
1132         assert len(suites) == 1, "BYHAND uploads must be to a single suite"
1133         suite = suites[0]
1134
1135         cnf = Config()
1136         control = self.changes.changes
1137         automatic_byhand_packages = cnf.subtree("AutomaticByHandPackages")
1138
1139         remaining = []
1140         for f in byhand:
1141             if '_' in f.filename:
1142                 parts = f.filename.split('_', 2)
1143                 if len(parts) != 3:
1144                     print "W: unexpected byhand filename {0}. No automatic processing.".format(f.filename)
1145                     remaining.append(f)
1146                     continue
1147
1148                 package, version, archext = parts
1149                 arch, ext = archext.split('.', 1)
1150             else:
1151                 parts = f.filename.split('.')
1152                 if len(parts) < 2:
1153                     print "W: unexpected byhand filename {0}. No automatic processing.".format(f.filename)
1154                     remaining.append(f)
1155                     continue
1156
1157                 package = parts[0]
1158                 version = '0'
1159                 arch = 'all'
1160                 ext = parts[-1]
1161
1162             try:
1163                 rule = automatic_byhand_packages.subtree(package)
1164             except KeyError:
1165                 remaining.append(f)
1166                 continue
1167
1168             if rule['Source'] != self.changes.source_name \
1169                     or rule['Section'] != f.section \
1170                     or ('Extension' in rule and rule['Extension'] != ext):
1171                 remaining.append(f)
1172                 continue
1173
1174             script = rule['Script']
1175             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)
1176             if retcode != 0:
1177                 print "W: error processing {0}.".format(f.filename)
1178                 remaining.append(f)
1179
1180         return len(remaining) == 0
1181
1182     def _install_byhand(self, policy_queue_upload, hashed_file):
1183         """install byhand file
1184
1185         @type  policy_queue_upload: L{daklib.dbconn.PolicyQueueUpload}
1186
1187         @type  hashed_file: L{daklib.upload.HashedFile}
1188         """
1189         fs = self.transaction.fs
1190         session = self.transaction.session
1191         policy_queue = policy_queue_upload.policy_queue
1192
1193         byhand_file = PolicyQueueByhandFile()
1194         byhand_file.upload = policy_queue_upload
1195         byhand_file.filename = hashed_file.filename
1196         session.add(byhand_file)
1197         session.flush()
1198
1199         src = os.path.join(self.directory, hashed_file.filename)
1200         dst = os.path.join(policy_queue.path, hashed_file.filename)
1201         fs.copy(src, dst, mode=policy_queue.change_perms)
1202
1203         return byhand_file
1204
1205     def _do_bts_versiontracking(self):
1206         cnf = Config()
1207         fs = self.transaction.fs
1208
1209         btsdir = cnf.get('Dir::BTSVersionTrack')
1210         if btsdir is None or btsdir == '':
1211             return
1212
1213         base = os.path.join(btsdir, self.changes.filename[:-8])
1214
1215         # version history
1216         sourcedir = self.unpacked_source()
1217         if sourcedir is not None:
1218             fh = open(os.path.join(sourcedir, 'debian', 'changelog'), 'r')
1219             versions = fs.create("{0}.versions".format(base), mode=0o644)
1220             for line in fh.readlines():
1221                 if re_changelog_versions.match(line):
1222                     versions.write(line)
1223             fh.close()
1224             versions.close()
1225
1226         # binary -> source mapping
1227         debinfo = fs.create("{0}.debinfo".format(base), mode=0o644)
1228         for binary in self.changes.binaries:
1229             control = binary.control
1230             source_package, source_version = binary.source
1231             line = " ".join([control['Package'], control['Version'], control['Architecture'], source_package, source_version])
1232             print >>debinfo, line
1233         debinfo.close()
1234
1235     def _policy_queue(self, suite):
1236         if suite.policy_queue is not None:
1237             return suite.policy_queue
1238         return None
1239
1240     def install(self):
1241         """install upload
1242
1243         Install upload to a suite or policy queue.  This method does B{not}
1244         handle uploads to NEW.
1245
1246         You need to have called the C{check} method before calling this method.
1247         """
1248         assert len(self.reject_reasons) == 0
1249         assert self.changes.valid_signature
1250         assert self.final_suites is not None
1251         assert self._checked
1252         assert not self.new
1253
1254         db_changes = self._install_changes()
1255
1256         for suite in self.final_suites:
1257             overridesuite = suite
1258             if suite.overridesuite is not None:
1259                 overridesuite = self.session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
1260
1261             policy_queue = self._policy_queue(suite)
1262
1263             redirected_suite = suite
1264             if policy_queue is not None:
1265                 redirected_suite = policy_queue.suite
1266
1267             # source can be in the suite we install to or any suite we enhance
1268             source_suite_ids = set([suite.suite_id, redirected_suite.suite_id])
1269             for enhanced_suite_id, in self.session.query(VersionCheck.reference_id) \
1270                     .filter(VersionCheck.suite_id.in_(source_suite_ids)) \
1271                     .filter(VersionCheck.check == 'Enhances'):
1272                 source_suite_ids.add(enhanced_suite_id)
1273
1274             source_suites = self.session.query(Suite).filter(Suite.suite_id.in_(source_suite_ids)).subquery()
1275
1276             source_component_func = lambda source: self._source_override(overridesuite, source).component
1277             binary_component_func = lambda binary: self._binary_component(overridesuite, binary, only_overrides=False)
1278
1279             (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])
1280
1281             if policy_queue is not None:
1282                 self._install_policy(policy_queue, suite, db_changes, db_source, db_binaries)
1283
1284             # copy to build queues
1285             if policy_queue is None or policy_queue.send_to_build_queues:
1286                 for build_queue in suite.copy_queues:
1287                     self._install_to_suite(build_queue.suite, source_component_func, binary_component_func, source_suites=source_suites, extra_source_archives=[suite.archive])
1288
1289         self._do_bts_versiontracking()
1290
1291     def install_to_new(self):
1292         """install upload to NEW
1293
1294         Install upload to NEW.  This method does B{not} handle regular uploads
1295         to suites or policy queues.
1296
1297         You need to have called the C{check} method before calling this method.
1298         """
1299         # Uploads to NEW are special as we don't have overrides.
1300         assert len(self.reject_reasons) == 0
1301         assert self.changes.valid_signature
1302         assert self.final_suites is not None
1303
1304         source = self.changes.source
1305         binaries = self.changes.binaries
1306         byhand = self.changes.byhand_files
1307
1308         # we need a suite to guess components
1309         suites = list(self.final_suites)
1310         assert len(suites) == 1, "NEW uploads must be to a single suite"
1311         suite = suites[0]
1312
1313         # decide which NEW queue to use
1314         if suite.new_queue is None:
1315             new_queue = self.transaction.session.query(PolicyQueue).filter_by(queue_name='new').one()
1316         else:
1317             new_queue = suite.new_queue
1318         if len(byhand) > 0:
1319             # There is only one global BYHAND queue
1320             new_queue = self.transaction.session.query(PolicyQueue).filter_by(queue_name='byhand').one()
1321         new_suite = new_queue.suite
1322
1323
1324         def binary_component_func(binary):
1325             return self._binary_component(suite, binary, only_overrides=False)
1326
1327         # guess source component
1328         # XXX: should be moved into an extra method
1329         binary_component_names = set()
1330         for binary in binaries:
1331             component = binary_component_func(binary)
1332             binary_component_names.add(component.component_name)
1333         source_component_name = None
1334         for c in self.session.query(Component).order_by(Component.component_id):
1335             guess = c.component_name
1336             if guess in binary_component_names:
1337                 source_component_name = guess
1338                 break
1339         if source_component_name is None:
1340             source_component = self.session.query(Component).order_by(Component.component_id).first()
1341         else:
1342             source_component = self.session.query(Component).filter_by(component_name=source_component_name).one()
1343         source_component_func = lambda source: source_component
1344
1345         db_changes = self._install_changes()
1346         (db_source, db_binaries) = self._install_to_suite(new_suite, source_component_func, binary_component_func, source_suites=True, extra_source_archives=[suite.archive])
1347         policy_upload = self._install_policy(new_queue, suite, db_changes, db_source, db_binaries)
1348
1349         for f in byhand:
1350             self._install_byhand(policy_upload, f)
1351
1352         self._do_bts_versiontracking()
1353
1354     def commit(self):
1355         """commit changes"""
1356         self.transaction.commit()
1357
1358     def rollback(self):
1359         """rollback changes"""
1360         self.transaction.rollback()
1361
1362     def __enter__(self):
1363         self.prepare()
1364         return self
1365
1366     def __exit__(self, type, value, traceback):
1367         if self.directory is not None:
1368             shutil.rmtree(self.directory)
1369             self.directory = None
1370         self.changes = None
1371         self.transaction.rollback()
1372         return None