]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
ae5a9e0d21b6583712fcc5d7fd3578cb55479876
[dak.git] / daklib / dbconn.py
1 #!/usr/bin/python
2
3 """ DB access class
4
5 @contact: Debian FTPMaster <ftpmaster@debian.org>
6 @copyright: 2000, 2001, 2002, 2003, 2004, 2006  James Troup <james@nocrew.org>
7 @copyright: 2008-2009  Mark Hymers <mhy@debian.org>
8 @copyright: 2009, 2010  Joerg Jaspert <joerg@debian.org>
9 @copyright: 2009  Mike O'Connor <stew@debian.org>
10 @license: GNU General Public License version 2 or later
11 """
12
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27 ################################################################################
28
29 # < mhy> I need a funny comment
30 # < sgran> two peanuts were walking down a dark street
31 # < sgran> one was a-salted
32 #  * mhy looks up the definition of "funny"
33
34 ################################################################################
35
36 import os
37 from os.path import normpath
38 import re
39 import psycopg2
40 import traceback
41 import commands
42
43 try:
44     # python >= 2.6
45     import json
46 except:
47     # python <= 2.5
48     import simplejson as json
49
50 from datetime import datetime, timedelta
51 from errno import ENOENT
52 from tempfile import mkstemp, mkdtemp
53 from subprocess import Popen, PIPE
54 from tarfile import TarFile
55
56 from inspect import getargspec
57
58 import sqlalchemy
59 from sqlalchemy import create_engine, Table, MetaData, Column, Integer, desc, \
60     Text, ForeignKey
61 from sqlalchemy.orm import sessionmaker, mapper, relation, object_session, \
62     backref, MapperExtension, EXT_CONTINUE, object_mapper, clear_mappers
63 from sqlalchemy import types as sqltypes
64 from sqlalchemy.orm.collections import attribute_mapped_collection
65 from sqlalchemy.ext.associationproxy import association_proxy
66
67 # Don't remove this, we re-export the exceptions to scripts which import us
68 from sqlalchemy.exc import *
69 from sqlalchemy.orm.exc import NoResultFound
70
71 # Only import Config until Queue stuff is changed to store its config
72 # in the database
73 from config import Config
74 from textutils import fix_maintainer
75 from dak_exceptions import DBUpdateError, NoSourceFieldError
76
77 # suppress some deprecation warnings in squeeze related to sqlalchemy
78 import warnings
79 warnings.filterwarnings('ignore', \
80     "The SQLAlchemy PostgreSQL dialect has been renamed from 'postgres' to 'postgresql'.*", \
81     SADeprecationWarning)
82
83
84 ################################################################################
85
86 # Patch in support for the debversion field type so that it works during
87 # reflection
88
89 try:
90     # that is for sqlalchemy 0.6
91     UserDefinedType = sqltypes.UserDefinedType
92 except:
93     # this one for sqlalchemy 0.5
94     UserDefinedType = sqltypes.TypeEngine
95
96 class DebVersion(UserDefinedType):
97     def get_col_spec(self):
98         return "DEBVERSION"
99
100     def bind_processor(self, dialect):
101         return None
102
103     # ' = None' is needed for sqlalchemy 0.5:
104     def result_processor(self, dialect, coltype = None):
105         return None
106
107 sa_major_version = sqlalchemy.__version__[0:3]
108 if sa_major_version in ["0.5", "0.6"]:
109     from sqlalchemy.databases import postgres
110     postgres.ischema_names['debversion'] = DebVersion
111 else:
112     raise Exception("dak only ported to SQLA versions 0.5 and 0.6.  See daklib/dbconn.py")
113
114 ################################################################################
115
116 __all__ = ['IntegrityError', 'SQLAlchemyError', 'DebVersion']
117
118 ################################################################################
119
120 def session_wrapper(fn):
121     """
122     Wrapper around common ".., session=None):" handling. If the wrapped
123     function is called without passing 'session', we create a local one
124     and destroy it when the function ends.
125
126     Also attaches a commit_or_flush method to the session; if we created a
127     local session, this is a synonym for session.commit(), otherwise it is a
128     synonym for session.flush().
129     """
130
131     def wrapped(*args, **kwargs):
132         private_transaction = False
133
134         # Find the session object
135         session = kwargs.get('session')
136
137         if session is None:
138             if len(args) <= len(getargspec(fn)[0]) - 1:
139                 # No session specified as last argument or in kwargs
140                 private_transaction = True
141                 session = kwargs['session'] = DBConn().session()
142             else:
143                 # Session is last argument in args
144                 session = args[-1]
145                 if session is None:
146                     args = list(args)
147                     session = args[-1] = DBConn().session()
148                     private_transaction = True
149
150         if private_transaction:
151             session.commit_or_flush = session.commit
152         else:
153             session.commit_or_flush = session.flush
154
155         try:
156             return fn(*args, **kwargs)
157         finally:
158             if private_transaction:
159                 # We created a session; close it.
160                 session.close()
161
162     wrapped.__doc__ = fn.__doc__
163     wrapped.func_name = fn.func_name
164
165     return wrapped
166
167 __all__.append('session_wrapper')
168
169 ################################################################################
170
171 class ORMObject(object):
172     """
173     ORMObject is a base class for all ORM classes mapped by SQLalchemy. All
174     derived classes must implement the properties() method.
175     """
176
177     def properties(self):
178         '''
179         This method should be implemented by all derived classes and returns a
180         list of the important properties. The properties 'created' and
181         'modified' will be added automatically. A suffix '_count' should be
182         added to properties that are lists or query objects. The most important
183         property name should be returned as the first element in the list
184         because it is used by repr().
185         '''
186         return []
187
188     def json(self):
189         '''
190         Returns a JSON representation of the object based on the properties
191         returned from the properties() method.
192         '''
193         data = {}
194         # add created and modified
195         all_properties = self.properties() + ['created', 'modified']
196         for property in all_properties:
197             # check for list or query
198             if property[-6:] == '_count':
199                 real_property = property[:-6]
200                 if not hasattr(self, real_property):
201                     continue
202                 value = getattr(self, real_property)
203                 if hasattr(value, '__len__'):
204                     # list
205                     value = len(value)
206                 elif hasattr(value, 'count'):
207                     # query (but not during validation)
208                     if self.in_validation:
209                         continue
210                     value = value.count()
211                 else:
212                     raise KeyError('Do not understand property %s.' % property)
213             else:
214                 if not hasattr(self, property):
215                     continue
216                 # plain object
217                 value = getattr(self, property)
218                 if value is None:
219                     # skip None
220                     continue
221                 elif isinstance(value, ORMObject):
222                     # use repr() for ORMObject types
223                     value = repr(value)
224                 else:
225                     # we want a string for all other types because json cannot
226                     # encode everything
227                     value = str(value)
228             data[property] = value
229         return json.dumps(data)
230
231     def classname(self):
232         '''
233         Returns the name of the class.
234         '''
235         return type(self).__name__
236
237     def __repr__(self):
238         '''
239         Returns a short string representation of the object using the first
240         element from the properties() method.
241         '''
242         primary_property = self.properties()[0]
243         value = getattr(self, primary_property)
244         return '<%s %s>' % (self.classname(), str(value))
245
246     def __str__(self):
247         '''
248         Returns a human readable form of the object using the properties()
249         method.
250         '''
251         return '<%s %s>' % (self.classname(), self.json())
252
253     def not_null_constraints(self):
254         '''
255         Returns a list of properties that must be not NULL. Derived classes
256         should override this method if needed.
257         '''
258         return []
259
260     validation_message = \
261         "Validation failed because property '%s' must not be empty in object\n%s"
262
263     in_validation = False
264
265     def validate(self):
266         '''
267         This function validates the not NULL constraints as returned by
268         not_null_constraints(). It raises the DBUpdateError exception if
269         validation fails.
270         '''
271         for property in self.not_null_constraints():
272             # TODO: It is a bit awkward that the mapper configuration allow
273             # directly setting the numeric _id columns. We should get rid of it
274             # in the long run.
275             if hasattr(self, property + '_id') and \
276                 getattr(self, property + '_id') is not None:
277                 continue
278             if not hasattr(self, property) or getattr(self, property) is None:
279                 # str() might lead to races due to a 2nd flush
280                 self.in_validation = True
281                 message = self.validation_message % (property, str(self))
282                 self.in_validation = False
283                 raise DBUpdateError(message)
284
285     @classmethod
286     @session_wrapper
287     def get(cls, primary_key,  session = None):
288         '''
289         This is a support function that allows getting an object by its primary
290         key.
291
292         Architecture.get(3[, session])
293
294         instead of the more verbose
295
296         session.query(Architecture).get(3)
297         '''
298         return session.query(cls).get(primary_key)
299
300     def session(self, replace = False):
301         '''
302         Returns the current session that is associated with the object. May
303         return None is object is in detached state.
304         '''
305
306         return object_session(self)
307
308     def clone(self, session = None):
309         '''
310         Clones the current object in a new session and returns the new clone. A
311         fresh session is created if the optional session parameter is not
312         provided. The function will fail if a session is provided and has
313         unflushed changes.
314
315         RATIONALE: SQLAlchemy's session is not thread safe. This method clones
316         an existing object to allow several threads to work with their own
317         instances of an ORMObject.
318
319         WARNING: Only persistent (committed) objects can be cloned. Changes
320         made to the original object that are not committed yet will get lost.
321         The session of the new object will always be rolled back to avoid
322         ressource leaks.
323         '''
324
325         if self.session() is None:
326             raise RuntimeError( \
327                 'Method clone() failed for detached object:\n%s' % self)
328         self.session().flush()
329         mapper = object_mapper(self)
330         primary_key = mapper.primary_key_from_instance(self)
331         object_class = self.__class__
332         if session is None:
333             session = DBConn().session()
334         elif len(session.new) + len(session.dirty) + len(session.deleted) > 0:
335             raise RuntimeError( \
336                 'Method clone() failed due to unflushed changes in session.')
337         new_object = session.query(object_class).get(primary_key)
338         session.rollback()
339         if new_object is None:
340             raise RuntimeError( \
341                 'Method clone() failed for non-persistent object:\n%s' % self)
342         return new_object
343
344 __all__.append('ORMObject')
345
346 ################################################################################
347
348 class Validator(MapperExtension):
349     '''
350     This class calls the validate() method for each instance for the
351     'before_update' and 'before_insert' events. A global object validator is
352     used for configuring the individual mappers.
353     '''
354
355     def before_update(self, mapper, connection, instance):
356         instance.validate()
357         return EXT_CONTINUE
358
359     def before_insert(self, mapper, connection, instance):
360         instance.validate()
361         return EXT_CONTINUE
362
363 validator = Validator()
364
365 ################################################################################
366
367 class Architecture(ORMObject):
368     def __init__(self, arch_string = None, description = None):
369         self.arch_string = arch_string
370         self.description = description
371
372     def __eq__(self, val):
373         if isinstance(val, str):
374             return (self.arch_string== val)
375         # This signals to use the normal comparison operator
376         return NotImplemented
377
378     def __ne__(self, val):
379         if isinstance(val, str):
380             return (self.arch_string != val)
381         # This signals to use the normal comparison operator
382         return NotImplemented
383
384     def properties(self):
385         return ['arch_string', 'arch_id', 'suites_count']
386
387     def not_null_constraints(self):
388         return ['arch_string']
389
390 __all__.append('Architecture')
391
392 @session_wrapper
393 def get_architecture(architecture, session=None):
394     """
395     Returns database id for given C{architecture}.
396
397     @type architecture: string
398     @param architecture: The name of the architecture
399
400     @type session: Session
401     @param session: Optional SQLA session object (a temporary one will be
402     generated if not supplied)
403
404     @rtype: Architecture
405     @return: Architecture object for the given arch (None if not present)
406     """
407
408     q = session.query(Architecture).filter_by(arch_string=architecture)
409
410     try:
411         return q.one()
412     except NoResultFound:
413         return None
414
415 __all__.append('get_architecture')
416
417 # TODO: should be removed because the implementation is too trivial
418 @session_wrapper
419 def get_architecture_suites(architecture, session=None):
420     """
421     Returns list of Suite objects for given C{architecture} name
422
423     @type architecture: str
424     @param architecture: Architecture name to search for
425
426     @type session: Session
427     @param session: Optional SQL session object (a temporary one will be
428     generated if not supplied)
429
430     @rtype: list
431     @return: list of Suite objects for the given name (may be empty)
432     """
433
434     return get_architecture(architecture, session).suites
435
436 __all__.append('get_architecture_suites')
437
438 ################################################################################
439
440 class Archive(object):
441     def __init__(self, *args, **kwargs):
442         pass
443
444     def __repr__(self):
445         return '<Archive %s>' % self.archive_name
446
447 __all__.append('Archive')
448
449 @session_wrapper
450 def get_archive(archive, session=None):
451     """
452     returns database id for given C{archive}.
453
454     @type archive: string
455     @param archive: the name of the arhive
456
457     @type session: Session
458     @param session: Optional SQLA session object (a temporary one will be
459     generated if not supplied)
460
461     @rtype: Archive
462     @return: Archive object for the given name (None if not present)
463
464     """
465     archive = archive.lower()
466
467     q = session.query(Archive).filter_by(archive_name=archive)
468
469     try:
470         return q.one()
471     except NoResultFound:
472         return None
473
474 __all__.append('get_archive')
475
476 ################################################################################
477
478 class BinContents(ORMObject):
479     def __init__(self, file = None, binary = None):
480         self.file = file
481         self.binary = binary
482
483     def properties(self):
484         return ['file', 'binary']
485
486 __all__.append('BinContents')
487
488 ################################################################################
489
490 class DBBinary(ORMObject):
491     def __init__(self, package = None, source = None, version = None, \
492         maintainer = None, architecture = None, poolfile = None, \
493         binarytype = 'deb'):
494         self.package = package
495         self.source = source
496         self.version = version
497         self.maintainer = maintainer
498         self.architecture = architecture
499         self.poolfile = poolfile
500         self.binarytype = binarytype
501
502     @property
503     def pkid(self):
504         return self.binary_id
505
506     def properties(self):
507         return ['package', 'version', 'maintainer', 'source', 'architecture', \
508             'poolfile', 'binarytype', 'fingerprint', 'install_date', \
509             'suites_count', 'binary_id', 'contents_count', 'extra_sources']
510
511     def not_null_constraints(self):
512         return ['package', 'version', 'maintainer', 'source',  'poolfile', \
513             'binarytype']
514
515     metadata = association_proxy('key', 'value')
516
517     def get_component_name(self):
518         return self.poolfile.location.component.component_name
519
520     def scan_contents(self):
521         '''
522         Yields the contents of the package. Only regular files are yielded and
523         the path names are normalized after converting them from either utf-8
524         or iso8859-1 encoding. It yields the string ' <EMPTY PACKAGE>' if the
525         package does not contain any regular file.
526         '''
527         fullpath = self.poolfile.fullpath
528         dpkg = Popen(['dpkg-deb', '--fsys-tarfile', fullpath], stdout = PIPE)
529         tar = TarFile.open(fileobj = dpkg.stdout, mode = 'r|')
530         for member in tar.getmembers():
531             if not member.isdir():
532                 name = normpath(member.name)
533                 # enforce proper utf-8 encoding
534                 try:
535                     name.decode('utf-8')
536                 except UnicodeDecodeError:
537                     name = name.decode('iso8859-1').encode('utf-8')
538                 yield name
539         tar.close()
540         dpkg.stdout.close()
541         dpkg.wait()
542
543     def read_control(self):
544         '''
545         Reads the control information from a binary.
546
547         @rtype: text
548         @return: stanza text of the control section.
549         '''
550         import apt_inst
551         fullpath = self.poolfile.fullpath
552         deb_file = open(fullpath, 'r')
553         stanza = apt_inst.debExtractControl(deb_file)
554         deb_file.close()
555
556         return stanza
557
558     def read_control_fields(self):
559         '''
560         Reads the control information from a binary and return
561         as a dictionary.
562
563         @rtype: dict
564         @return: fields of the control section as a dictionary.
565         '''
566         import apt_pkg
567         stanza = self.read_control()
568         return apt_pkg.TagSection(stanza)
569
570 __all__.append('DBBinary')
571
572 @session_wrapper
573 def get_suites_binary_in(package, session=None):
574     """
575     Returns list of Suite objects which given C{package} name is in
576
577     @type package: str
578     @param package: DBBinary package name to search for
579
580     @rtype: list
581     @return: list of Suite objects for the given package
582     """
583
584     return session.query(Suite).filter(Suite.binaries.any(DBBinary.package == package)).all()
585
586 __all__.append('get_suites_binary_in')
587
588 @session_wrapper
589 def get_component_by_package_suite(package, suite_list, arch_list=[], session=None):
590     '''
591     Returns the component name of the newest binary package in suite_list or
592     None if no package is found. The result can be optionally filtered by a list
593     of architecture names.
594
595     @type package: str
596     @param package: DBBinary package name to search for
597
598     @type suite_list: list of str
599     @param suite_list: list of suite_name items
600
601     @type arch_list: list of str
602     @param arch_list: optional list of arch_string items that defaults to []
603
604     @rtype: str or NoneType
605     @return: name of component or None
606     '''
607
608     q = session.query(DBBinary).filter_by(package = package). \
609         join(DBBinary.suites).filter(Suite.suite_name.in_(suite_list))
610     if len(arch_list) > 0:
611         q = q.join(DBBinary.architecture). \
612             filter(Architecture.arch_string.in_(arch_list))
613     binary = q.order_by(desc(DBBinary.version)).first()
614     if binary is None:
615         return None
616     else:
617         return binary.get_component_name()
618
619 __all__.append('get_component_by_package_suite')
620
621 ################################################################################
622
623 class BinaryACL(object):
624     def __init__(self, *args, **kwargs):
625         pass
626
627     def __repr__(self):
628         return '<BinaryACL %s>' % self.binary_acl_id
629
630 __all__.append('BinaryACL')
631
632 ################################################################################
633
634 class BinaryACLMap(object):
635     def __init__(self, *args, **kwargs):
636         pass
637
638     def __repr__(self):
639         return '<BinaryACLMap %s>' % self.binary_acl_map_id
640
641 __all__.append('BinaryACLMap')
642
643 ################################################################################
644
645 MINIMAL_APT_CONF="""
646 Dir
647 {
648    ArchiveDir "%(archivepath)s";
649    OverrideDir "%(overridedir)s";
650    CacheDir "%(cachedir)s";
651 };
652
653 Default
654 {
655    Packages::Compress ". bzip2 gzip";
656    Sources::Compress ". bzip2 gzip";
657    DeLinkLimit 0;
658    FileMode 0664;
659 }
660
661 bindirectory "incoming"
662 {
663    Packages "Packages";
664    Contents " ";
665
666    BinOverride "override.sid.all3";
667    BinCacheDB "packages-accepted.db";
668
669    FileList "%(filelist)s";
670
671    PathPrefix "";
672    Packages::Extensions ".deb .udeb";
673 };
674
675 bindirectory "incoming/"
676 {
677    Sources "Sources";
678    BinOverride "override.sid.all3";
679    SrcOverride "override.sid.all3.src";
680    FileList "%(filelist)s";
681 };
682 """
683
684 class BuildQueue(object):
685     def __init__(self, *args, **kwargs):
686         pass
687
688     def __repr__(self):
689         return '<BuildQueue %s>' % self.queue_name
690
691     def write_metadata(self, starttime, force=False):
692         # Do we write out metafiles?
693         if not (force or self.generate_metadata):
694             return
695
696         session = DBConn().session().object_session(self)
697
698         fl_fd = fl_name = ac_fd = ac_name = None
699         tempdir = None
700         arches = " ".join([ a.arch_string for a in session.query(Architecture).all() if a.arch_string != 'source' ])
701         startdir = os.getcwd()
702
703         try:
704             # Grab files we want to include
705             newer = session.query(BuildQueueFile).filter_by(build_queue_id = self.queue_id).filter(BuildQueueFile.lastused + timedelta(seconds=self.stay_of_execution) > starttime).all()
706             newer += session.query(BuildQueuePolicyFile).filter_by(build_queue_id = self.queue_id).filter(BuildQueuePolicyFile.lastused + timedelta(seconds=self.stay_of_execution) > starttime).all()
707             # Write file list with newer files
708             (fl_fd, fl_name) = mkstemp()
709             for n in newer:
710                 os.write(fl_fd, '%s\n' % n.fullpath)
711             os.close(fl_fd)
712
713             cnf = Config()
714
715             # Write minimal apt.conf
716             # TODO: Remove hardcoding from template
717             (ac_fd, ac_name) = mkstemp()
718             os.write(ac_fd, MINIMAL_APT_CONF % {'archivepath': self.path,
719                                                 'filelist': fl_name,
720                                                 'cachedir': cnf["Dir::Cache"],
721                                                 'overridedir': cnf["Dir::Override"],
722                                                 })
723             os.close(ac_fd)
724
725             # Run apt-ftparchive generate
726             os.chdir(os.path.dirname(ac_name))
727             os.system('apt-ftparchive -qq -o APT::FTPArchive::Contents=off generate %s' % os.path.basename(ac_name))
728
729             # Run apt-ftparchive release
730             # TODO: Eww - fix this
731             bname = os.path.basename(self.path)
732             os.chdir(self.path)
733             os.chdir('..')
734
735             # We have to remove the Release file otherwise it'll be included in the
736             # new one
737             try:
738                 os.unlink(os.path.join(bname, 'Release'))
739             except OSError:
740                 pass
741
742             os.system("""apt-ftparchive -qq -o APT::FTPArchive::Release::Origin="%s" -o APT::FTPArchive::Release::Label="%s" -o APT::FTPArchive::Release::Description="%s" -o APT::FTPArchive::Release::Architectures="%s" release %s > Release""" % (self.origin, self.label, self.releasedescription, arches, bname))
743
744             # Crude hack with open and append, but this whole section is and should be redone.
745             if self.notautomatic:
746                 release=open("Release", "a")
747                 release.write("NotAutomatic: yes")
748                 release.close()
749
750             # Sign if necessary
751             if self.signingkey:
752                 keyring = "--secret-keyring \"%s\"" % cnf["Dinstall::SigningKeyring"]
753                 if cnf.has_key("Dinstall::SigningPubKeyring"):
754                     keyring += " --keyring \"%s\"" % cnf["Dinstall::SigningPubKeyring"]
755
756                 os.system("gpg %s --no-options --batch --no-tty --armour --default-key %s --detach-sign -o Release.gpg Release""" % (keyring, self.signingkey))
757
758             # Move the files if we got this far
759             os.rename('Release', os.path.join(bname, 'Release'))
760             if self.signingkey:
761                 os.rename('Release.gpg', os.path.join(bname, 'Release.gpg'))
762
763         # Clean up any left behind files
764         finally:
765             os.chdir(startdir)
766             if fl_fd:
767                 try:
768                     os.close(fl_fd)
769                 except OSError:
770                     pass
771
772             if fl_name:
773                 try:
774                     os.unlink(fl_name)
775                 except OSError:
776                     pass
777
778             if ac_fd:
779                 try:
780                     os.close(ac_fd)
781                 except OSError:
782                     pass
783
784             if ac_name:
785                 try:
786                     os.unlink(ac_name)
787                 except OSError:
788                     pass
789
790     def clean_and_update(self, starttime, Logger, dryrun=False):
791         """WARNING: This routine commits for you"""
792         session = DBConn().session().object_session(self)
793
794         if self.generate_metadata and not dryrun:
795             self.write_metadata(starttime)
796
797         # Grab files older than our execution time
798         older = session.query(BuildQueueFile).filter_by(build_queue_id = self.queue_id).filter(BuildQueueFile.lastused + timedelta(seconds=self.stay_of_execution) <= starttime).all()
799         older += session.query(BuildQueuePolicyFile).filter_by(build_queue_id = self.queue_id).filter(BuildQueuePolicyFile.lastused + timedelta(seconds=self.stay_of_execution) <= starttime).all()
800
801         for o in older:
802             killdb = False
803             try:
804                 if dryrun:
805                     Logger.log(["I: Would have removed %s from the queue" % o.fullpath])
806                 else:
807                     Logger.log(["I: Removing %s from the queue" % o.fullpath])
808                     os.unlink(o.fullpath)
809                     killdb = True
810             except OSError, e:
811                 # If it wasn't there, don't worry
812                 if e.errno == ENOENT:
813                     killdb = True
814                 else:
815                     # TODO: Replace with proper logging call
816                     Logger.log(["E: Could not remove %s" % o.fullpath])
817
818             if killdb:
819                 session.delete(o)
820
821         session.commit()
822
823         for f in os.listdir(self.path):
824             if f.startswith('Packages') or f.startswith('Source') or f.startswith('Release') or f.startswith('advisory'):
825                 continue
826
827             if not self.contains_filename(f):
828                 fp = os.path.join(self.path, f)
829                 if dryrun:
830                     Logger.log(["I: Would remove unused link %s" % fp])
831                 else:
832                     Logger.log(["I: Removing unused link %s" % fp])
833                     try:
834                         os.unlink(fp)
835                     except OSError:
836                         Logger.log(["E: Failed to unlink unreferenced file %s" % r.fullpath])
837
838     def contains_filename(self, filename):
839         """
840         @rtype Boolean
841         @returns True if filename is supposed to be in the queue; False otherwise
842         """
843         session = DBConn().session().object_session(self)
844         if session.query(BuildQueueFile).filter_by(build_queue_id = self.queue_id, filename = filename).count() > 0:
845             return True
846         elif session.query(BuildQueuePolicyFile).filter_by(build_queue = self, filename = filename).count() > 0:
847             return True
848         return False
849
850     def add_file_from_pool(self, poolfile):
851         """Copies a file into the pool.  Assumes that the PoolFile object is
852         attached to the same SQLAlchemy session as the Queue object is.
853
854         The caller is responsible for committing after calling this function."""
855         poolfile_basename = poolfile.filename[poolfile.filename.rindex(os.sep)+1:]
856
857         # Check if we have a file of this name or this ID already
858         for f in self.queuefiles:
859             if f.fileid is not None and f.fileid == poolfile.file_id or \
860                f.poolfile.filename == poolfile_basename:
861                    # In this case, update the BuildQueueFile entry so we
862                    # don't remove it too early
863                    f.lastused = datetime.now()
864                    DBConn().session().object_session(poolfile).add(f)
865                    return f
866
867         # Prepare BuildQueueFile object
868         qf = BuildQueueFile()
869         qf.build_queue_id = self.queue_id
870         qf.lastused = datetime.now()
871         qf.filename = poolfile_basename
872
873         targetpath = poolfile.fullpath
874         queuepath = os.path.join(self.path, poolfile_basename)
875
876         try:
877             if self.copy_files:
878                 # We need to copy instead of symlink
879                 import utils
880                 utils.copy(targetpath, queuepath)
881                 # NULL in the fileid field implies a copy
882                 qf.fileid = None
883             else:
884                 os.symlink(targetpath, queuepath)
885                 qf.fileid = poolfile.file_id
886         except OSError:
887             return None
888
889         # Get the same session as the PoolFile is using and add the qf to it
890         DBConn().session().object_session(poolfile).add(qf)
891
892         return qf
893
894     def add_changes_from_policy_queue(self, policyqueue, changes):
895         """
896         Copies a changes from a policy queue together with its poolfiles.
897
898         @type policyqueue: PolicyQueue
899         @param policyqueue: policy queue to copy the changes from
900
901         @type changes: DBChange
902         @param changes: changes to copy to this build queue
903         """
904         for policyqueuefile in changes.files:
905             self.add_file_from_policy_queue(policyqueue, policyqueuefile)
906         for poolfile in changes.poolfiles:
907             self.add_file_from_pool(poolfile)
908
909     def add_file_from_policy_queue(self, policyqueue, policyqueuefile):
910         """
911         Copies a file from a policy queue.
912         Assumes that the policyqueuefile is attached to the same SQLAlchemy
913         session as the Queue object is.  The caller is responsible for
914         committing after calling this function.
915
916         @type policyqueue: PolicyQueue
917         @param policyqueue: policy queue to copy the file from
918
919         @type policyqueuefile: ChangePendingFile
920         @param policyqueuefile: file to be added to the build queue
921         """
922         session = DBConn().session().object_session(policyqueuefile)
923
924         # Is the file already there?
925         try:
926             f = session.query(BuildQueuePolicyFile).filter_by(build_queue=self, file=policyqueuefile).one()
927             f.lastused = datetime.now()
928             return f
929         except NoResultFound:
930             pass # continue below
931
932         # We have to add the file.
933         f = BuildQueuePolicyFile()
934         f.build_queue = self
935         f.file = policyqueuefile
936         f.filename = policyqueuefile.filename
937
938         source = os.path.join(policyqueue.path, policyqueuefile.filename)
939         target = f.fullpath
940         try:
941             # Always copy files from policy queues as they might move around.
942             import utils
943             utils.copy(source, target)
944         except OSError:
945             return None
946
947         session.add(f)
948         return f
949
950 __all__.append('BuildQueue')
951
952 @session_wrapper
953 def get_build_queue(queuename, session=None):
954     """
955     Returns BuildQueue object for given C{queue name}, creating it if it does not
956     exist.
957
958     @type queuename: string
959     @param queuename: The name of the queue
960
961     @type session: Session
962     @param session: Optional SQLA session object (a temporary one will be
963     generated if not supplied)
964
965     @rtype: BuildQueue
966     @return: BuildQueue object for the given queue
967     """
968
969     q = session.query(BuildQueue).filter_by(queue_name=queuename)
970
971     try:
972         return q.one()
973     except NoResultFound:
974         return None
975
976 __all__.append('get_build_queue')
977
978 ################################################################################
979
980 class BuildQueueFile(object):
981     """
982     BuildQueueFile represents a file in a build queue coming from a pool.
983     """
984
985     def __init__(self, *args, **kwargs):
986         pass
987
988     def __repr__(self):
989         return '<BuildQueueFile %s (%s)>' % (self.filename, self.build_queue_id)
990
991     @property
992     def fullpath(self):
993         return os.path.join(self.buildqueue.path, self.filename)
994
995
996 __all__.append('BuildQueueFile')
997
998 ################################################################################
999
1000 class BuildQueuePolicyFile(object):
1001     """
1002     BuildQueuePolicyFile represents a file in a build queue that comes from a
1003     policy queue (and not a pool).
1004     """
1005
1006     def __init__(self, *args, **kwargs):
1007         pass
1008
1009     #@property
1010     #def filename(self):
1011     #    return self.file.filename
1012
1013     @property
1014     def fullpath(self):
1015         return os.path.join(self.build_queue.path, self.filename)
1016
1017 __all__.append('BuildQueuePolicyFile')
1018
1019 ################################################################################
1020
1021 class ChangePendingBinary(object):
1022     def __init__(self, *args, **kwargs):
1023         pass
1024
1025     def __repr__(self):
1026         return '<ChangePendingBinary %s>' % self.change_pending_binary_id
1027
1028 __all__.append('ChangePendingBinary')
1029
1030 ################################################################################
1031
1032 class ChangePendingFile(object):
1033     def __init__(self, *args, **kwargs):
1034         pass
1035
1036     def __repr__(self):
1037         return '<ChangePendingFile %s>' % self.change_pending_file_id
1038
1039 __all__.append('ChangePendingFile')
1040
1041 ################################################################################
1042
1043 class ChangePendingSource(object):
1044     def __init__(self, *args, **kwargs):
1045         pass
1046
1047     def __repr__(self):
1048         return '<ChangePendingSource %s>' % self.change_pending_source_id
1049
1050 __all__.append('ChangePendingSource')
1051
1052 ################################################################################
1053
1054 class Component(ORMObject):
1055     def __init__(self, component_name = None):
1056         self.component_name = component_name
1057
1058     def __eq__(self, val):
1059         if isinstance(val, str):
1060             return (self.component_name == val)
1061         # This signals to use the normal comparison operator
1062         return NotImplemented
1063
1064     def __ne__(self, val):
1065         if isinstance(val, str):
1066             return (self.component_name != val)
1067         # This signals to use the normal comparison operator
1068         return NotImplemented
1069
1070     def properties(self):
1071         return ['component_name', 'component_id', 'description', \
1072             'location_count', 'meets_dfsg', 'overrides_count']
1073
1074     def not_null_constraints(self):
1075         return ['component_name']
1076
1077
1078 __all__.append('Component')
1079
1080 @session_wrapper
1081 def get_component(component, session=None):
1082     """
1083     Returns database id for given C{component}.
1084
1085     @type component: string
1086     @param component: The name of the override type
1087
1088     @rtype: int
1089     @return: the database id for the given component
1090
1091     """
1092     component = component.lower()
1093
1094     q = session.query(Component).filter_by(component_name=component)
1095
1096     try:
1097         return q.one()
1098     except NoResultFound:
1099         return None
1100
1101 __all__.append('get_component')
1102
1103 ################################################################################
1104
1105 class DBConfig(object):
1106     def __init__(self, *args, **kwargs):
1107         pass
1108
1109     def __repr__(self):
1110         return '<DBConfig %s>' % self.name
1111
1112 __all__.append('DBConfig')
1113
1114 ################################################################################
1115
1116 @session_wrapper
1117 def get_or_set_contents_file_id(filename, session=None):
1118     """
1119     Returns database id for given filename.
1120
1121     If no matching file is found, a row is inserted.
1122
1123     @type filename: string
1124     @param filename: The filename
1125     @type session: SQLAlchemy
1126     @param session: Optional SQL session object (a temporary one will be
1127     generated if not supplied).  If not passed, a commit will be performed at
1128     the end of the function, otherwise the caller is responsible for commiting.
1129
1130     @rtype: int
1131     @return: the database id for the given component
1132     """
1133
1134     q = session.query(ContentFilename).filter_by(filename=filename)
1135
1136     try:
1137         ret = q.one().cafilename_id
1138     except NoResultFound:
1139         cf = ContentFilename()
1140         cf.filename = filename
1141         session.add(cf)
1142         session.commit_or_flush()
1143         ret = cf.cafilename_id
1144
1145     return ret
1146
1147 __all__.append('get_or_set_contents_file_id')
1148
1149 @session_wrapper
1150 def get_contents(suite, overridetype, section=None, session=None):
1151     """
1152     Returns contents for a suite / overridetype combination, limiting
1153     to a section if not None.
1154
1155     @type suite: Suite
1156     @param suite: Suite object
1157
1158     @type overridetype: OverrideType
1159     @param overridetype: OverrideType object
1160
1161     @type section: Section
1162     @param section: Optional section object to limit results to
1163
1164     @type session: SQLAlchemy
1165     @param session: Optional SQL session object (a temporary one will be
1166     generated if not supplied)
1167
1168     @rtype: ResultsProxy
1169     @return: ResultsProxy object set up to return tuples of (filename, section,
1170     package, arch_id)
1171     """
1172
1173     # find me all of the contents for a given suite
1174     contents_q = """SELECT (p.path||'/'||n.file) AS fn,
1175                             s.section,
1176                             b.package,
1177                             b.architecture
1178                    FROM content_associations c join content_file_paths p ON (c.filepath=p.id)
1179                    JOIN content_file_names n ON (c.filename=n.id)
1180                    JOIN binaries b ON (b.id=c.binary_pkg)
1181                    JOIN override o ON (o.package=b.package)
1182                    JOIN section s ON (s.id=o.section)
1183                    WHERE o.suite = :suiteid AND o.type = :overridetypeid
1184                    AND b.type=:overridetypename"""
1185
1186     vals = {'suiteid': suite.suite_id,
1187             'overridetypeid': overridetype.overridetype_id,
1188             'overridetypename': overridetype.overridetype}
1189
1190     if section is not None:
1191         contents_q += " AND s.id = :sectionid"
1192         vals['sectionid'] = section.section_id
1193
1194     contents_q += " ORDER BY fn"
1195
1196     return session.execute(contents_q, vals)
1197
1198 __all__.append('get_contents')
1199
1200 ################################################################################
1201
1202 class ContentFilepath(object):
1203     def __init__(self, *args, **kwargs):
1204         pass
1205
1206     def __repr__(self):
1207         return '<ContentFilepath %s>' % self.filepath
1208
1209 __all__.append('ContentFilepath')
1210
1211 @session_wrapper
1212 def get_or_set_contents_path_id(filepath, session=None):
1213     """
1214     Returns database id for given path.
1215
1216     If no matching file is found, a row is inserted.
1217
1218     @type filepath: string
1219     @param filepath: The filepath
1220
1221     @type session: SQLAlchemy
1222     @param session: Optional SQL session object (a temporary one will be
1223     generated if not supplied).  If not passed, a commit will be performed at
1224     the end of the function, otherwise the caller is responsible for commiting.
1225
1226     @rtype: int
1227     @return: the database id for the given path
1228     """
1229
1230     q = session.query(ContentFilepath).filter_by(filepath=filepath)
1231
1232     try:
1233         ret = q.one().cafilepath_id
1234     except NoResultFound:
1235         cf = ContentFilepath()
1236         cf.filepath = filepath
1237         session.add(cf)
1238         session.commit_or_flush()
1239         ret = cf.cafilepath_id
1240
1241     return ret
1242
1243 __all__.append('get_or_set_contents_path_id')
1244
1245 ################################################################################
1246
1247 class ContentAssociation(object):
1248     def __init__(self, *args, **kwargs):
1249         pass
1250
1251     def __repr__(self):
1252         return '<ContentAssociation %s>' % self.ca_id
1253
1254 __all__.append('ContentAssociation')
1255
1256 def insert_content_paths(binary_id, fullpaths, session=None):
1257     """
1258     Make sure given path is associated with given binary id
1259
1260     @type binary_id: int
1261     @param binary_id: the id of the binary
1262     @type fullpaths: list
1263     @param fullpaths: the list of paths of the file being associated with the binary
1264     @type session: SQLAlchemy session
1265     @param session: Optional SQLAlchemy session.  If this is passed, the caller
1266     is responsible for ensuring a transaction has begun and committing the
1267     results or rolling back based on the result code.  If not passed, a commit
1268     will be performed at the end of the function, otherwise the caller is
1269     responsible for commiting.
1270
1271     @return: True upon success
1272     """
1273
1274     privatetrans = False
1275     if session is None:
1276         session = DBConn().session()
1277         privatetrans = True
1278
1279     try:
1280         # Insert paths
1281         def generate_path_dicts():
1282             for fullpath in fullpaths:
1283                 if fullpath.startswith( './' ):
1284                     fullpath = fullpath[2:]
1285
1286                 yield {'filename':fullpath, 'id': binary_id }
1287
1288         for d in generate_path_dicts():
1289             session.execute( "INSERT INTO bin_contents ( file, binary_id ) VALUES ( :filename, :id )",
1290                          d )
1291
1292         session.commit()
1293         if privatetrans:
1294             session.close()
1295         return True
1296
1297     except:
1298         traceback.print_exc()
1299
1300         # Only rollback if we set up the session ourself
1301         if privatetrans:
1302             session.rollback()
1303             session.close()
1304
1305         return False
1306
1307 __all__.append('insert_content_paths')
1308
1309 ################################################################################
1310
1311 class DSCFile(object):
1312     def __init__(self, *args, **kwargs):
1313         pass
1314
1315     def __repr__(self):
1316         return '<DSCFile %s>' % self.dscfile_id
1317
1318 __all__.append('DSCFile')
1319
1320 @session_wrapper
1321 def get_dscfiles(dscfile_id=None, source_id=None, poolfile_id=None, session=None):
1322     """
1323     Returns a list of DSCFiles which may be empty
1324
1325     @type dscfile_id: int (optional)
1326     @param dscfile_id: the dscfile_id of the DSCFiles to find
1327
1328     @type source_id: int (optional)
1329     @param source_id: the source id related to the DSCFiles to find
1330
1331     @type poolfile_id: int (optional)
1332     @param poolfile_id: the poolfile id related to the DSCFiles to find
1333
1334     @rtype: list
1335     @return: Possibly empty list of DSCFiles
1336     """
1337
1338     q = session.query(DSCFile)
1339
1340     if dscfile_id is not None:
1341         q = q.filter_by(dscfile_id=dscfile_id)
1342
1343     if source_id is not None:
1344         q = q.filter_by(source_id=source_id)
1345
1346     if poolfile_id is not None:
1347         q = q.filter_by(poolfile_id=poolfile_id)
1348
1349     return q.all()
1350
1351 __all__.append('get_dscfiles')
1352
1353 ################################################################################
1354
1355 class PoolFile(ORMObject):
1356     def __init__(self, filename = None, location = None, filesize = -1, \
1357         md5sum = None):
1358         self.filename = filename
1359         self.location = location
1360         self.filesize = filesize
1361         self.md5sum = md5sum
1362
1363     @property
1364     def fullpath(self):
1365         return os.path.join(self.location.path, self.filename)
1366
1367     def is_valid(self, filesize = -1, md5sum = None):
1368         return self.filesize == long(filesize) and self.md5sum == md5sum
1369
1370     def properties(self):
1371         return ['filename', 'file_id', 'filesize', 'md5sum', 'sha1sum', \
1372             'sha256sum', 'location', 'source', 'binary', 'last_used']
1373
1374     def not_null_constraints(self):
1375         return ['filename', 'md5sum', 'location']
1376
1377 __all__.append('PoolFile')
1378
1379 @session_wrapper
1380 def check_poolfile(filename, filesize, md5sum, location_id, session=None):
1381     """
1382     Returns a tuple:
1383     (ValidFileFound [boolean], PoolFile object or None)
1384
1385     @type filename: string
1386     @param filename: the filename of the file to check against the DB
1387
1388     @type filesize: int
1389     @param filesize: the size of the file to check against the DB
1390
1391     @type md5sum: string
1392     @param md5sum: the md5sum of the file to check against the DB
1393
1394     @type location_id: int
1395     @param location_id: the id of the location to look in
1396
1397     @rtype: tuple
1398     @return: Tuple of length 2.
1399                  - If valid pool file found: (C{True}, C{PoolFile object})
1400                  - If valid pool file not found:
1401                      - (C{False}, C{None}) if no file found
1402                      - (C{False}, C{PoolFile object}) if file found with size/md5sum mismatch
1403     """
1404
1405     poolfile = session.query(Location).get(location_id). \
1406         files.filter_by(filename=filename).first()
1407     valid = False
1408     if poolfile and poolfile.is_valid(filesize = filesize, md5sum = md5sum):
1409         valid = True
1410
1411     return (valid, poolfile)
1412
1413 __all__.append('check_poolfile')
1414
1415 # TODO: the implementation can trivially be inlined at the place where the
1416 # function is called
1417 @session_wrapper
1418 def get_poolfile_by_id(file_id, session=None):
1419     """
1420     Returns a PoolFile objects or None for the given id
1421
1422     @type file_id: int
1423     @param file_id: the id of the file to look for
1424
1425     @rtype: PoolFile or None
1426     @return: either the PoolFile object or None
1427     """
1428
1429     return session.query(PoolFile).get(file_id)
1430
1431 __all__.append('get_poolfile_by_id')
1432
1433 @session_wrapper
1434 def get_poolfile_like_name(filename, session=None):
1435     """
1436     Returns an array of PoolFile objects which are like the given name
1437
1438     @type filename: string
1439     @param filename: the filename of the file to check against the DB
1440
1441     @rtype: array
1442     @return: array of PoolFile objects
1443     """
1444
1445     # TODO: There must be a way of properly using bind parameters with %FOO%
1446     q = session.query(PoolFile).filter(PoolFile.filename.like('%%/%s' % filename))
1447
1448     return q.all()
1449
1450 __all__.append('get_poolfile_like_name')
1451
1452 @session_wrapper
1453 def add_poolfile(filename, datadict, location_id, session=None):
1454     """
1455     Add a new file to the pool
1456
1457     @type filename: string
1458     @param filename: filename
1459
1460     @type datadict: dict
1461     @param datadict: dict with needed data
1462
1463     @type location_id: int
1464     @param location_id: database id of the location
1465
1466     @rtype: PoolFile
1467     @return: the PoolFile object created
1468     """
1469     poolfile = PoolFile()
1470     poolfile.filename = filename
1471     poolfile.filesize = datadict["size"]
1472     poolfile.md5sum = datadict["md5sum"]
1473     poolfile.sha1sum = datadict["sha1sum"]
1474     poolfile.sha256sum = datadict["sha256sum"]
1475     poolfile.location_id = location_id
1476
1477     session.add(poolfile)
1478     # Flush to get a file id (NB: This is not a commit)
1479     session.flush()
1480
1481     return poolfile
1482
1483 __all__.append('add_poolfile')
1484
1485 ################################################################################
1486
1487 class Fingerprint(ORMObject):
1488     def __init__(self, fingerprint = None):
1489         self.fingerprint = fingerprint
1490
1491     def properties(self):
1492         return ['fingerprint', 'fingerprint_id', 'keyring', 'uid', \
1493             'binary_reject']
1494
1495     def not_null_constraints(self):
1496         return ['fingerprint']
1497
1498 __all__.append('Fingerprint')
1499
1500 @session_wrapper
1501 def get_fingerprint(fpr, session=None):
1502     """
1503     Returns Fingerprint object for given fpr.
1504
1505     @type fpr: string
1506     @param fpr: The fpr to find / add
1507
1508     @type session: SQLAlchemy
1509     @param session: Optional SQL session object (a temporary one will be
1510     generated if not supplied).
1511
1512     @rtype: Fingerprint
1513     @return: the Fingerprint object for the given fpr or None
1514     """
1515
1516     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1517
1518     try:
1519         ret = q.one()
1520     except NoResultFound:
1521         ret = None
1522
1523     return ret
1524
1525 __all__.append('get_fingerprint')
1526
1527 @session_wrapper
1528 def get_or_set_fingerprint(fpr, session=None):
1529     """
1530     Returns Fingerprint object for given fpr.
1531
1532     If no matching fpr is found, a row is inserted.
1533
1534     @type fpr: string
1535     @param fpr: The fpr to find / add
1536
1537     @type session: SQLAlchemy
1538     @param session: Optional SQL session object (a temporary one will be
1539     generated if not supplied).  If not passed, a commit will be performed at
1540     the end of the function, otherwise the caller is responsible for commiting.
1541     A flush will be performed either way.
1542
1543     @rtype: Fingerprint
1544     @return: the Fingerprint object for the given fpr
1545     """
1546
1547     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1548
1549     try:
1550         ret = q.one()
1551     except NoResultFound:
1552         fingerprint = Fingerprint()
1553         fingerprint.fingerprint = fpr
1554         session.add(fingerprint)
1555         session.commit_or_flush()
1556         ret = fingerprint
1557
1558     return ret
1559
1560 __all__.append('get_or_set_fingerprint')
1561
1562 ################################################################################
1563
1564 # Helper routine for Keyring class
1565 def get_ldap_name(entry):
1566     name = []
1567     for k in ["cn", "mn", "sn"]:
1568         ret = entry.get(k)
1569         if ret and ret[0] != "" and ret[0] != "-":
1570             name.append(ret[0])
1571     return " ".join(name)
1572
1573 ################################################################################
1574
1575 class Keyring(object):
1576     gpg_invocation = "gpg --no-default-keyring --keyring %s" +\
1577                      " --with-colons --fingerprint --fingerprint"
1578
1579     keys = {}
1580     fpr_lookup = {}
1581
1582     def __init__(self, *args, **kwargs):
1583         pass
1584
1585     def __repr__(self):
1586         return '<Keyring %s>' % self.keyring_name
1587
1588     def de_escape_gpg_str(self, txt):
1589         esclist = re.split(r'(\\x..)', txt)
1590         for x in range(1,len(esclist),2):
1591             esclist[x] = "%c" % (int(esclist[x][2:],16))
1592         return "".join(esclist)
1593
1594     def parse_address(self, uid):
1595         """parses uid and returns a tuple of real name and email address"""
1596         import email.Utils
1597         (name, address) = email.Utils.parseaddr(uid)
1598         name = re.sub(r"\s*[(].*[)]", "", name)
1599         name = self.de_escape_gpg_str(name)
1600         if name == "":
1601             name = uid
1602         return (name, address)
1603
1604     def load_keys(self, keyring):
1605         if not self.keyring_id:
1606             raise Exception('Must be initialized with database information')
1607
1608         k = os.popen(self.gpg_invocation % keyring, "r")
1609         key = None
1610         signingkey = False
1611
1612         for line in k.xreadlines():
1613             field = line.split(":")
1614             if field[0] == "pub":
1615                 key = field[4]
1616                 self.keys[key] = {}
1617                 (name, addr) = self.parse_address(field[9])
1618                 if "@" in addr:
1619                     self.keys[key]["email"] = addr
1620                     self.keys[key]["name"] = name
1621                 self.keys[key]["fingerprints"] = []
1622                 signingkey = True
1623             elif key and field[0] == "sub" and len(field) >= 12:
1624                 signingkey = ("s" in field[11])
1625             elif key and field[0] == "uid":
1626                 (name, addr) = self.parse_address(field[9])
1627                 if "email" not in self.keys[key] and "@" in addr:
1628                     self.keys[key]["email"] = addr
1629                     self.keys[key]["name"] = name
1630             elif signingkey and field[0] == "fpr":
1631                 self.keys[key]["fingerprints"].append(field[9])
1632                 self.fpr_lookup[field[9]] = key
1633
1634     def import_users_from_ldap(self, session):
1635         import ldap
1636         cnf = Config()
1637
1638         LDAPDn = cnf["Import-LDAP-Fingerprints::LDAPDn"]
1639         LDAPServer = cnf["Import-LDAP-Fingerprints::LDAPServer"]
1640
1641         l = ldap.open(LDAPServer)
1642         l.simple_bind_s("","")
1643         Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
1644                "(&(keyfingerprint=*)(gidnumber=%s))" % (cnf["Import-Users-From-Passwd::ValidGID"]),
1645                ["uid", "keyfingerprint", "cn", "mn", "sn"])
1646
1647         ldap_fin_uid_id = {}
1648
1649         byuid = {}
1650         byname = {}
1651
1652         for i in Attrs:
1653             entry = i[1]
1654             uid = entry["uid"][0]
1655             name = get_ldap_name(entry)
1656             fingerprints = entry["keyFingerPrint"]
1657             keyid = None
1658             for f in fingerprints:
1659                 key = self.fpr_lookup.get(f, None)
1660                 if key not in self.keys:
1661                     continue
1662                 self.keys[key]["uid"] = uid
1663
1664                 if keyid != None:
1665                     continue
1666                 keyid = get_or_set_uid(uid, session).uid_id
1667                 byuid[keyid] = (uid, name)
1668                 byname[uid] = (keyid, name)
1669
1670         return (byname, byuid)
1671
1672     def generate_users_from_keyring(self, format, session):
1673         byuid = {}
1674         byname = {}
1675         any_invalid = False
1676         for x in self.keys.keys():
1677             if "email" not in self.keys[x]:
1678                 any_invalid = True
1679                 self.keys[x]["uid"] = format % "invalid-uid"
1680             else:
1681                 uid = format % self.keys[x]["email"]
1682                 keyid = get_or_set_uid(uid, session).uid_id
1683                 byuid[keyid] = (uid, self.keys[x]["name"])
1684                 byname[uid] = (keyid, self.keys[x]["name"])
1685                 self.keys[x]["uid"] = uid
1686
1687         if any_invalid:
1688             uid = format % "invalid-uid"
1689             keyid = get_or_set_uid(uid, session).uid_id
1690             byuid[keyid] = (uid, "ungeneratable user id")
1691             byname[uid] = (keyid, "ungeneratable user id")
1692
1693         return (byname, byuid)
1694
1695 __all__.append('Keyring')
1696
1697 @session_wrapper
1698 def get_keyring(keyring, session=None):
1699     """
1700     If C{keyring} does not have an entry in the C{keyrings} table yet, return None
1701     If C{keyring} already has an entry, simply return the existing Keyring
1702
1703     @type keyring: string
1704     @param keyring: the keyring name
1705
1706     @rtype: Keyring
1707     @return: the Keyring object for this keyring
1708     """
1709
1710     q = session.query(Keyring).filter_by(keyring_name=keyring)
1711
1712     try:
1713         return q.one()
1714     except NoResultFound:
1715         return None
1716
1717 __all__.append('get_keyring')
1718
1719 ################################################################################
1720
1721 class KeyringACLMap(object):
1722     def __init__(self, *args, **kwargs):
1723         pass
1724
1725     def __repr__(self):
1726         return '<KeyringACLMap %s>' % self.keyring_acl_map_id
1727
1728 __all__.append('KeyringACLMap')
1729
1730 ################################################################################
1731
1732 class DBChange(object):
1733     def __init__(self, *args, **kwargs):
1734         pass
1735
1736     def __repr__(self):
1737         return '<DBChange %s>' % self.changesname
1738
1739     def clean_from_queue(self):
1740         session = DBConn().session().object_session(self)
1741
1742         # Remove changes_pool_files entries
1743         self.poolfiles = []
1744
1745         # Remove changes_pending_files references
1746         self.files = []
1747
1748         # Clear out of queue
1749         self.in_queue = None
1750         self.approved_for_id = None
1751
1752 __all__.append('DBChange')
1753
1754 @session_wrapper
1755 def get_dbchange(filename, session=None):
1756     """
1757     returns DBChange object for given C{filename}.
1758
1759     @type filename: string
1760     @param filename: the name of the file
1761
1762     @type session: Session
1763     @param session: Optional SQLA session object (a temporary one will be
1764     generated if not supplied)
1765
1766     @rtype: DBChange
1767     @return:  DBChange object for the given filename (C{None} if not present)
1768
1769     """
1770     q = session.query(DBChange).filter_by(changesname=filename)
1771
1772     try:
1773         return q.one()
1774     except NoResultFound:
1775         return None
1776
1777 __all__.append('get_dbchange')
1778
1779 ################################################################################
1780
1781 class Location(ORMObject):
1782     def __init__(self, path = None, component = None):
1783         self.path = path
1784         self.component = component
1785         # the column 'type' should go away, see comment at mapper
1786         self.archive_type = 'pool'
1787
1788     def properties(self):
1789         return ['path', 'location_id', 'archive_type', 'component', \
1790             'files_count']
1791
1792     def not_null_constraints(self):
1793         return ['path', 'archive_type']
1794
1795 __all__.append('Location')
1796
1797 @session_wrapper
1798 def get_location(location, component=None, archive=None, session=None):
1799     """
1800     Returns Location object for the given combination of location, component
1801     and archive
1802
1803     @type location: string
1804     @param location: the path of the location, e.g. I{/srv/ftp-master.debian.org/ftp/pool/}
1805
1806     @type component: string
1807     @param component: the component name (if None, no restriction applied)
1808
1809     @type archive: string
1810     @param archive: the archive name (if None, no restriction applied)
1811
1812     @rtype: Location / None
1813     @return: Either a Location object or None if one can't be found
1814     """
1815
1816     q = session.query(Location).filter_by(path=location)
1817
1818     if archive is not None:
1819         q = q.join(Archive).filter_by(archive_name=archive)
1820
1821     if component is not None:
1822         q = q.join(Component).filter_by(component_name=component)
1823
1824     try:
1825         return q.one()
1826     except NoResultFound:
1827         return None
1828
1829 __all__.append('get_location')
1830
1831 ################################################################################
1832
1833 class Maintainer(ORMObject):
1834     def __init__(self, name = None):
1835         self.name = name
1836
1837     def properties(self):
1838         return ['name', 'maintainer_id']
1839
1840     def not_null_constraints(self):
1841         return ['name']
1842
1843     def get_split_maintainer(self):
1844         if not hasattr(self, 'name') or self.name is None:
1845             return ('', '', '', '')
1846
1847         return fix_maintainer(self.name.strip())
1848
1849 __all__.append('Maintainer')
1850
1851 @session_wrapper
1852 def get_or_set_maintainer(name, session=None):
1853     """
1854     Returns Maintainer object for given maintainer name.
1855
1856     If no matching maintainer name is found, a row is inserted.
1857
1858     @type name: string
1859     @param name: The maintainer name to add
1860
1861     @type session: SQLAlchemy
1862     @param session: Optional SQL session object (a temporary one will be
1863     generated if not supplied).  If not passed, a commit will be performed at
1864     the end of the function, otherwise the caller is responsible for commiting.
1865     A flush will be performed either way.
1866
1867     @rtype: Maintainer
1868     @return: the Maintainer object for the given maintainer
1869     """
1870
1871     q = session.query(Maintainer).filter_by(name=name)
1872     try:
1873         ret = q.one()
1874     except NoResultFound:
1875         maintainer = Maintainer()
1876         maintainer.name = name
1877         session.add(maintainer)
1878         session.commit_or_flush()
1879         ret = maintainer
1880
1881     return ret
1882
1883 __all__.append('get_or_set_maintainer')
1884
1885 @session_wrapper
1886 def get_maintainer(maintainer_id, session=None):
1887     """
1888     Return the name of the maintainer behind C{maintainer_id} or None if that
1889     maintainer_id is invalid.
1890
1891     @type maintainer_id: int
1892     @param maintainer_id: the id of the maintainer
1893
1894     @rtype: Maintainer
1895     @return: the Maintainer with this C{maintainer_id}
1896     """
1897
1898     return session.query(Maintainer).get(maintainer_id)
1899
1900 __all__.append('get_maintainer')
1901
1902 ################################################################################
1903
1904 class NewComment(object):
1905     def __init__(self, *args, **kwargs):
1906         pass
1907
1908     def __repr__(self):
1909         return '''<NewComment for '%s %s' (%s)>''' % (self.package, self.version, self.comment_id)
1910
1911 __all__.append('NewComment')
1912
1913 @session_wrapper
1914 def has_new_comment(package, version, session=None):
1915     """
1916     Returns true if the given combination of C{package}, C{version} has a comment.
1917
1918     @type package: string
1919     @param package: name of the package
1920
1921     @type version: string
1922     @param version: package version
1923
1924     @type session: Session
1925     @param session: Optional SQLA session object (a temporary one will be
1926     generated if not supplied)
1927
1928     @rtype: boolean
1929     @return: true/false
1930     """
1931
1932     q = session.query(NewComment)
1933     q = q.filter_by(package=package)
1934     q = q.filter_by(version=version)
1935
1936     return bool(q.count() > 0)
1937
1938 __all__.append('has_new_comment')
1939
1940 @session_wrapper
1941 def get_new_comments(package=None, version=None, comment_id=None, session=None):
1942     """
1943     Returns (possibly empty) list of NewComment objects for the given
1944     parameters
1945
1946     @type package: string (optional)
1947     @param package: name of the package
1948
1949     @type version: string (optional)
1950     @param version: package version
1951
1952     @type comment_id: int (optional)
1953     @param comment_id: An id of a comment
1954
1955     @type session: Session
1956     @param session: Optional SQLA session object (a temporary one will be
1957     generated if not supplied)
1958
1959     @rtype: list
1960     @return: A (possibly empty) list of NewComment objects will be returned
1961     """
1962
1963     q = session.query(NewComment)
1964     if package is not None: q = q.filter_by(package=package)
1965     if version is not None: q = q.filter_by(version=version)
1966     if comment_id is not None: q = q.filter_by(comment_id=comment_id)
1967
1968     return q.all()
1969
1970 __all__.append('get_new_comments')
1971
1972 ################################################################################
1973
1974 class Override(ORMObject):
1975     def __init__(self, package = None, suite = None, component = None, overridetype = None, \
1976         section = None, priority = None):
1977         self.package = package
1978         self.suite = suite
1979         self.component = component
1980         self.overridetype = overridetype
1981         self.section = section
1982         self.priority = priority
1983
1984     def properties(self):
1985         return ['package', 'suite', 'component', 'overridetype', 'section', \
1986             'priority']
1987
1988     def not_null_constraints(self):
1989         return ['package', 'suite', 'component', 'overridetype', 'section']
1990
1991 __all__.append('Override')
1992
1993 @session_wrapper
1994 def get_override(package, suite=None, component=None, overridetype=None, session=None):
1995     """
1996     Returns Override object for the given parameters
1997
1998     @type package: string
1999     @param package: The name of the package
2000
2001     @type suite: string, list or None
2002     @param suite: The name of the suite (or suites if a list) to limit to.  If
2003                   None, don't limit.  Defaults to None.
2004
2005     @type component: string, list or None
2006     @param component: The name of the component (or components if a list) to
2007                       limit to.  If None, don't limit.  Defaults to None.
2008
2009     @type overridetype: string, list or None
2010     @param overridetype: The name of the overridetype (or overridetypes if a list) to
2011                          limit to.  If None, don't limit.  Defaults to None.
2012
2013     @type session: Session
2014     @param session: Optional SQLA session object (a temporary one will be
2015     generated if not supplied)
2016
2017     @rtype: list
2018     @return: A (possibly empty) list of Override objects will be returned
2019     """
2020
2021     q = session.query(Override)
2022     q = q.filter_by(package=package)
2023
2024     if suite is not None:
2025         if not isinstance(suite, list): suite = [suite]
2026         q = q.join(Suite).filter(Suite.suite_name.in_(suite))
2027
2028     if component is not None:
2029         if not isinstance(component, list): component = [component]
2030         q = q.join(Component).filter(Component.component_name.in_(component))
2031
2032     if overridetype is not None:
2033         if not isinstance(overridetype, list): overridetype = [overridetype]
2034         q = q.join(OverrideType).filter(OverrideType.overridetype.in_(overridetype))
2035
2036     return q.all()
2037
2038 __all__.append('get_override')
2039
2040
2041 ################################################################################
2042
2043 class OverrideType(ORMObject):
2044     def __init__(self, overridetype = None):
2045         self.overridetype = overridetype
2046
2047     def properties(self):
2048         return ['overridetype', 'overridetype_id', 'overrides_count']
2049
2050     def not_null_constraints(self):
2051         return ['overridetype']
2052
2053 __all__.append('OverrideType')
2054
2055 @session_wrapper
2056 def get_override_type(override_type, session=None):
2057     """
2058     Returns OverrideType object for given C{override type}.
2059
2060     @type override_type: string
2061     @param override_type: The name of the override type
2062
2063     @type session: Session
2064     @param session: Optional SQLA session object (a temporary one will be
2065     generated if not supplied)
2066
2067     @rtype: int
2068     @return: the database id for the given override type
2069     """
2070
2071     q = session.query(OverrideType).filter_by(overridetype=override_type)
2072
2073     try:
2074         return q.one()
2075     except NoResultFound:
2076         return None
2077
2078 __all__.append('get_override_type')
2079
2080 ################################################################################
2081
2082 class PolicyQueue(object):
2083     def __init__(self, *args, **kwargs):
2084         pass
2085
2086     def __repr__(self):
2087         return '<PolicyQueue %s>' % self.queue_name
2088
2089 __all__.append('PolicyQueue')
2090
2091 @session_wrapper
2092 def get_policy_queue(queuename, session=None):
2093     """
2094     Returns PolicyQueue object for given C{queue name}
2095
2096     @type queuename: string
2097     @param queuename: The name of the queue
2098
2099     @type session: Session
2100     @param session: Optional SQLA session object (a temporary one will be
2101     generated if not supplied)
2102
2103     @rtype: PolicyQueue
2104     @return: PolicyQueue object for the given queue
2105     """
2106
2107     q = session.query(PolicyQueue).filter_by(queue_name=queuename)
2108
2109     try:
2110         return q.one()
2111     except NoResultFound:
2112         return None
2113
2114 __all__.append('get_policy_queue')
2115
2116 @session_wrapper
2117 def get_policy_queue_from_path(pathname, session=None):
2118     """
2119     Returns PolicyQueue object for given C{path name}
2120
2121     @type queuename: string
2122     @param queuename: The path
2123
2124     @type session: Session
2125     @param session: Optional SQLA session object (a temporary one will be
2126     generated if not supplied)
2127
2128     @rtype: PolicyQueue
2129     @return: PolicyQueue object for the given queue
2130     """
2131
2132     q = session.query(PolicyQueue).filter_by(path=pathname)
2133
2134     try:
2135         return q.one()
2136     except NoResultFound:
2137         return None
2138
2139 __all__.append('get_policy_queue_from_path')
2140
2141 ################################################################################
2142
2143 class Priority(ORMObject):
2144     def __init__(self, priority = None, level = None):
2145         self.priority = priority
2146         self.level = level
2147
2148     def properties(self):
2149         return ['priority', 'priority_id', 'level', 'overrides_count']
2150
2151     def not_null_constraints(self):
2152         return ['priority', 'level']
2153
2154     def __eq__(self, val):
2155         if isinstance(val, str):
2156             return (self.priority == val)
2157         # This signals to use the normal comparison operator
2158         return NotImplemented
2159
2160     def __ne__(self, val):
2161         if isinstance(val, str):
2162             return (self.priority != val)
2163         # This signals to use the normal comparison operator
2164         return NotImplemented
2165
2166 __all__.append('Priority')
2167
2168 @session_wrapper
2169 def get_priority(priority, session=None):
2170     """
2171     Returns Priority object for given C{priority name}.
2172
2173     @type priority: string
2174     @param priority: The name of the priority
2175
2176     @type session: Session
2177     @param session: Optional SQLA session object (a temporary one will be
2178     generated if not supplied)
2179
2180     @rtype: Priority
2181     @return: Priority object for the given priority
2182     """
2183
2184     q = session.query(Priority).filter_by(priority=priority)
2185
2186     try:
2187         return q.one()
2188     except NoResultFound:
2189         return None
2190
2191 __all__.append('get_priority')
2192
2193 @session_wrapper
2194 def get_priorities(session=None):
2195     """
2196     Returns dictionary of priority names -> id mappings
2197
2198     @type session: Session
2199     @param session: Optional SQL session object (a temporary one will be
2200     generated if not supplied)
2201
2202     @rtype: dictionary
2203     @return: dictionary of priority names -> id mappings
2204     """
2205
2206     ret = {}
2207     q = session.query(Priority)
2208     for x in q.all():
2209         ret[x.priority] = x.priority_id
2210
2211     return ret
2212
2213 __all__.append('get_priorities')
2214
2215 ################################################################################
2216
2217 class Section(ORMObject):
2218     def __init__(self, section = None):
2219         self.section = section
2220
2221     def properties(self):
2222         return ['section', 'section_id', 'overrides_count']
2223
2224     def not_null_constraints(self):
2225         return ['section']
2226
2227     def __eq__(self, val):
2228         if isinstance(val, str):
2229             return (self.section == val)
2230         # This signals to use the normal comparison operator
2231         return NotImplemented
2232
2233     def __ne__(self, val):
2234         if isinstance(val, str):
2235             return (self.section != val)
2236         # This signals to use the normal comparison operator
2237         return NotImplemented
2238
2239 __all__.append('Section')
2240
2241 @session_wrapper
2242 def get_section(section, session=None):
2243     """
2244     Returns Section object for given C{section name}.
2245
2246     @type section: string
2247     @param section: The name of the section
2248
2249     @type session: Session
2250     @param session: Optional SQLA session object (a temporary one will be
2251     generated if not supplied)
2252
2253     @rtype: Section
2254     @return: Section object for the given section name
2255     """
2256
2257     q = session.query(Section).filter_by(section=section)
2258
2259     try:
2260         return q.one()
2261     except NoResultFound:
2262         return None
2263
2264 __all__.append('get_section')
2265
2266 @session_wrapper
2267 def get_sections(session=None):
2268     """
2269     Returns dictionary of section names -> id mappings
2270
2271     @type session: Session
2272     @param session: Optional SQL session object (a temporary one will be
2273     generated if not supplied)
2274
2275     @rtype: dictionary
2276     @return: dictionary of section names -> id mappings
2277     """
2278
2279     ret = {}
2280     q = session.query(Section)
2281     for x in q.all():
2282         ret[x.section] = x.section_id
2283
2284     return ret
2285
2286 __all__.append('get_sections')
2287
2288 ################################################################################
2289
2290 class SrcContents(ORMObject):
2291     def __init__(self, file = None, source = None):
2292         self.file = file
2293         self.source = source
2294
2295     def properties(self):
2296         return ['file', 'source']
2297
2298 __all__.append('SrcContents')
2299
2300 ################################################################################
2301
2302 from debian.debfile import Deb822
2303
2304 # Temporary Deb822 subclass to fix bugs with : handling; see #597249
2305 class Dak822(Deb822):
2306     def _internal_parser(self, sequence, fields=None):
2307         # The key is non-whitespace, non-colon characters before any colon.
2308         key_part = r"^(?P<key>[^: \t\n\r\f\v]+)\s*:\s*"
2309         single = re.compile(key_part + r"(?P<data>\S.*?)\s*$")
2310         multi = re.compile(key_part + r"$")
2311         multidata = re.compile(r"^\s(?P<data>.+?)\s*$")
2312
2313         wanted_field = lambda f: fields is None or f in fields
2314
2315         if isinstance(sequence, basestring):
2316             sequence = sequence.splitlines()
2317
2318         curkey = None
2319         content = ""
2320         for line in self.gpg_stripped_paragraph(sequence):
2321             m = single.match(line)
2322             if m:
2323                 if curkey:
2324                     self[curkey] = content
2325
2326                 if not wanted_field(m.group('key')):
2327                     curkey = None
2328                     continue
2329
2330                 curkey = m.group('key')
2331                 content = m.group('data')
2332                 continue
2333
2334             m = multi.match(line)
2335             if m:
2336                 if curkey:
2337                     self[curkey] = content
2338
2339                 if not wanted_field(m.group('key')):
2340                     curkey = None
2341                     continue
2342
2343                 curkey = m.group('key')
2344                 content = ""
2345                 continue
2346
2347             m = multidata.match(line)
2348             if m:
2349                 content += '\n' + line # XXX not m.group('data')?
2350                 continue
2351
2352         if curkey:
2353             self[curkey] = content
2354
2355
2356 class DBSource(ORMObject):
2357     def __init__(self, source = None, version = None, maintainer = None, \
2358         changedby = None, poolfile = None, install_date = None):
2359         self.source = source
2360         self.version = version
2361         self.maintainer = maintainer
2362         self.changedby = changedby
2363         self.poolfile = poolfile
2364         self.install_date = install_date
2365
2366     @property
2367     def pkid(self):
2368         return self.source_id
2369
2370     def properties(self):
2371         return ['source', 'source_id', 'maintainer', 'changedby', \
2372             'fingerprint', 'poolfile', 'version', 'suites_count', \
2373             'install_date', 'binaries_count', 'uploaders_count']
2374
2375     def not_null_constraints(self):
2376         return ['source', 'version', 'install_date', 'maintainer', \
2377             'changedby', 'poolfile', 'install_date']
2378
2379     def read_control_fields(self):
2380         '''
2381         Reads the control information from a dsc
2382
2383         @rtype: tuple
2384         @return: fields is the dsc information in a dictionary form
2385         '''
2386         fullpath = self.poolfile.fullpath
2387         fields = Dak822(open(self.poolfile.fullpath, 'r'))
2388         return fields
2389
2390     metadata = association_proxy('key', 'value')
2391
2392     def scan_contents(self):
2393         '''
2394         Returns a set of names for non directories. The path names are
2395         normalized after converting them from either utf-8 or iso8859-1
2396         encoding.
2397         '''
2398         fullpath = self.poolfile.fullpath
2399         from daklib.contents import UnpackedSource
2400         unpacked = UnpackedSource(fullpath)
2401         fileset = set()
2402         for name in unpacked.get_all_filenames():
2403             # enforce proper utf-8 encoding
2404             try:
2405                 name.decode('utf-8')
2406             except UnicodeDecodeError:
2407                 name = name.decode('iso8859-1').encode('utf-8')
2408             fileset.add(name)
2409         return fileset
2410
2411 __all__.append('DBSource')
2412
2413 @session_wrapper
2414 def source_exists(source, source_version, suites = ["any"], session=None):
2415     """
2416     Ensure that source exists somewhere in the archive for the binary
2417     upload being processed.
2418       1. exact match     => 1.0-3
2419       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
2420
2421     @type source: string
2422     @param source: source name
2423
2424     @type source_version: string
2425     @param source_version: expected source version
2426
2427     @type suites: list
2428     @param suites: list of suites to check in, default I{any}
2429
2430     @type session: Session
2431     @param session: Optional SQLA session object (a temporary one will be
2432     generated if not supplied)
2433
2434     @rtype: int
2435     @return: returns 1 if a source with expected version is found, otherwise 0
2436
2437     """
2438
2439     cnf = Config()
2440     ret = True
2441
2442     from daklib.regexes import re_bin_only_nmu
2443     orig_source_version = re_bin_only_nmu.sub('', source_version)
2444
2445     for suite in suites:
2446         q = session.query(DBSource).filter_by(source=source). \
2447             filter(DBSource.version.in_([source_version, orig_source_version]))
2448         if suite != "any":
2449             # source must exist in suite X, or in some other suite that's
2450             # mapped to X, recursively... silent-maps are counted too,
2451             # unreleased-maps aren't.
2452             maps = cnf.ValueList("SuiteMappings")[:]
2453             maps.reverse()
2454             maps = [ m.split() for m in maps ]
2455             maps = [ (x[1], x[2]) for x in maps
2456                             if x[0] == "map" or x[0] == "silent-map" ]
2457             s = [suite]
2458             for (from_, to) in maps:
2459                 if from_ in s and to not in s:
2460                     s.append(to)
2461
2462             q = q.filter(DBSource.suites.any(Suite.suite_name.in_(s)))
2463
2464         if q.count() > 0:
2465             continue
2466
2467         # No source found so return not ok
2468         ret = False
2469
2470     return ret
2471
2472 __all__.append('source_exists')
2473
2474 @session_wrapper
2475 def get_suites_source_in(source, session=None):
2476     """
2477     Returns list of Suite objects which given C{source} name is in
2478
2479     @type source: str
2480     @param source: DBSource package name to search for
2481
2482     @rtype: list
2483     @return: list of Suite objects for the given source
2484     """
2485
2486     return session.query(Suite).filter(Suite.sources.any(source=source)).all()
2487
2488 __all__.append('get_suites_source_in')
2489
2490 @session_wrapper
2491 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
2492     """
2493     Returns list of DBSource objects for given C{source} name and other parameters
2494
2495     @type source: str
2496     @param source: DBSource package name to search for
2497
2498     @type version: str or None
2499     @param version: DBSource version name to search for or None if not applicable
2500
2501     @type dm_upload_allowed: bool
2502     @param dm_upload_allowed: If None, no effect.  If True or False, only
2503     return packages with that dm_upload_allowed setting
2504
2505     @type session: Session
2506     @param session: Optional SQL session object (a temporary one will be
2507     generated if not supplied)
2508
2509     @rtype: list
2510     @return: list of DBSource objects for the given name (may be empty)
2511     """
2512
2513     q = session.query(DBSource).filter_by(source=source)
2514
2515     if version is not None:
2516         q = q.filter_by(version=version)
2517
2518     if dm_upload_allowed is not None:
2519         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
2520
2521     return q.all()
2522
2523 __all__.append('get_sources_from_name')
2524
2525 # FIXME: This function fails badly if it finds more than 1 source package and
2526 # its implementation is trivial enough to be inlined.
2527 @session_wrapper
2528 def get_source_in_suite(source, suite, session=None):
2529     """
2530     Returns a DBSource object for a combination of C{source} and C{suite}.
2531
2532       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2533       - B{suite} - a suite name, eg. I{unstable}
2534
2535     @type source: string
2536     @param source: source package name
2537
2538     @type suite: string
2539     @param suite: the suite name
2540
2541     @rtype: string
2542     @return: the version for I{source} in I{suite}
2543
2544     """
2545
2546     q = get_suite(suite, session).get_sources(source)
2547     try:
2548         return q.one()
2549     except NoResultFound:
2550         return None
2551
2552 __all__.append('get_source_in_suite')
2553
2554 @session_wrapper
2555 def import_metadata_into_db(obj, session=None):
2556     """
2557     This routine works on either DBBinary or DBSource objects and imports
2558     their metadata into the database
2559     """
2560     fields = obj.read_control_fields()
2561     for k in fields.keys():
2562         try:
2563             # Try raw ASCII
2564             val = str(fields[k])
2565         except UnicodeEncodeError:
2566             # Fall back to UTF-8
2567             try:
2568                 val = fields[k].encode('utf-8')
2569             except UnicodeEncodeError:
2570                 # Finally try iso8859-1
2571                 val = fields[k].encode('iso8859-1')
2572                 # Otherwise we allow the exception to percolate up and we cause
2573                 # a reject as someone is playing silly buggers
2574
2575         obj.metadata[get_or_set_metadatakey(k, session)] = val
2576
2577     session.commit_or_flush()
2578
2579 __all__.append('import_metadata_into_db')
2580
2581
2582 ################################################################################
2583
2584 @session_wrapper
2585 def add_dsc_to_db(u, filename, session=None):
2586     entry = u.pkg.files[filename]
2587     source = DBSource()
2588     pfs = []
2589
2590     source.source = u.pkg.dsc["source"]
2591     source.version = u.pkg.dsc["version"] # NB: not files[file]["version"], that has no epoch
2592     source.maintainer_id = get_or_set_maintainer(u.pkg.dsc["maintainer"], session).maintainer_id
2593     source.changedby_id = get_or_set_maintainer(u.pkg.changes["changed-by"], session).maintainer_id
2594     source.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2595     source.install_date = datetime.now().date()
2596
2597     dsc_component = entry["component"]
2598     dsc_location_id = entry["location id"]
2599
2600     source.dm_upload_allowed = (u.pkg.dsc.get("dm-upload-allowed", '') == "yes")
2601
2602     # Set up a new poolfile if necessary
2603     if not entry.has_key("files id") or not entry["files id"]:
2604         filename = entry["pool name"] + filename
2605         poolfile = add_poolfile(filename, entry, dsc_location_id, session)
2606         session.flush()
2607         pfs.append(poolfile)
2608         entry["files id"] = poolfile.file_id
2609
2610     source.poolfile_id = entry["files id"]
2611     session.add(source)
2612
2613     suite_names = u.pkg.changes["distribution"].keys()
2614     source.suites = session.query(Suite). \
2615         filter(Suite.suite_name.in_(suite_names)).all()
2616
2617     # Add the source files to the DB (files and dsc_files)
2618     dscfile = DSCFile()
2619     dscfile.source_id = source.source_id
2620     dscfile.poolfile_id = entry["files id"]
2621     session.add(dscfile)
2622
2623     for dsc_file, dentry in u.pkg.dsc_files.items():
2624         df = DSCFile()
2625         df.source_id = source.source_id
2626
2627         # If the .orig tarball is already in the pool, it's
2628         # files id is stored in dsc_files by check_dsc().
2629         files_id = dentry.get("files id", None)
2630
2631         # Find the entry in the files hash
2632         # TODO: Bail out here properly
2633         dfentry = None
2634         for f, e in u.pkg.files.items():
2635             if f == dsc_file:
2636                 dfentry = e
2637                 break
2638
2639         if files_id is None:
2640             filename = dfentry["pool name"] + dsc_file
2641
2642             (found, obj) = check_poolfile(filename, dentry["size"], dentry["md5sum"], dsc_location_id)
2643             # FIXME: needs to check for -1/-2 and or handle exception
2644             if found and obj is not None:
2645                 files_id = obj.file_id
2646                 pfs.append(obj)
2647
2648             # If still not found, add it
2649             if files_id is None:
2650                 # HACK: Force sha1sum etc into dentry
2651                 dentry["sha1sum"] = dfentry["sha1sum"]
2652                 dentry["sha256sum"] = dfentry["sha256sum"]
2653                 poolfile = add_poolfile(filename, dentry, dsc_location_id, session)
2654                 pfs.append(poolfile)
2655                 files_id = poolfile.file_id
2656         else:
2657             poolfile = get_poolfile_by_id(files_id, session)
2658             if poolfile is None:
2659                 utils.fubar("INTERNAL ERROR. Found no poolfile with id %d" % files_id)
2660             pfs.append(poolfile)
2661
2662         df.poolfile_id = files_id
2663         session.add(df)
2664
2665     # Add the src_uploaders to the DB
2666     source.uploaders = [source.maintainer]
2667     if u.pkg.dsc.has_key("uploaders"):
2668         for up in u.pkg.dsc["uploaders"].replace(">, ", ">\t").split("\t"):
2669             up = up.strip()
2670             source.uploaders.append(get_or_set_maintainer(up, session))
2671
2672     session.flush()
2673
2674     return source, dsc_component, dsc_location_id, pfs
2675
2676 __all__.append('add_dsc_to_db')
2677
2678 @session_wrapper
2679 def add_deb_to_db(u, filename, session=None):
2680     """
2681     Contrary to what you might expect, this routine deals with both
2682     debs and udebs.  That info is in 'dbtype', whilst 'type' is
2683     'deb' for both of them
2684     """
2685     cnf = Config()
2686     entry = u.pkg.files[filename]
2687
2688     bin = DBBinary()
2689     bin.package = entry["package"]
2690     bin.version = entry["version"]
2691     bin.maintainer_id = get_or_set_maintainer(entry["maintainer"], session).maintainer_id
2692     bin.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2693     bin.arch_id = get_architecture(entry["architecture"], session).arch_id
2694     bin.binarytype = entry["dbtype"]
2695
2696     # Find poolfile id
2697     filename = entry["pool name"] + filename
2698     fullpath = os.path.join(cnf["Dir::Pool"], filename)
2699     if not entry.get("location id", None):
2700         entry["location id"] = get_location(cnf["Dir::Pool"], entry["component"], session=session).location_id
2701
2702     if entry.get("files id", None):
2703         poolfile = get_poolfile_by_id(bin.poolfile_id)
2704         bin.poolfile_id = entry["files id"]
2705     else:
2706         poolfile = add_poolfile(filename, entry, entry["location id"], session)
2707         bin.poolfile_id = entry["files id"] = poolfile.file_id
2708
2709     # Find source id
2710     bin_sources = get_sources_from_name(entry["source package"], entry["source version"], session=session)
2711     if len(bin_sources) != 1:
2712         raise NoSourceFieldError, "Unable to find a unique source id for %s (%s), %s, file %s, type %s, signed by %s" % \
2713                                   (bin.package, bin.version, entry["architecture"],
2714                                    filename, bin.binarytype, u.pkg.changes["fingerprint"])
2715
2716     bin.source_id = bin_sources[0].source_id
2717
2718     if entry.has_key("built-using"):
2719         for srcname, version in entry["built-using"]:
2720             exsources = get_sources_from_name(srcname, version, session=session)
2721             if len(exsources) != 1:
2722                 raise NoSourceFieldError, "Unable to find source package (%s = %s) in Built-Using for %s (%s), %s, file %s, type %s, signed by %s" % \
2723                                           (srcname, version, bin.package, bin.version, entry["architecture"],
2724                                            filename, bin.binarytype, u.pkg.changes["fingerprint"])
2725
2726             bin.extra_sources.append(exsources[0])
2727
2728     # Add and flush object so it has an ID
2729     session.add(bin)
2730
2731     suite_names = u.pkg.changes["distribution"].keys()
2732     bin.suites = session.query(Suite). \
2733         filter(Suite.suite_name.in_(suite_names)).all()
2734
2735     session.flush()
2736
2737     # Deal with contents - disabled for now
2738     #contents = copy_temporary_contents(bin.package, bin.version, bin.architecture.arch_string, os.path.basename(filename), None, session)
2739     #if not contents:
2740     #    print "REJECT\nCould not determine contents of package %s" % bin.package
2741     #    session.rollback()
2742     #    raise MissingContents, "No contents stored for package %s, and couldn't determine contents of %s" % (bin.package, filename)
2743
2744     return bin, poolfile
2745
2746 __all__.append('add_deb_to_db')
2747
2748 ################################################################################
2749
2750 class SourceACL(object):
2751     def __init__(self, *args, **kwargs):
2752         pass
2753
2754     def __repr__(self):
2755         return '<SourceACL %s>' % self.source_acl_id
2756
2757 __all__.append('SourceACL')
2758
2759 ################################################################################
2760
2761 class SrcFormat(object):
2762     def __init__(self, *args, **kwargs):
2763         pass
2764
2765     def __repr__(self):
2766         return '<SrcFormat %s>' % (self.format_name)
2767
2768 __all__.append('SrcFormat')
2769
2770 ################################################################################
2771
2772 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
2773                  ('SuiteID', 'suite_id'),
2774                  ('Version', 'version'),
2775                  ('Origin', 'origin'),
2776                  ('Label', 'label'),
2777                  ('Description', 'description'),
2778                  ('Untouchable', 'untouchable'),
2779                  ('Announce', 'announce'),
2780                  ('Codename', 'codename'),
2781                  ('OverrideCodename', 'overridecodename'),
2782                  ('ValidTime', 'validtime'),
2783                  ('Priority', 'priority'),
2784                  ('NotAutomatic', 'notautomatic'),
2785                  ('CopyChanges', 'copychanges'),
2786                  ('OverrideSuite', 'overridesuite')]
2787
2788 # Why the heck don't we have any UNIQUE constraints in table suite?
2789 # TODO: Add UNIQUE constraints for appropriate columns.
2790 class Suite(ORMObject):
2791     def __init__(self, suite_name = None, version = None):
2792         self.suite_name = suite_name
2793         self.version = version
2794
2795     def properties(self):
2796         return ['suite_name', 'version', 'sources_count', 'binaries_count', \
2797             'overrides_count']
2798
2799     def not_null_constraints(self):
2800         return ['suite_name']
2801
2802     def __eq__(self, val):
2803         if isinstance(val, str):
2804             return (self.suite_name == val)
2805         # This signals to use the normal comparison operator
2806         return NotImplemented
2807
2808     def __ne__(self, val):
2809         if isinstance(val, str):
2810             return (self.suite_name != val)
2811         # This signals to use the normal comparison operator
2812         return NotImplemented
2813
2814     def details(self):
2815         ret = []
2816         for disp, field in SUITE_FIELDS:
2817             val = getattr(self, field, None)
2818             if val is not None:
2819                 ret.append("%s: %s" % (disp, val))
2820
2821         return "\n".join(ret)
2822
2823     def get_architectures(self, skipsrc=False, skipall=False):
2824         """
2825         Returns list of Architecture objects
2826
2827         @type skipsrc: boolean
2828         @param skipsrc: Whether to skip returning the 'source' architecture entry
2829         (Default False)
2830
2831         @type skipall: boolean
2832         @param skipall: Whether to skip returning the 'all' architecture entry
2833         (Default False)
2834
2835         @rtype: list
2836         @return: list of Architecture objects for the given name (may be empty)
2837         """
2838
2839         q = object_session(self).query(Architecture).with_parent(self)
2840         if skipsrc:
2841             q = q.filter(Architecture.arch_string != 'source')
2842         if skipall:
2843             q = q.filter(Architecture.arch_string != 'all')
2844         return q.order_by(Architecture.arch_string).all()
2845
2846     def get_sources(self, source):
2847         """
2848         Returns a query object representing DBSource that is part of C{suite}.
2849
2850           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2851
2852         @type source: string
2853         @param source: source package name
2854
2855         @rtype: sqlalchemy.orm.query.Query
2856         @return: a query of DBSource
2857
2858         """
2859
2860         session = object_session(self)
2861         return session.query(DBSource).filter_by(source = source). \
2862             with_parent(self)
2863
2864 __all__.append('Suite')
2865
2866 @session_wrapper
2867 def get_suite(suite, session=None):
2868     """
2869     Returns Suite object for given C{suite name}.
2870
2871     @type suite: string
2872     @param suite: The name of the suite
2873
2874     @type session: Session
2875     @param session: Optional SQLA session object (a temporary one will be
2876     generated if not supplied)
2877
2878     @rtype: Suite
2879     @return: Suite object for the requested suite name (None if not present)
2880     """
2881
2882     q = session.query(Suite).filter_by(suite_name=suite)
2883
2884     try:
2885         return q.one()
2886     except NoResultFound:
2887         return None
2888
2889 __all__.append('get_suite')
2890
2891 ################################################################################
2892
2893 # TODO: should be removed because the implementation is too trivial
2894 @session_wrapper
2895 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
2896     """
2897     Returns list of Architecture objects for given C{suite} name
2898
2899     @type suite: str
2900     @param suite: Suite name to search for
2901
2902     @type skipsrc: boolean
2903     @param skipsrc: Whether to skip returning the 'source' architecture entry
2904     (Default False)
2905
2906     @type skipall: boolean
2907     @param skipall: Whether to skip returning the 'all' architecture entry
2908     (Default False)
2909
2910     @type session: Session
2911     @param session: Optional SQL session object (a temporary one will be
2912     generated if not supplied)
2913
2914     @rtype: list
2915     @return: list of Architecture objects for the given name (may be empty)
2916     """
2917
2918     return get_suite(suite, session).get_architectures(skipsrc, skipall)
2919
2920 __all__.append('get_suite_architectures')
2921
2922 ################################################################################
2923
2924 class SuiteSrcFormat(object):
2925     def __init__(self, *args, **kwargs):
2926         pass
2927
2928     def __repr__(self):
2929         return '<SuiteSrcFormat (%s, %s)>' % (self.suite_id, self.src_format_id)
2930
2931 __all__.append('SuiteSrcFormat')
2932
2933 @session_wrapper
2934 def get_suite_src_formats(suite, session=None):
2935     """
2936     Returns list of allowed SrcFormat for C{suite}.
2937
2938     @type suite: str
2939     @param suite: Suite name to search for
2940
2941     @type session: Session
2942     @param session: Optional SQL session object (a temporary one will be
2943     generated if not supplied)
2944
2945     @rtype: list
2946     @return: the list of allowed source formats for I{suite}
2947     """
2948
2949     q = session.query(SrcFormat)
2950     q = q.join(SuiteSrcFormat)
2951     q = q.join(Suite).filter_by(suite_name=suite)
2952     q = q.order_by('format_name')
2953
2954     return q.all()
2955
2956 __all__.append('get_suite_src_formats')
2957
2958 ################################################################################
2959
2960 class Uid(ORMObject):
2961     def __init__(self, uid = None, name = None):
2962         self.uid = uid
2963         self.name = name
2964
2965     def __eq__(self, val):
2966         if isinstance(val, str):
2967             return (self.uid == val)
2968         # This signals to use the normal comparison operator
2969         return NotImplemented
2970
2971     def __ne__(self, val):
2972         if isinstance(val, str):
2973             return (self.uid != val)
2974         # This signals to use the normal comparison operator
2975         return NotImplemented
2976
2977     def properties(self):
2978         return ['uid', 'name', 'fingerprint']
2979
2980     def not_null_constraints(self):
2981         return ['uid']
2982
2983 __all__.append('Uid')
2984
2985 @session_wrapper
2986 def get_or_set_uid(uidname, session=None):
2987     """
2988     Returns uid object for given uidname.
2989
2990     If no matching uidname is found, a row is inserted.
2991
2992     @type uidname: string
2993     @param uidname: The uid to add
2994
2995     @type session: SQLAlchemy
2996     @param session: Optional SQL session object (a temporary one will be
2997     generated if not supplied).  If not passed, a commit will be performed at
2998     the end of the function, otherwise the caller is responsible for commiting.
2999
3000     @rtype: Uid
3001     @return: the uid object for the given uidname
3002     """
3003
3004     q = session.query(Uid).filter_by(uid=uidname)
3005
3006     try:
3007         ret = q.one()
3008     except NoResultFound:
3009         uid = Uid()
3010         uid.uid = uidname
3011         session.add(uid)
3012         session.commit_or_flush()
3013         ret = uid
3014
3015     return ret
3016
3017 __all__.append('get_or_set_uid')
3018
3019 @session_wrapper
3020 def get_uid_from_fingerprint(fpr, session=None):
3021     q = session.query(Uid)
3022     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
3023
3024     try:
3025         return q.one()
3026     except NoResultFound:
3027         return None
3028
3029 __all__.append('get_uid_from_fingerprint')
3030
3031 ################################################################################
3032
3033 class UploadBlock(object):
3034     def __init__(self, *args, **kwargs):
3035         pass
3036
3037     def __repr__(self):
3038         return '<UploadBlock %s (%s)>' % (self.source, self.upload_block_id)
3039
3040 __all__.append('UploadBlock')
3041
3042 ################################################################################
3043
3044 class MetadataKey(ORMObject):
3045     def __init__(self, key = None):
3046         self.key = key
3047
3048     def properties(self):
3049         return ['key']
3050
3051     def not_null_constraints(self):
3052         return ['key']
3053
3054 __all__.append('MetadataKey')
3055
3056 @session_wrapper
3057 def get_or_set_metadatakey(keyname, session=None):
3058     """
3059     Returns MetadataKey object for given uidname.
3060
3061     If no matching keyname is found, a row is inserted.
3062
3063     @type uidname: string
3064     @param uidname: The keyname to add
3065
3066     @type session: SQLAlchemy
3067     @param session: Optional SQL session object (a temporary one will be
3068     generated if not supplied).  If not passed, a commit will be performed at
3069     the end of the function, otherwise the caller is responsible for commiting.
3070
3071     @rtype: MetadataKey
3072     @return: the metadatakey object for the given keyname
3073     """
3074
3075     q = session.query(MetadataKey).filter_by(key=keyname)
3076
3077     try:
3078         ret = q.one()
3079     except NoResultFound:
3080         ret = MetadataKey(keyname)
3081         session.add(ret)
3082         session.commit_or_flush()
3083
3084     return ret
3085
3086 __all__.append('get_or_set_metadatakey')
3087
3088 ################################################################################
3089
3090 class BinaryMetadata(ORMObject):
3091     def __init__(self, key = None, value = None, binary = None):
3092         self.key = key
3093         self.value = value
3094         self.binary = binary
3095
3096     def properties(self):
3097         return ['binary', 'key', 'value']
3098
3099     def not_null_constraints(self):
3100         return ['value']
3101
3102 __all__.append('BinaryMetadata')
3103
3104 ################################################################################
3105
3106 class SourceMetadata(ORMObject):
3107     def __init__(self, key = None, value = None, source = None):
3108         self.key = key
3109         self.value = value
3110         self.source = source
3111
3112     def properties(self):
3113         return ['source', 'key', 'value']
3114
3115     def not_null_constraints(self):
3116         return ['value']
3117
3118 __all__.append('SourceMetadata')
3119
3120 ################################################################################
3121
3122 class VersionCheck(ORMObject):
3123     def __init__(self, *args, **kwargs):
3124         pass
3125
3126     def properties(self):
3127         #return ['suite_id', 'check', 'reference_id']
3128         return ['check']
3129
3130     def not_null_constraints(self):
3131         return ['suite', 'check', 'reference']
3132
3133 __all__.append('VersionCheck')
3134
3135 @session_wrapper
3136 def get_version_checks(suite_name, check = None, session = None):
3137     suite = get_suite(suite_name, session)
3138     if not suite:
3139         return None
3140     q = session.query(VersionCheck).filter_by(suite=suite)
3141     if check:
3142         q = q.filter_by(check=check)
3143     return q.all()
3144
3145 __all__.append('get_version_checks')
3146
3147 ################################################################################
3148
3149 class DBConn(object):
3150     """
3151     database module init.
3152     """
3153     __shared_state = {}
3154
3155     def __init__(self, *args, **kwargs):
3156         self.__dict__ = self.__shared_state
3157
3158         if not getattr(self, 'initialised', False):
3159             self.initialised = True
3160             self.debug = kwargs.has_key('debug')
3161             self.__createconn()
3162
3163     def __setuptables(self):
3164         tables = (
3165             'architecture',
3166             'archive',
3167             'bin_associations',
3168             'bin_contents',
3169             'binaries',
3170             'binaries_metadata',
3171             'binary_acl',
3172             'binary_acl_map',
3173             'build_queue',
3174             'build_queue_files',
3175             'build_queue_policy_files',
3176             'changelogs_text',
3177             'changes',
3178             'component',
3179             'config',
3180             'changes_pending_binaries',
3181             'changes_pending_files',
3182             'changes_pending_source',
3183             'changes_pending_files_map',
3184             'changes_pending_source_files',
3185             'changes_pool_files',
3186             'dsc_files',
3187             'extra_src_references',
3188             'files',
3189             'fingerprint',
3190             'keyrings',
3191             'keyring_acl_map',
3192             'location',
3193             'maintainer',
3194             'metadata_keys',
3195             'new_comments',
3196             # TODO: the maintainer column in table override should be removed.
3197             'override',
3198             'override_type',
3199             'policy_queue',
3200             'priority',
3201             'section',
3202             'source',
3203             'source_acl',
3204             'source_metadata',
3205             'src_associations',
3206             'src_contents',
3207             'src_format',
3208             'src_uploaders',
3209             'suite',
3210             'suite_architectures',
3211             'suite_build_queue_copy',
3212             'suite_src_formats',
3213             'uid',
3214             'upload_blocks',
3215             'version_check',
3216         )
3217
3218         views = (
3219             'almost_obsolete_all_associations',
3220             'almost_obsolete_src_associations',
3221             'any_associations_source',
3222             'bin_assoc_by_arch',
3223             'bin_associations_binaries',
3224             'binaries_suite_arch',
3225             'binfiles_suite_component_arch',
3226             'changelogs',
3227             'file_arch_suite',
3228             'newest_all_associations',
3229             'newest_any_associations',
3230             'newest_source',
3231             'newest_src_association',
3232             'obsolete_all_associations',
3233             'obsolete_any_associations',
3234             'obsolete_any_by_all_associations',
3235             'obsolete_src_associations',
3236             'source_suite',
3237             'src_associations_bin',
3238             'src_associations_src',
3239             'suite_arch_by_name',
3240         )
3241
3242         for table_name in tables:
3243             table = Table(table_name, self.db_meta, \
3244                 autoload=True, useexisting=True)
3245             setattr(self, 'tbl_%s' % table_name, table)
3246
3247         for view_name in views:
3248             view = Table(view_name, self.db_meta, autoload=True)
3249             setattr(self, 'view_%s' % view_name, view)
3250
3251     def __setupmappers(self):
3252         mapper(Architecture, self.tbl_architecture,
3253             properties = dict(arch_id = self.tbl_architecture.c.id,
3254                suites = relation(Suite, secondary=self.tbl_suite_architectures,
3255                    order_by='suite_name',
3256                    backref=backref('architectures', order_by='arch_string'))),
3257             extension = validator)
3258
3259         mapper(Archive, self.tbl_archive,
3260                properties = dict(archive_id = self.tbl_archive.c.id,
3261                                  archive_name = self.tbl_archive.c.name))
3262
3263         mapper(BuildQueue, self.tbl_build_queue,
3264                properties = dict(queue_id = self.tbl_build_queue.c.id))
3265
3266         mapper(BuildQueueFile, self.tbl_build_queue_files,
3267                properties = dict(buildqueue = relation(BuildQueue, backref='queuefiles'),
3268                                  poolfile = relation(PoolFile, backref='buildqueueinstances')))
3269
3270         mapper(BuildQueuePolicyFile, self.tbl_build_queue_policy_files,
3271                properties = dict(
3272                 build_queue = relation(BuildQueue, backref='policy_queue_files'),
3273                 file = relation(ChangePendingFile, lazy='joined')))
3274
3275         mapper(DBBinary, self.tbl_binaries,
3276                properties = dict(binary_id = self.tbl_binaries.c.id,
3277                                  package = self.tbl_binaries.c.package,
3278                                  version = self.tbl_binaries.c.version,
3279                                  maintainer_id = self.tbl_binaries.c.maintainer,
3280                                  maintainer = relation(Maintainer),
3281                                  source_id = self.tbl_binaries.c.source,
3282                                  source = relation(DBSource, backref='binaries'),
3283                                  arch_id = self.tbl_binaries.c.architecture,
3284                                  architecture = relation(Architecture),
3285                                  poolfile_id = self.tbl_binaries.c.file,
3286                                  poolfile = relation(PoolFile, backref=backref('binary', uselist = False)),
3287                                  binarytype = self.tbl_binaries.c.type,
3288                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
3289                                  fingerprint = relation(Fingerprint),
3290                                  install_date = self.tbl_binaries.c.install_date,
3291                                  suites = relation(Suite, secondary=self.tbl_bin_associations,
3292                                      backref=backref('binaries', lazy='dynamic')),
3293                                  extra_sources = relation(DBSource, secondary=self.tbl_extra_src_references,
3294                                      backref=backref('extra_binary_references', lazy='dynamic')),
3295                                  key = relation(BinaryMetadata, cascade='all',
3296                                      collection_class=attribute_mapped_collection('key'))),
3297                 extension = validator)
3298
3299         mapper(BinaryACL, self.tbl_binary_acl,
3300                properties = dict(binary_acl_id = self.tbl_binary_acl.c.id))
3301
3302         mapper(BinaryACLMap, self.tbl_binary_acl_map,
3303                properties = dict(binary_acl_map_id = self.tbl_binary_acl_map.c.id,
3304                                  fingerprint = relation(Fingerprint, backref="binary_acl_map"),
3305                                  architecture = relation(Architecture)))
3306
3307         mapper(Component, self.tbl_component,
3308                properties = dict(component_id = self.tbl_component.c.id,
3309                                  component_name = self.tbl_component.c.name),
3310                extension = validator)
3311
3312         mapper(DBConfig, self.tbl_config,
3313                properties = dict(config_id = self.tbl_config.c.id))
3314
3315         mapper(DSCFile, self.tbl_dsc_files,
3316                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
3317                                  source_id = self.tbl_dsc_files.c.source,
3318                                  source = relation(DBSource),
3319                                  poolfile_id = self.tbl_dsc_files.c.file,
3320                                  poolfile = relation(PoolFile)))
3321
3322         mapper(PoolFile, self.tbl_files,
3323                properties = dict(file_id = self.tbl_files.c.id,
3324                                  filesize = self.tbl_files.c.size,
3325                                  location_id = self.tbl_files.c.location,
3326                                  location = relation(Location,
3327                                      # using lazy='dynamic' in the back
3328                                      # reference because we have A LOT of
3329                                      # files in one location
3330                                      backref=backref('files', lazy='dynamic'))),
3331                 extension = validator)
3332
3333         mapper(Fingerprint, self.tbl_fingerprint,
3334                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
3335                                  uid_id = self.tbl_fingerprint.c.uid,
3336                                  uid = relation(Uid),
3337                                  keyring_id = self.tbl_fingerprint.c.keyring,
3338                                  keyring = relation(Keyring),
3339                                  source_acl = relation(SourceACL),
3340                                  binary_acl = relation(BinaryACL)),
3341                extension = validator)
3342
3343         mapper(Keyring, self.tbl_keyrings,
3344                properties = dict(keyring_name = self.tbl_keyrings.c.name,
3345                                  keyring_id = self.tbl_keyrings.c.id))
3346
3347         mapper(DBChange, self.tbl_changes,
3348                properties = dict(change_id = self.tbl_changes.c.id,
3349                                  poolfiles = relation(PoolFile,
3350                                                       secondary=self.tbl_changes_pool_files,
3351                                                       backref="changeslinks"),
3352                                  seen = self.tbl_changes.c.seen,
3353                                  source = self.tbl_changes.c.source,
3354                                  binaries = self.tbl_changes.c.binaries,
3355                                  architecture = self.tbl_changes.c.architecture,
3356                                  distribution = self.tbl_changes.c.distribution,
3357                                  urgency = self.tbl_changes.c.urgency,
3358                                  maintainer = self.tbl_changes.c.maintainer,
3359                                  changedby = self.tbl_changes.c.changedby,
3360                                  date = self.tbl_changes.c.date,
3361                                  version = self.tbl_changes.c.version,
3362                                  files = relation(ChangePendingFile,
3363                                                   secondary=self.tbl_changes_pending_files_map,
3364                                                   backref="changesfile"),
3365                                  in_queue_id = self.tbl_changes.c.in_queue,
3366                                  in_queue = relation(PolicyQueue,
3367                                                      primaryjoin=(self.tbl_changes.c.in_queue==self.tbl_policy_queue.c.id)),
3368                                  approved_for_id = self.tbl_changes.c.approved_for))
3369
3370         mapper(ChangePendingBinary, self.tbl_changes_pending_binaries,
3371                properties = dict(change_pending_binary_id = self.tbl_changes_pending_binaries.c.id))
3372
3373         mapper(ChangePendingFile, self.tbl_changes_pending_files,
3374                properties = dict(change_pending_file_id = self.tbl_changes_pending_files.c.id,
3375                                  filename = self.tbl_changes_pending_files.c.filename,
3376                                  size = self.tbl_changes_pending_files.c.size,
3377                                  md5sum = self.tbl_changes_pending_files.c.md5sum,
3378                                  sha1sum = self.tbl_changes_pending_files.c.sha1sum,
3379                                  sha256sum = self.tbl_changes_pending_files.c.sha256sum))
3380
3381         mapper(ChangePendingSource, self.tbl_changes_pending_source,
3382                properties = dict(change_pending_source_id = self.tbl_changes_pending_source.c.id,
3383                                  change = relation(DBChange),
3384                                  maintainer = relation(Maintainer,
3385                                                        primaryjoin=(self.tbl_changes_pending_source.c.maintainer_id==self.tbl_maintainer.c.id)),
3386                                  changedby = relation(Maintainer,
3387                                                       primaryjoin=(self.tbl_changes_pending_source.c.changedby_id==self.tbl_maintainer.c.id)),
3388                                  fingerprint = relation(Fingerprint),
3389                                  source_files = relation(ChangePendingFile,
3390                                                          secondary=self.tbl_changes_pending_source_files,
3391                                                          backref="pending_sources")))
3392
3393
3394         mapper(KeyringACLMap, self.tbl_keyring_acl_map,
3395                properties = dict(keyring_acl_map_id = self.tbl_keyring_acl_map.c.id,
3396                                  keyring = relation(Keyring, backref="keyring_acl_map"),
3397                                  architecture = relation(Architecture)))
3398
3399         mapper(Location, self.tbl_location,
3400                properties = dict(location_id = self.tbl_location.c.id,
3401                                  component_id = self.tbl_location.c.component,
3402                                  component = relation(Component, backref='location'),
3403                                  archive_id = self.tbl_location.c.archive,
3404                                  archive = relation(Archive),
3405                                  # FIXME: the 'type' column is old cruft and
3406                                  # should be removed in the future.
3407                                  archive_type = self.tbl_location.c.type),
3408                extension = validator)
3409
3410         mapper(Maintainer, self.tbl_maintainer,
3411                properties = dict(maintainer_id = self.tbl_maintainer.c.id,
3412                    maintains_sources = relation(DBSource, backref='maintainer',
3413                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.maintainer)),
3414                    changed_sources = relation(DBSource, backref='changedby',
3415                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.changedby))),
3416                 extension = validator)
3417
3418         mapper(NewComment, self.tbl_new_comments,
3419                properties = dict(comment_id = self.tbl_new_comments.c.id))
3420
3421         mapper(Override, self.tbl_override,
3422                properties = dict(suite_id = self.tbl_override.c.suite,
3423                                  suite = relation(Suite, \
3424                                     backref=backref('overrides', lazy='dynamic')),
3425                                  package = self.tbl_override.c.package,
3426                                  component_id = self.tbl_override.c.component,
3427                                  component = relation(Component, \
3428                                     backref=backref('overrides', lazy='dynamic')),
3429                                  priority_id = self.tbl_override.c.priority,
3430                                  priority = relation(Priority, \
3431                                     backref=backref('overrides', lazy='dynamic')),
3432                                  section_id = self.tbl_override.c.section,
3433                                  section = relation(Section, \
3434                                     backref=backref('overrides', lazy='dynamic')),
3435                                  overridetype_id = self.tbl_override.c.type,
3436                                  overridetype = relation(OverrideType, \
3437                                     backref=backref('overrides', lazy='dynamic'))))
3438
3439         mapper(OverrideType, self.tbl_override_type,
3440                properties = dict(overridetype = self.tbl_override_type.c.type,
3441                                  overridetype_id = self.tbl_override_type.c.id))
3442
3443         mapper(PolicyQueue, self.tbl_policy_queue,
3444                properties = dict(policy_queue_id = self.tbl_policy_queue.c.id))
3445
3446         mapper(Priority, self.tbl_priority,
3447                properties = dict(priority_id = self.tbl_priority.c.id))
3448
3449         mapper(Section, self.tbl_section,
3450                properties = dict(section_id = self.tbl_section.c.id,
3451                                  section=self.tbl_section.c.section))
3452
3453         mapper(DBSource, self.tbl_source,
3454                properties = dict(source_id = self.tbl_source.c.id,
3455                                  version = self.tbl_source.c.version,
3456                                  maintainer_id = self.tbl_source.c.maintainer,
3457                                  poolfile_id = self.tbl_source.c.file,
3458                                  poolfile = relation(PoolFile, backref=backref('source', uselist = False)),
3459                                  fingerprint_id = self.tbl_source.c.sig_fpr,
3460                                  fingerprint = relation(Fingerprint),
3461                                  changedby_id = self.tbl_source.c.changedby,
3462                                  srcfiles = relation(DSCFile,
3463                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
3464                                  suites = relation(Suite, secondary=self.tbl_src_associations,
3465                                      backref=backref('sources', lazy='dynamic')),
3466                                  uploaders = relation(Maintainer,
3467                                      secondary=self.tbl_src_uploaders),
3468                                  key = relation(SourceMetadata, cascade='all',
3469                                      collection_class=attribute_mapped_collection('key'))),
3470                extension = validator)
3471
3472         mapper(SourceACL, self.tbl_source_acl,
3473                properties = dict(source_acl_id = self.tbl_source_acl.c.id))
3474
3475         mapper(SrcFormat, self.tbl_src_format,
3476                properties = dict(src_format_id = self.tbl_src_format.c.id,
3477                                  format_name = self.tbl_src_format.c.format_name))
3478
3479         mapper(Suite, self.tbl_suite,
3480                properties = dict(suite_id = self.tbl_suite.c.id,
3481                                  policy_queue = relation(PolicyQueue),
3482                                  copy_queues = relation(BuildQueue,
3483                                      secondary=self.tbl_suite_build_queue_copy)),
3484                 extension = validator)
3485
3486         mapper(SuiteSrcFormat, self.tbl_suite_src_formats,
3487                properties = dict(suite_id = self.tbl_suite_src_formats.c.suite,
3488                                  suite = relation(Suite, backref='suitesrcformats'),
3489                                  src_format_id = self.tbl_suite_src_formats.c.src_format,
3490                                  src_format = relation(SrcFormat)))
3491
3492         mapper(Uid, self.tbl_uid,
3493                properties = dict(uid_id = self.tbl_uid.c.id,
3494                                  fingerprint = relation(Fingerprint)),
3495                extension = validator)
3496
3497         mapper(UploadBlock, self.tbl_upload_blocks,
3498                properties = dict(upload_block_id = self.tbl_upload_blocks.c.id,
3499                                  fingerprint = relation(Fingerprint, backref="uploadblocks"),
3500                                  uid = relation(Uid, backref="uploadblocks")))
3501
3502         mapper(BinContents, self.tbl_bin_contents,
3503             properties = dict(
3504                 binary = relation(DBBinary,
3505                     backref=backref('contents', lazy='dynamic', cascade='all')),
3506                 file = self.tbl_bin_contents.c.file))
3507
3508         mapper(SrcContents, self.tbl_src_contents,
3509             properties = dict(
3510                 source = relation(DBSource,
3511                     backref=backref('contents', lazy='dynamic', cascade='all')),
3512                 file = self.tbl_src_contents.c.file))
3513
3514         mapper(MetadataKey, self.tbl_metadata_keys,
3515             properties = dict(
3516                 key_id = self.tbl_metadata_keys.c.key_id,
3517                 key = self.tbl_metadata_keys.c.key))
3518
3519         mapper(BinaryMetadata, self.tbl_binaries_metadata,
3520             properties = dict(
3521                 binary_id = self.tbl_binaries_metadata.c.bin_id,
3522                 binary = relation(DBBinary),
3523                 key_id = self.tbl_binaries_metadata.c.key_id,
3524                 key = relation(MetadataKey),
3525                 value = self.tbl_binaries_metadata.c.value))
3526
3527         mapper(SourceMetadata, self.tbl_source_metadata,
3528             properties = dict(
3529                 source_id = self.tbl_source_metadata.c.src_id,
3530                 source = relation(DBSource),
3531                 key_id = self.tbl_source_metadata.c.key_id,
3532                 key = relation(MetadataKey),
3533                 value = self.tbl_source_metadata.c.value))
3534
3535         mapper(VersionCheck, self.tbl_version_check,
3536             properties = dict(
3537                 suite_id = self.tbl_version_check.c.suite,
3538                 suite = relation(Suite, primaryjoin=self.tbl_version_check.c.suite==self.tbl_suite.c.id),
3539                 reference_id = self.tbl_version_check.c.reference,
3540                 reference = relation(Suite, primaryjoin=self.tbl_version_check.c.reference==self.tbl_suite.c.id, lazy='joined')))
3541
3542     ## Connection functions
3543     def __createconn(self):
3544         from config import Config
3545         cnf = Config()
3546         if cnf.has_key("DB::Service"):
3547             connstr = "postgresql://service=%s" % cnf["DB::Service"]
3548         elif cnf.has_key("DB::Host"):
3549             # TCP/IP
3550             connstr = "postgresql://%s" % cnf["DB::Host"]
3551             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
3552                 connstr += ":%s" % cnf["DB::Port"]
3553             connstr += "/%s" % cnf["DB::Name"]
3554         else:
3555             # Unix Socket
3556             connstr = "postgresql:///%s" % cnf["DB::Name"]
3557             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
3558                 connstr += "?port=%s" % cnf["DB::Port"]
3559
3560         engine_args = { 'echo': self.debug }
3561         if cnf.has_key('DB::PoolSize'):
3562             engine_args['pool_size'] = int(cnf['DB::PoolSize'])
3563         if cnf.has_key('DB::MaxOverflow'):
3564             engine_args['max_overflow'] = int(cnf['DB::MaxOverflow'])
3565         if sa_major_version == '0.6' and cnf.has_key('DB::Unicode') and \
3566             cnf['DB::Unicode'] == 'false':
3567             engine_args['use_native_unicode'] = False
3568
3569         # Monkey patch a new dialect in in order to support service= syntax
3570         import sqlalchemy.dialects.postgresql
3571         from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
3572         class PGDialect_psycopg2_dak(PGDialect_psycopg2):
3573             def create_connect_args(self, url):
3574                 if str(url).startswith('postgresql://service='):
3575                     # Eww
3576                     servicename = str(url)[21:]
3577                     return (['service=%s' % servicename], {})
3578                 else:
3579                     return PGDialect_psycopg2.create_connect_args(self, url)
3580
3581         sqlalchemy.dialects.postgresql.base.dialect = PGDialect_psycopg2_dak
3582
3583         self.db_pg   = create_engine(connstr, **engine_args)
3584         self.db_meta = MetaData()
3585         self.db_meta.bind = self.db_pg
3586         self.db_smaker = sessionmaker(bind=self.db_pg,
3587                                       autoflush=True,
3588                                       autocommit=False)
3589
3590         self.__setuptables()
3591         self.__setupmappers()
3592         self.pid = os.getpid()
3593
3594     def session(self):
3595         # reinitialize DBConn in new processes
3596         if self.pid != os.getpid():
3597             clear_mappers()
3598             self.__createconn()
3599         return self.db_smaker()
3600
3601 __all__.append('DBConn')
3602
3603