]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
482ec471bf64db2391ed833266fd766192d5aa01
[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 is not None and 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 ExternalOverride(ORMObject):
1356     def __init__(self, *args, **kwargs):
1357         pass
1358
1359     def __repr__(self):
1360         return '<ExternalOverride %s = %s: %s>' % (self.package, self.key, self.value)
1361
1362 __all__.append('ExternalOverride')
1363
1364 ################################################################################
1365
1366 class PoolFile(ORMObject):
1367     def __init__(self, filename = None, location = None, filesize = -1, \
1368         md5sum = None):
1369         self.filename = filename
1370         self.location = location
1371         self.filesize = filesize
1372         self.md5sum = md5sum
1373
1374     @property
1375     def fullpath(self):
1376         return os.path.join(self.location.path, self.filename)
1377
1378     def is_valid(self, filesize = -1, md5sum = None):
1379         return self.filesize == long(filesize) and self.md5sum == md5sum
1380
1381     def properties(self):
1382         return ['filename', 'file_id', 'filesize', 'md5sum', 'sha1sum', \
1383             'sha256sum', 'location', 'source', 'binary', 'last_used']
1384
1385     def not_null_constraints(self):
1386         return ['filename', 'md5sum', 'location']
1387
1388 __all__.append('PoolFile')
1389
1390 @session_wrapper
1391 def check_poolfile(filename, filesize, md5sum, location_id, session=None):
1392     """
1393     Returns a tuple:
1394     (ValidFileFound [boolean], PoolFile object or None)
1395
1396     @type filename: string
1397     @param filename: the filename of the file to check against the DB
1398
1399     @type filesize: int
1400     @param filesize: the size of the file to check against the DB
1401
1402     @type md5sum: string
1403     @param md5sum: the md5sum of the file to check against the DB
1404
1405     @type location_id: int
1406     @param location_id: the id of the location to look in
1407
1408     @rtype: tuple
1409     @return: Tuple of length 2.
1410                  - If valid pool file found: (C{True}, C{PoolFile object})
1411                  - If valid pool file not found:
1412                      - (C{False}, C{None}) if no file found
1413                      - (C{False}, C{PoolFile object}) if file found with size/md5sum mismatch
1414     """
1415
1416     poolfile = session.query(Location).get(location_id). \
1417         files.filter_by(filename=filename).first()
1418     valid = False
1419     if poolfile and poolfile.is_valid(filesize = filesize, md5sum = md5sum):
1420         valid = True
1421
1422     return (valid, poolfile)
1423
1424 __all__.append('check_poolfile')
1425
1426 # TODO: the implementation can trivially be inlined at the place where the
1427 # function is called
1428 @session_wrapper
1429 def get_poolfile_by_id(file_id, session=None):
1430     """
1431     Returns a PoolFile objects or None for the given id
1432
1433     @type file_id: int
1434     @param file_id: the id of the file to look for
1435
1436     @rtype: PoolFile or None
1437     @return: either the PoolFile object or None
1438     """
1439
1440     return session.query(PoolFile).get(file_id)
1441
1442 __all__.append('get_poolfile_by_id')
1443
1444 @session_wrapper
1445 def get_poolfile_like_name(filename, session=None):
1446     """
1447     Returns an array of PoolFile objects which are like the given name
1448
1449     @type filename: string
1450     @param filename: the filename of the file to check against the DB
1451
1452     @rtype: array
1453     @return: array of PoolFile objects
1454     """
1455
1456     # TODO: There must be a way of properly using bind parameters with %FOO%
1457     q = session.query(PoolFile).filter(PoolFile.filename.like('%%/%s' % filename))
1458
1459     return q.all()
1460
1461 __all__.append('get_poolfile_like_name')
1462
1463 @session_wrapper
1464 def add_poolfile(filename, datadict, location_id, session=None):
1465     """
1466     Add a new file to the pool
1467
1468     @type filename: string
1469     @param filename: filename
1470
1471     @type datadict: dict
1472     @param datadict: dict with needed data
1473
1474     @type location_id: int
1475     @param location_id: database id of the location
1476
1477     @rtype: PoolFile
1478     @return: the PoolFile object created
1479     """
1480     poolfile = PoolFile()
1481     poolfile.filename = filename
1482     poolfile.filesize = datadict["size"]
1483     poolfile.md5sum = datadict["md5sum"]
1484     poolfile.sha1sum = datadict["sha1sum"]
1485     poolfile.sha256sum = datadict["sha256sum"]
1486     poolfile.location_id = location_id
1487
1488     session.add(poolfile)
1489     # Flush to get a file id (NB: This is not a commit)
1490     session.flush()
1491
1492     return poolfile
1493
1494 __all__.append('add_poolfile')
1495
1496 ################################################################################
1497
1498 class Fingerprint(ORMObject):
1499     def __init__(self, fingerprint = None):
1500         self.fingerprint = fingerprint
1501
1502     def properties(self):
1503         return ['fingerprint', 'fingerprint_id', 'keyring', 'uid', \
1504             'binary_reject']
1505
1506     def not_null_constraints(self):
1507         return ['fingerprint']
1508
1509 __all__.append('Fingerprint')
1510
1511 @session_wrapper
1512 def get_fingerprint(fpr, session=None):
1513     """
1514     Returns Fingerprint object for given fpr.
1515
1516     @type fpr: string
1517     @param fpr: The fpr to find / add
1518
1519     @type session: SQLAlchemy
1520     @param session: Optional SQL session object (a temporary one will be
1521     generated if not supplied).
1522
1523     @rtype: Fingerprint
1524     @return: the Fingerprint object for the given fpr or None
1525     """
1526
1527     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1528
1529     try:
1530         ret = q.one()
1531     except NoResultFound:
1532         ret = None
1533
1534     return ret
1535
1536 __all__.append('get_fingerprint')
1537
1538 @session_wrapper
1539 def get_or_set_fingerprint(fpr, session=None):
1540     """
1541     Returns Fingerprint object for given fpr.
1542
1543     If no matching fpr is found, a row is inserted.
1544
1545     @type fpr: string
1546     @param fpr: The fpr to find / add
1547
1548     @type session: SQLAlchemy
1549     @param session: Optional SQL session object (a temporary one will be
1550     generated if not supplied).  If not passed, a commit will be performed at
1551     the end of the function, otherwise the caller is responsible for commiting.
1552     A flush will be performed either way.
1553
1554     @rtype: Fingerprint
1555     @return: the Fingerprint object for the given fpr
1556     """
1557
1558     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1559
1560     try:
1561         ret = q.one()
1562     except NoResultFound:
1563         fingerprint = Fingerprint()
1564         fingerprint.fingerprint = fpr
1565         session.add(fingerprint)
1566         session.commit_or_flush()
1567         ret = fingerprint
1568
1569     return ret
1570
1571 __all__.append('get_or_set_fingerprint')
1572
1573 ################################################################################
1574
1575 # Helper routine for Keyring class
1576 def get_ldap_name(entry):
1577     name = []
1578     for k in ["cn", "mn", "sn"]:
1579         ret = entry.get(k)
1580         if ret and ret[0] != "" and ret[0] != "-":
1581             name.append(ret[0])
1582     return " ".join(name)
1583
1584 ################################################################################
1585
1586 class Keyring(object):
1587     gpg_invocation = "gpg --no-default-keyring --keyring %s" +\
1588                      " --with-colons --fingerprint --fingerprint"
1589
1590     keys = {}
1591     fpr_lookup = {}
1592
1593     def __init__(self, *args, **kwargs):
1594         pass
1595
1596     def __repr__(self):
1597         return '<Keyring %s>' % self.keyring_name
1598
1599     def de_escape_gpg_str(self, txt):
1600         esclist = re.split(r'(\\x..)', txt)
1601         for x in range(1,len(esclist),2):
1602             esclist[x] = "%c" % (int(esclist[x][2:],16))
1603         return "".join(esclist)
1604
1605     def parse_address(self, uid):
1606         """parses uid and returns a tuple of real name and email address"""
1607         import email.Utils
1608         (name, address) = email.Utils.parseaddr(uid)
1609         name = re.sub(r"\s*[(].*[)]", "", name)
1610         name = self.de_escape_gpg_str(name)
1611         if name == "":
1612             name = uid
1613         return (name, address)
1614
1615     def load_keys(self, keyring):
1616         if not self.keyring_id:
1617             raise Exception('Must be initialized with database information')
1618
1619         k = os.popen(self.gpg_invocation % keyring, "r")
1620         key = None
1621         signingkey = False
1622
1623         for line in k.xreadlines():
1624             field = line.split(":")
1625             if field[0] == "pub":
1626                 key = field[4]
1627                 self.keys[key] = {}
1628                 (name, addr) = self.parse_address(field[9])
1629                 if "@" in addr:
1630                     self.keys[key]["email"] = addr
1631                     self.keys[key]["name"] = name
1632                 self.keys[key]["fingerprints"] = []
1633                 signingkey = True
1634             elif key and field[0] == "sub" and len(field) >= 12:
1635                 signingkey = ("s" in field[11])
1636             elif key and field[0] == "uid":
1637                 (name, addr) = self.parse_address(field[9])
1638                 if "email" not in self.keys[key] and "@" in addr:
1639                     self.keys[key]["email"] = addr
1640                     self.keys[key]["name"] = name
1641             elif signingkey and field[0] == "fpr":
1642                 self.keys[key]["fingerprints"].append(field[9])
1643                 self.fpr_lookup[field[9]] = key
1644
1645     def import_users_from_ldap(self, session):
1646         import ldap
1647         cnf = Config()
1648
1649         LDAPDn = cnf["Import-LDAP-Fingerprints::LDAPDn"]
1650         LDAPServer = cnf["Import-LDAP-Fingerprints::LDAPServer"]
1651
1652         l = ldap.open(LDAPServer)
1653         l.simple_bind_s("","")
1654         Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
1655                "(&(keyfingerprint=*)(gidnumber=%s))" % (cnf["Import-Users-From-Passwd::ValidGID"]),
1656                ["uid", "keyfingerprint", "cn", "mn", "sn"])
1657
1658         ldap_fin_uid_id = {}
1659
1660         byuid = {}
1661         byname = {}
1662
1663         for i in Attrs:
1664             entry = i[1]
1665             uid = entry["uid"][0]
1666             name = get_ldap_name(entry)
1667             fingerprints = entry["keyFingerPrint"]
1668             keyid = None
1669             for f in fingerprints:
1670                 key = self.fpr_lookup.get(f, None)
1671                 if key not in self.keys:
1672                     continue
1673                 self.keys[key]["uid"] = uid
1674
1675                 if keyid != None:
1676                     continue
1677                 keyid = get_or_set_uid(uid, session).uid_id
1678                 byuid[keyid] = (uid, name)
1679                 byname[uid] = (keyid, name)
1680
1681         return (byname, byuid)
1682
1683     def generate_users_from_keyring(self, format, session):
1684         byuid = {}
1685         byname = {}
1686         any_invalid = False
1687         for x in self.keys.keys():
1688             if "email" not in self.keys[x]:
1689                 any_invalid = True
1690                 self.keys[x]["uid"] = format % "invalid-uid"
1691             else:
1692                 uid = format % self.keys[x]["email"]
1693                 keyid = get_or_set_uid(uid, session).uid_id
1694                 byuid[keyid] = (uid, self.keys[x]["name"])
1695                 byname[uid] = (keyid, self.keys[x]["name"])
1696                 self.keys[x]["uid"] = uid
1697
1698         if any_invalid:
1699             uid = format % "invalid-uid"
1700             keyid = get_or_set_uid(uid, session).uid_id
1701             byuid[keyid] = (uid, "ungeneratable user id")
1702             byname[uid] = (keyid, "ungeneratable user id")
1703
1704         return (byname, byuid)
1705
1706 __all__.append('Keyring')
1707
1708 @session_wrapper
1709 def get_keyring(keyring, session=None):
1710     """
1711     If C{keyring} does not have an entry in the C{keyrings} table yet, return None
1712     If C{keyring} already has an entry, simply return the existing Keyring
1713
1714     @type keyring: string
1715     @param keyring: the keyring name
1716
1717     @rtype: Keyring
1718     @return: the Keyring object for this keyring
1719     """
1720
1721     q = session.query(Keyring).filter_by(keyring_name=keyring)
1722
1723     try:
1724         return q.one()
1725     except NoResultFound:
1726         return None
1727
1728 __all__.append('get_keyring')
1729
1730 ################################################################################
1731
1732 class KeyringACLMap(object):
1733     def __init__(self, *args, **kwargs):
1734         pass
1735
1736     def __repr__(self):
1737         return '<KeyringACLMap %s>' % self.keyring_acl_map_id
1738
1739 __all__.append('KeyringACLMap')
1740
1741 ################################################################################
1742
1743 class DBChange(object):
1744     def __init__(self, *args, **kwargs):
1745         pass
1746
1747     def __repr__(self):
1748         return '<DBChange %s>' % self.changesname
1749
1750     def clean_from_queue(self):
1751         session = DBConn().session().object_session(self)
1752
1753         # Remove changes_pool_files entries
1754         self.poolfiles = []
1755
1756         # Remove changes_pending_files references
1757         self.files = []
1758
1759         # Clear out of queue
1760         self.in_queue = None
1761         self.approved_for_id = None
1762
1763 __all__.append('DBChange')
1764
1765 @session_wrapper
1766 def get_dbchange(filename, session=None):
1767     """
1768     returns DBChange object for given C{filename}.
1769
1770     @type filename: string
1771     @param filename: the name of the file
1772
1773     @type session: Session
1774     @param session: Optional SQLA session object (a temporary one will be
1775     generated if not supplied)
1776
1777     @rtype: DBChange
1778     @return:  DBChange object for the given filename (C{None} if not present)
1779
1780     """
1781     q = session.query(DBChange).filter_by(changesname=filename)
1782
1783     try:
1784         return q.one()
1785     except NoResultFound:
1786         return None
1787
1788 __all__.append('get_dbchange')
1789
1790 ################################################################################
1791
1792 class Location(ORMObject):
1793     def __init__(self, path = None, component = None):
1794         self.path = path
1795         self.component = component
1796         # the column 'type' should go away, see comment at mapper
1797         self.archive_type = 'pool'
1798
1799     def properties(self):
1800         return ['path', 'location_id', 'archive_type', 'component', \
1801             'files_count']
1802
1803     def not_null_constraints(self):
1804         return ['path', 'archive_type']
1805
1806 __all__.append('Location')
1807
1808 @session_wrapper
1809 def get_location(location, component=None, archive=None, session=None):
1810     """
1811     Returns Location object for the given combination of location, component
1812     and archive
1813
1814     @type location: string
1815     @param location: the path of the location, e.g. I{/srv/ftp-master.debian.org/ftp/pool/}
1816
1817     @type component: string
1818     @param component: the component name (if None, no restriction applied)
1819
1820     @type archive: string
1821     @param archive: the archive name (if None, no restriction applied)
1822
1823     @rtype: Location / None
1824     @return: Either a Location object or None if one can't be found
1825     """
1826
1827     q = session.query(Location).filter_by(path=location)
1828
1829     if archive is not None:
1830         q = q.join(Archive).filter_by(archive_name=archive)
1831
1832     if component is not None:
1833         q = q.join(Component).filter_by(component_name=component)
1834
1835     try:
1836         return q.one()
1837     except NoResultFound:
1838         return None
1839
1840 __all__.append('get_location')
1841
1842 ################################################################################
1843
1844 class Maintainer(ORMObject):
1845     def __init__(self, name = None):
1846         self.name = name
1847
1848     def properties(self):
1849         return ['name', 'maintainer_id']
1850
1851     def not_null_constraints(self):
1852         return ['name']
1853
1854     def get_split_maintainer(self):
1855         if not hasattr(self, 'name') or self.name is None:
1856             return ('', '', '', '')
1857
1858         return fix_maintainer(self.name.strip())
1859
1860 __all__.append('Maintainer')
1861
1862 @session_wrapper
1863 def get_or_set_maintainer(name, session=None):
1864     """
1865     Returns Maintainer object for given maintainer name.
1866
1867     If no matching maintainer name is found, a row is inserted.
1868
1869     @type name: string
1870     @param name: The maintainer name to add
1871
1872     @type session: SQLAlchemy
1873     @param session: Optional SQL session object (a temporary one will be
1874     generated if not supplied).  If not passed, a commit will be performed at
1875     the end of the function, otherwise the caller is responsible for commiting.
1876     A flush will be performed either way.
1877
1878     @rtype: Maintainer
1879     @return: the Maintainer object for the given maintainer
1880     """
1881
1882     q = session.query(Maintainer).filter_by(name=name)
1883     try:
1884         ret = q.one()
1885     except NoResultFound:
1886         maintainer = Maintainer()
1887         maintainer.name = name
1888         session.add(maintainer)
1889         session.commit_or_flush()
1890         ret = maintainer
1891
1892     return ret
1893
1894 __all__.append('get_or_set_maintainer')
1895
1896 @session_wrapper
1897 def get_maintainer(maintainer_id, session=None):
1898     """
1899     Return the name of the maintainer behind C{maintainer_id} or None if that
1900     maintainer_id is invalid.
1901
1902     @type maintainer_id: int
1903     @param maintainer_id: the id of the maintainer
1904
1905     @rtype: Maintainer
1906     @return: the Maintainer with this C{maintainer_id}
1907     """
1908
1909     return session.query(Maintainer).get(maintainer_id)
1910
1911 __all__.append('get_maintainer')
1912
1913 ################################################################################
1914
1915 class NewComment(object):
1916     def __init__(self, *args, **kwargs):
1917         pass
1918
1919     def __repr__(self):
1920         return '''<NewComment for '%s %s' (%s)>''' % (self.package, self.version, self.comment_id)
1921
1922 __all__.append('NewComment')
1923
1924 @session_wrapper
1925 def has_new_comment(package, version, session=None):
1926     """
1927     Returns true if the given combination of C{package}, C{version} has a comment.
1928
1929     @type package: string
1930     @param package: name of the package
1931
1932     @type version: string
1933     @param version: package version
1934
1935     @type session: Session
1936     @param session: Optional SQLA session object (a temporary one will be
1937     generated if not supplied)
1938
1939     @rtype: boolean
1940     @return: true/false
1941     """
1942
1943     q = session.query(NewComment)
1944     q = q.filter_by(package=package)
1945     q = q.filter_by(version=version)
1946
1947     return bool(q.count() > 0)
1948
1949 __all__.append('has_new_comment')
1950
1951 @session_wrapper
1952 def get_new_comments(package=None, version=None, comment_id=None, session=None):
1953     """
1954     Returns (possibly empty) list of NewComment objects for the given
1955     parameters
1956
1957     @type package: string (optional)
1958     @param package: name of the package
1959
1960     @type version: string (optional)
1961     @param version: package version
1962
1963     @type comment_id: int (optional)
1964     @param comment_id: An id of a comment
1965
1966     @type session: Session
1967     @param session: Optional SQLA session object (a temporary one will be
1968     generated if not supplied)
1969
1970     @rtype: list
1971     @return: A (possibly empty) list of NewComment objects will be returned
1972     """
1973
1974     q = session.query(NewComment)
1975     if package is not None: q = q.filter_by(package=package)
1976     if version is not None: q = q.filter_by(version=version)
1977     if comment_id is not None: q = q.filter_by(comment_id=comment_id)
1978
1979     return q.all()
1980
1981 __all__.append('get_new_comments')
1982
1983 ################################################################################
1984
1985 class Override(ORMObject):
1986     def __init__(self, package = None, suite = None, component = None, overridetype = None, \
1987         section = None, priority = None):
1988         self.package = package
1989         self.suite = suite
1990         self.component = component
1991         self.overridetype = overridetype
1992         self.section = section
1993         self.priority = priority
1994
1995     def properties(self):
1996         return ['package', 'suite', 'component', 'overridetype', 'section', \
1997             'priority']
1998
1999     def not_null_constraints(self):
2000         return ['package', 'suite', 'component', 'overridetype', 'section']
2001
2002 __all__.append('Override')
2003
2004 @session_wrapper
2005 def get_override(package, suite=None, component=None, overridetype=None, session=None):
2006     """
2007     Returns Override object for the given parameters
2008
2009     @type package: string
2010     @param package: The name of the package
2011
2012     @type suite: string, list or None
2013     @param suite: The name of the suite (or suites if a list) to limit to.  If
2014                   None, don't limit.  Defaults to None.
2015
2016     @type component: string, list or None
2017     @param component: The name of the component (or components if a list) to
2018                       limit to.  If None, don't limit.  Defaults to None.
2019
2020     @type overridetype: string, list or None
2021     @param overridetype: The name of the overridetype (or overridetypes if a list) to
2022                          limit to.  If None, don't limit.  Defaults to None.
2023
2024     @type session: Session
2025     @param session: Optional SQLA session object (a temporary one will be
2026     generated if not supplied)
2027
2028     @rtype: list
2029     @return: A (possibly empty) list of Override objects will be returned
2030     """
2031
2032     q = session.query(Override)
2033     q = q.filter_by(package=package)
2034
2035     if suite is not None:
2036         if not isinstance(suite, list): suite = [suite]
2037         q = q.join(Suite).filter(Suite.suite_name.in_(suite))
2038
2039     if component is not None:
2040         if not isinstance(component, list): component = [component]
2041         q = q.join(Component).filter(Component.component_name.in_(component))
2042
2043     if overridetype is not None:
2044         if not isinstance(overridetype, list): overridetype = [overridetype]
2045         q = q.join(OverrideType).filter(OverrideType.overridetype.in_(overridetype))
2046
2047     return q.all()
2048
2049 __all__.append('get_override')
2050
2051
2052 ################################################################################
2053
2054 class OverrideType(ORMObject):
2055     def __init__(self, overridetype = None):
2056         self.overridetype = overridetype
2057
2058     def properties(self):
2059         return ['overridetype', 'overridetype_id', 'overrides_count']
2060
2061     def not_null_constraints(self):
2062         return ['overridetype']
2063
2064 __all__.append('OverrideType')
2065
2066 @session_wrapper
2067 def get_override_type(override_type, session=None):
2068     """
2069     Returns OverrideType object for given C{override type}.
2070
2071     @type override_type: string
2072     @param override_type: The name of the override type
2073
2074     @type session: Session
2075     @param session: Optional SQLA session object (a temporary one will be
2076     generated if not supplied)
2077
2078     @rtype: int
2079     @return: the database id for the given override type
2080     """
2081
2082     q = session.query(OverrideType).filter_by(overridetype=override_type)
2083
2084     try:
2085         return q.one()
2086     except NoResultFound:
2087         return None
2088
2089 __all__.append('get_override_type')
2090
2091 ################################################################################
2092
2093 class PolicyQueue(object):
2094     def __init__(self, *args, **kwargs):
2095         pass
2096
2097     def __repr__(self):
2098         return '<PolicyQueue %s>' % self.queue_name
2099
2100 __all__.append('PolicyQueue')
2101
2102 @session_wrapper
2103 def get_policy_queue(queuename, session=None):
2104     """
2105     Returns PolicyQueue object for given C{queue name}
2106
2107     @type queuename: string
2108     @param queuename: The name of the queue
2109
2110     @type session: Session
2111     @param session: Optional SQLA session object (a temporary one will be
2112     generated if not supplied)
2113
2114     @rtype: PolicyQueue
2115     @return: PolicyQueue object for the given queue
2116     """
2117
2118     q = session.query(PolicyQueue).filter_by(queue_name=queuename)
2119
2120     try:
2121         return q.one()
2122     except NoResultFound:
2123         return None
2124
2125 __all__.append('get_policy_queue')
2126
2127 @session_wrapper
2128 def get_policy_queue_from_path(pathname, session=None):
2129     """
2130     Returns PolicyQueue object for given C{path name}
2131
2132     @type queuename: string
2133     @param queuename: The path
2134
2135     @type session: Session
2136     @param session: Optional SQLA session object (a temporary one will be
2137     generated if not supplied)
2138
2139     @rtype: PolicyQueue
2140     @return: PolicyQueue object for the given queue
2141     """
2142
2143     q = session.query(PolicyQueue).filter_by(path=pathname)
2144
2145     try:
2146         return q.one()
2147     except NoResultFound:
2148         return None
2149
2150 __all__.append('get_policy_queue_from_path')
2151
2152 ################################################################################
2153
2154 class Priority(ORMObject):
2155     def __init__(self, priority = None, level = None):
2156         self.priority = priority
2157         self.level = level
2158
2159     def properties(self):
2160         return ['priority', 'priority_id', 'level', 'overrides_count']
2161
2162     def not_null_constraints(self):
2163         return ['priority', 'level']
2164
2165     def __eq__(self, val):
2166         if isinstance(val, str):
2167             return (self.priority == val)
2168         # This signals to use the normal comparison operator
2169         return NotImplemented
2170
2171     def __ne__(self, val):
2172         if isinstance(val, str):
2173             return (self.priority != val)
2174         # This signals to use the normal comparison operator
2175         return NotImplemented
2176
2177 __all__.append('Priority')
2178
2179 @session_wrapper
2180 def get_priority(priority, session=None):
2181     """
2182     Returns Priority object for given C{priority name}.
2183
2184     @type priority: string
2185     @param priority: The name of the priority
2186
2187     @type session: Session
2188     @param session: Optional SQLA session object (a temporary one will be
2189     generated if not supplied)
2190
2191     @rtype: Priority
2192     @return: Priority object for the given priority
2193     """
2194
2195     q = session.query(Priority).filter_by(priority=priority)
2196
2197     try:
2198         return q.one()
2199     except NoResultFound:
2200         return None
2201
2202 __all__.append('get_priority')
2203
2204 @session_wrapper
2205 def get_priorities(session=None):
2206     """
2207     Returns dictionary of priority names -> id mappings
2208
2209     @type session: Session
2210     @param session: Optional SQL session object (a temporary one will be
2211     generated if not supplied)
2212
2213     @rtype: dictionary
2214     @return: dictionary of priority names -> id mappings
2215     """
2216
2217     ret = {}
2218     q = session.query(Priority)
2219     for x in q.all():
2220         ret[x.priority] = x.priority_id
2221
2222     return ret
2223
2224 __all__.append('get_priorities')
2225
2226 ################################################################################
2227
2228 class Section(ORMObject):
2229     def __init__(self, section = None):
2230         self.section = section
2231
2232     def properties(self):
2233         return ['section', 'section_id', 'overrides_count']
2234
2235     def not_null_constraints(self):
2236         return ['section']
2237
2238     def __eq__(self, val):
2239         if isinstance(val, str):
2240             return (self.section == val)
2241         # This signals to use the normal comparison operator
2242         return NotImplemented
2243
2244     def __ne__(self, val):
2245         if isinstance(val, str):
2246             return (self.section != val)
2247         # This signals to use the normal comparison operator
2248         return NotImplemented
2249
2250 __all__.append('Section')
2251
2252 @session_wrapper
2253 def get_section(section, session=None):
2254     """
2255     Returns Section object for given C{section name}.
2256
2257     @type section: string
2258     @param section: The name of the section
2259
2260     @type session: Session
2261     @param session: Optional SQLA session object (a temporary one will be
2262     generated if not supplied)
2263
2264     @rtype: Section
2265     @return: Section object for the given section name
2266     """
2267
2268     q = session.query(Section).filter_by(section=section)
2269
2270     try:
2271         return q.one()
2272     except NoResultFound:
2273         return None
2274
2275 __all__.append('get_section')
2276
2277 @session_wrapper
2278 def get_sections(session=None):
2279     """
2280     Returns dictionary of section names -> id mappings
2281
2282     @type session: Session
2283     @param session: Optional SQL session object (a temporary one will be
2284     generated if not supplied)
2285
2286     @rtype: dictionary
2287     @return: dictionary of section names -> id mappings
2288     """
2289
2290     ret = {}
2291     q = session.query(Section)
2292     for x in q.all():
2293         ret[x.section] = x.section_id
2294
2295     return ret
2296
2297 __all__.append('get_sections')
2298
2299 ################################################################################
2300
2301 class SrcContents(ORMObject):
2302     def __init__(self, file = None, source = None):
2303         self.file = file
2304         self.source = source
2305
2306     def properties(self):
2307         return ['file', 'source']
2308
2309 __all__.append('SrcContents')
2310
2311 ################################################################################
2312
2313 from debian.debfile import Deb822
2314
2315 # Temporary Deb822 subclass to fix bugs with : handling; see #597249
2316 class Dak822(Deb822):
2317     def _internal_parser(self, sequence, fields=None):
2318         # The key is non-whitespace, non-colon characters before any colon.
2319         key_part = r"^(?P<key>[^: \t\n\r\f\v]+)\s*:\s*"
2320         single = re.compile(key_part + r"(?P<data>\S.*?)\s*$")
2321         multi = re.compile(key_part + r"$")
2322         multidata = re.compile(r"^\s(?P<data>.+?)\s*$")
2323
2324         wanted_field = lambda f: fields is None or f in fields
2325
2326         if isinstance(sequence, basestring):
2327             sequence = sequence.splitlines()
2328
2329         curkey = None
2330         content = ""
2331         for line in self.gpg_stripped_paragraph(sequence):
2332             m = single.match(line)
2333             if m:
2334                 if curkey:
2335                     self[curkey] = content
2336
2337                 if not wanted_field(m.group('key')):
2338                     curkey = None
2339                     continue
2340
2341                 curkey = m.group('key')
2342                 content = m.group('data')
2343                 continue
2344
2345             m = multi.match(line)
2346             if m:
2347                 if curkey:
2348                     self[curkey] = content
2349
2350                 if not wanted_field(m.group('key')):
2351                     curkey = None
2352                     continue
2353
2354                 curkey = m.group('key')
2355                 content = ""
2356                 continue
2357
2358             m = multidata.match(line)
2359             if m:
2360                 content += '\n' + line # XXX not m.group('data')?
2361                 continue
2362
2363         if curkey:
2364             self[curkey] = content
2365
2366
2367 class DBSource(ORMObject):
2368     def __init__(self, source = None, version = None, maintainer = None, \
2369         changedby = None, poolfile = None, install_date = None):
2370         self.source = source
2371         self.version = version
2372         self.maintainer = maintainer
2373         self.changedby = changedby
2374         self.poolfile = poolfile
2375         self.install_date = install_date
2376
2377     @property
2378     def pkid(self):
2379         return self.source_id
2380
2381     def properties(self):
2382         return ['source', 'source_id', 'maintainer', 'changedby', \
2383             'fingerprint', 'poolfile', 'version', 'suites_count', \
2384             'install_date', 'binaries_count', 'uploaders_count']
2385
2386     def not_null_constraints(self):
2387         return ['source', 'version', 'install_date', 'maintainer', \
2388             'changedby', 'poolfile', 'install_date']
2389
2390     def read_control_fields(self):
2391         '''
2392         Reads the control information from a dsc
2393
2394         @rtype: tuple
2395         @return: fields is the dsc information in a dictionary form
2396         '''
2397         fullpath = self.poolfile.fullpath
2398         fields = Dak822(open(self.poolfile.fullpath, 'r'))
2399         return fields
2400
2401     metadata = association_proxy('key', 'value')
2402
2403     def scan_contents(self):
2404         '''
2405         Returns a set of names for non directories. The path names are
2406         normalized after converting them from either utf-8 or iso8859-1
2407         encoding.
2408         '''
2409         fullpath = self.poolfile.fullpath
2410         from daklib.contents import UnpackedSource
2411         unpacked = UnpackedSource(fullpath)
2412         fileset = set()
2413         for name in unpacked.get_all_filenames():
2414             # enforce proper utf-8 encoding
2415             try:
2416                 name.decode('utf-8')
2417             except UnicodeDecodeError:
2418                 name = name.decode('iso8859-1').encode('utf-8')
2419             fileset.add(name)
2420         return fileset
2421
2422 __all__.append('DBSource')
2423
2424 @session_wrapper
2425 def source_exists(source, source_version, suites = ["any"], session=None):
2426     """
2427     Ensure that source exists somewhere in the archive for the binary
2428     upload being processed.
2429       1. exact match     => 1.0-3
2430       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
2431
2432     @type source: string
2433     @param source: source name
2434
2435     @type source_version: string
2436     @param source_version: expected source version
2437
2438     @type suites: list
2439     @param suites: list of suites to check in, default I{any}
2440
2441     @type session: Session
2442     @param session: Optional SQLA session object (a temporary one will be
2443     generated if not supplied)
2444
2445     @rtype: int
2446     @return: returns 1 if a source with expected version is found, otherwise 0
2447
2448     """
2449
2450     cnf = Config()
2451     ret = True
2452
2453     from daklib.regexes import re_bin_only_nmu
2454     orig_source_version = re_bin_only_nmu.sub('', source_version)
2455
2456     for suite in suites:
2457         q = session.query(DBSource).filter_by(source=source). \
2458             filter(DBSource.version.in_([source_version, orig_source_version]))
2459         if suite != "any":
2460             # source must exist in suite X, or in some other suite that's
2461             # mapped to X, recursively... silent-maps are counted too,
2462             # unreleased-maps aren't.
2463             maps = cnf.ValueList("SuiteMappings")[:]
2464             maps.reverse()
2465             maps = [ m.split() for m in maps ]
2466             maps = [ (x[1], x[2]) for x in maps
2467                             if x[0] == "map" or x[0] == "silent-map" ]
2468             s = [suite]
2469             for (from_, to) in maps:
2470                 if from_ in s and to not in s:
2471                     s.append(to)
2472
2473             q = q.filter(DBSource.suites.any(Suite.suite_name.in_(s)))
2474
2475         if q.count() > 0:
2476             continue
2477
2478         # No source found so return not ok
2479         ret = False
2480
2481     return ret
2482
2483 __all__.append('source_exists')
2484
2485 @session_wrapper
2486 def get_suites_source_in(source, session=None):
2487     """
2488     Returns list of Suite objects which given C{source} name is in
2489
2490     @type source: str
2491     @param source: DBSource package name to search for
2492
2493     @rtype: list
2494     @return: list of Suite objects for the given source
2495     """
2496
2497     return session.query(Suite).filter(Suite.sources.any(source=source)).all()
2498
2499 __all__.append('get_suites_source_in')
2500
2501 @session_wrapper
2502 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
2503     """
2504     Returns list of DBSource objects for given C{source} name and other parameters
2505
2506     @type source: str
2507     @param source: DBSource package name to search for
2508
2509     @type version: str or None
2510     @param version: DBSource version name to search for or None if not applicable
2511
2512     @type dm_upload_allowed: bool
2513     @param dm_upload_allowed: If None, no effect.  If True or False, only
2514     return packages with that dm_upload_allowed setting
2515
2516     @type session: Session
2517     @param session: Optional SQL session object (a temporary one will be
2518     generated if not supplied)
2519
2520     @rtype: list
2521     @return: list of DBSource objects for the given name (may be empty)
2522     """
2523
2524     q = session.query(DBSource).filter_by(source=source)
2525
2526     if version is not None:
2527         q = q.filter_by(version=version)
2528
2529     if dm_upload_allowed is not None:
2530         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
2531
2532     return q.all()
2533
2534 __all__.append('get_sources_from_name')
2535
2536 # FIXME: This function fails badly if it finds more than 1 source package and
2537 # its implementation is trivial enough to be inlined.
2538 @session_wrapper
2539 def get_source_in_suite(source, suite, session=None):
2540     """
2541     Returns a DBSource object for a combination of C{source} and C{suite}.
2542
2543       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2544       - B{suite} - a suite name, eg. I{unstable}
2545
2546     @type source: string
2547     @param source: source package name
2548
2549     @type suite: string
2550     @param suite: the suite name
2551
2552     @rtype: string
2553     @return: the version for I{source} in I{suite}
2554
2555     """
2556
2557     q = get_suite(suite, session).get_sources(source)
2558     try:
2559         return q.one()
2560     except NoResultFound:
2561         return None
2562
2563 __all__.append('get_source_in_suite')
2564
2565 @session_wrapper
2566 def import_metadata_into_db(obj, session=None):
2567     """
2568     This routine works on either DBBinary or DBSource objects and imports
2569     their metadata into the database
2570     """
2571     fields = obj.read_control_fields()
2572     for k in fields.keys():
2573         try:
2574             # Try raw ASCII
2575             val = str(fields[k])
2576         except UnicodeEncodeError:
2577             # Fall back to UTF-8
2578             try:
2579                 val = fields[k].encode('utf-8')
2580             except UnicodeEncodeError:
2581                 # Finally try iso8859-1
2582                 val = fields[k].encode('iso8859-1')
2583                 # Otherwise we allow the exception to percolate up and we cause
2584                 # a reject as someone is playing silly buggers
2585
2586         obj.metadata[get_or_set_metadatakey(k, session)] = val
2587
2588     session.commit_or_flush()
2589
2590 __all__.append('import_metadata_into_db')
2591
2592
2593 ################################################################################
2594
2595 @session_wrapper
2596 def add_dsc_to_db(u, filename, session=None):
2597     entry = u.pkg.files[filename]
2598     source = DBSource()
2599     pfs = []
2600
2601     source.source = u.pkg.dsc["source"]
2602     source.version = u.pkg.dsc["version"] # NB: not files[file]["version"], that has no epoch
2603     source.maintainer_id = get_or_set_maintainer(u.pkg.dsc["maintainer"], session).maintainer_id
2604     source.changedby_id = get_or_set_maintainer(u.pkg.changes["changed-by"], session).maintainer_id
2605     source.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2606     source.install_date = datetime.now().date()
2607
2608     dsc_component = entry["component"]
2609     dsc_location_id = entry["location id"]
2610
2611     source.dm_upload_allowed = (u.pkg.dsc.get("dm-upload-allowed", '') == "yes")
2612
2613     # Set up a new poolfile if necessary
2614     if not entry.has_key("files id") or not entry["files id"]:
2615         filename = entry["pool name"] + filename
2616         poolfile = add_poolfile(filename, entry, dsc_location_id, session)
2617         session.flush()
2618         pfs.append(poolfile)
2619         entry["files id"] = poolfile.file_id
2620
2621     source.poolfile_id = entry["files id"]
2622     session.add(source)
2623
2624     suite_names = u.pkg.changes["distribution"].keys()
2625     source.suites = session.query(Suite). \
2626         filter(Suite.suite_name.in_(suite_names)).all()
2627
2628     # Add the source files to the DB (files and dsc_files)
2629     dscfile = DSCFile()
2630     dscfile.source_id = source.source_id
2631     dscfile.poolfile_id = entry["files id"]
2632     session.add(dscfile)
2633
2634     for dsc_file, dentry in u.pkg.dsc_files.items():
2635         df = DSCFile()
2636         df.source_id = source.source_id
2637
2638         # If the .orig tarball is already in the pool, it's
2639         # files id is stored in dsc_files by check_dsc().
2640         files_id = dentry.get("files id", None)
2641
2642         # Find the entry in the files hash
2643         # TODO: Bail out here properly
2644         dfentry = None
2645         for f, e in u.pkg.files.items():
2646             if f == dsc_file:
2647                 dfentry = e
2648                 break
2649
2650         if files_id is None:
2651             filename = dfentry["pool name"] + dsc_file
2652
2653             (found, obj) = check_poolfile(filename, dentry["size"], dentry["md5sum"], dsc_location_id)
2654             # FIXME: needs to check for -1/-2 and or handle exception
2655             if found and obj is not None:
2656                 files_id = obj.file_id
2657                 pfs.append(obj)
2658
2659             # If still not found, add it
2660             if files_id is None:
2661                 # HACK: Force sha1sum etc into dentry
2662                 dentry["sha1sum"] = dfentry["sha1sum"]
2663                 dentry["sha256sum"] = dfentry["sha256sum"]
2664                 poolfile = add_poolfile(filename, dentry, dsc_location_id, session)
2665                 pfs.append(poolfile)
2666                 files_id = poolfile.file_id
2667         else:
2668             poolfile = get_poolfile_by_id(files_id, session)
2669             if poolfile is None:
2670                 utils.fubar("INTERNAL ERROR. Found no poolfile with id %d" % files_id)
2671             pfs.append(poolfile)
2672
2673         df.poolfile_id = files_id
2674         session.add(df)
2675
2676     # Add the src_uploaders to the DB
2677     source.uploaders = [source.maintainer]
2678     if u.pkg.dsc.has_key("uploaders"):
2679         for up in u.pkg.dsc["uploaders"].replace(">, ", ">\t").split("\t"):
2680             up = up.strip()
2681             source.uploaders.append(get_or_set_maintainer(up, session))
2682
2683     session.flush()
2684
2685     return source, dsc_component, dsc_location_id, pfs
2686
2687 __all__.append('add_dsc_to_db')
2688
2689 @session_wrapper
2690 def add_deb_to_db(u, filename, session=None):
2691     """
2692     Contrary to what you might expect, this routine deals with both
2693     debs and udebs.  That info is in 'dbtype', whilst 'type' is
2694     'deb' for both of them
2695     """
2696     cnf = Config()
2697     entry = u.pkg.files[filename]
2698
2699     bin = DBBinary()
2700     bin.package = entry["package"]
2701     bin.version = entry["version"]
2702     bin.maintainer_id = get_or_set_maintainer(entry["maintainer"], session).maintainer_id
2703     bin.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2704     bin.arch_id = get_architecture(entry["architecture"], session).arch_id
2705     bin.binarytype = entry["dbtype"]
2706
2707     # Find poolfile id
2708     filename = entry["pool name"] + filename
2709     fullpath = os.path.join(cnf["Dir::Pool"], filename)
2710     if not entry.get("location id", None):
2711         entry["location id"] = get_location(cnf["Dir::Pool"], entry["component"], session=session).location_id
2712
2713     if entry.get("files id", None):
2714         poolfile = get_poolfile_by_id(bin.poolfile_id)
2715         bin.poolfile_id = entry["files id"]
2716     else:
2717         poolfile = add_poolfile(filename, entry, entry["location id"], session)
2718         bin.poolfile_id = entry["files id"] = poolfile.file_id
2719
2720     # Find source id
2721     bin_sources = get_sources_from_name(entry["source package"], entry["source version"], session=session)
2722     if len(bin_sources) != 1:
2723         raise NoSourceFieldError, "Unable to find a unique source id for %s (%s), %s, file %s, type %s, signed by %s" % \
2724                                   (bin.package, bin.version, entry["architecture"],
2725                                    filename, bin.binarytype, u.pkg.changes["fingerprint"])
2726
2727     bin.source_id = bin_sources[0].source_id
2728
2729     if entry.has_key("built-using"):
2730         for srcname, version in entry["built-using"]:
2731             exsources = get_sources_from_name(srcname, version, session=session)
2732             if len(exsources) != 1:
2733                 raise NoSourceFieldError, "Unable to find source package (%s = %s) in Built-Using for %s (%s), %s, file %s, type %s, signed by %s" % \
2734                                           (srcname, version, bin.package, bin.version, entry["architecture"],
2735                                            filename, bin.binarytype, u.pkg.changes["fingerprint"])
2736
2737             bin.extra_sources.append(exsources[0])
2738
2739     # Add and flush object so it has an ID
2740     session.add(bin)
2741
2742     suite_names = u.pkg.changes["distribution"].keys()
2743     bin.suites = session.query(Suite). \
2744         filter(Suite.suite_name.in_(suite_names)).all()
2745
2746     session.flush()
2747
2748     # Deal with contents - disabled for now
2749     #contents = copy_temporary_contents(bin.package, bin.version, bin.architecture.arch_string, os.path.basename(filename), None, session)
2750     #if not contents:
2751     #    print "REJECT\nCould not determine contents of package %s" % bin.package
2752     #    session.rollback()
2753     #    raise MissingContents, "No contents stored for package %s, and couldn't determine contents of %s" % (bin.package, filename)
2754
2755     return bin, poolfile
2756
2757 __all__.append('add_deb_to_db')
2758
2759 ################################################################################
2760
2761 class SourceACL(object):
2762     def __init__(self, *args, **kwargs):
2763         pass
2764
2765     def __repr__(self):
2766         return '<SourceACL %s>' % self.source_acl_id
2767
2768 __all__.append('SourceACL')
2769
2770 ################################################################################
2771
2772 class SrcFormat(object):
2773     def __init__(self, *args, **kwargs):
2774         pass
2775
2776     def __repr__(self):
2777         return '<SrcFormat %s>' % (self.format_name)
2778
2779 __all__.append('SrcFormat')
2780
2781 ################################################################################
2782
2783 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
2784                  ('SuiteID', 'suite_id'),
2785                  ('Version', 'version'),
2786                  ('Origin', 'origin'),
2787                  ('Label', 'label'),
2788                  ('Description', 'description'),
2789                  ('Untouchable', 'untouchable'),
2790                  ('Announce', 'announce'),
2791                  ('Codename', 'codename'),
2792                  ('OverrideCodename', 'overridecodename'),
2793                  ('ValidTime', 'validtime'),
2794                  ('Priority', 'priority'),
2795                  ('NotAutomatic', 'notautomatic'),
2796                  ('CopyChanges', 'copychanges'),
2797                  ('OverrideSuite', 'overridesuite')]
2798
2799 # Why the heck don't we have any UNIQUE constraints in table suite?
2800 # TODO: Add UNIQUE constraints for appropriate columns.
2801 class Suite(ORMObject):
2802     def __init__(self, suite_name = None, version = None):
2803         self.suite_name = suite_name
2804         self.version = version
2805
2806     def properties(self):
2807         return ['suite_name', 'version', 'sources_count', 'binaries_count', \
2808             'overrides_count']
2809
2810     def not_null_constraints(self):
2811         return ['suite_name']
2812
2813     def __eq__(self, val):
2814         if isinstance(val, str):
2815             return (self.suite_name == val)
2816         # This signals to use the normal comparison operator
2817         return NotImplemented
2818
2819     def __ne__(self, val):
2820         if isinstance(val, str):
2821             return (self.suite_name != val)
2822         # This signals to use the normal comparison operator
2823         return NotImplemented
2824
2825     def details(self):
2826         ret = []
2827         for disp, field in SUITE_FIELDS:
2828             val = getattr(self, field, None)
2829             if val is not None:
2830                 ret.append("%s: %s" % (disp, val))
2831
2832         return "\n".join(ret)
2833
2834     def get_architectures(self, skipsrc=False, skipall=False):
2835         """
2836         Returns list of Architecture objects
2837
2838         @type skipsrc: boolean
2839         @param skipsrc: Whether to skip returning the 'source' architecture entry
2840         (Default False)
2841
2842         @type skipall: boolean
2843         @param skipall: Whether to skip returning the 'all' architecture entry
2844         (Default False)
2845
2846         @rtype: list
2847         @return: list of Architecture objects for the given name (may be empty)
2848         """
2849
2850         q = object_session(self).query(Architecture).with_parent(self)
2851         if skipsrc:
2852             q = q.filter(Architecture.arch_string != 'source')
2853         if skipall:
2854             q = q.filter(Architecture.arch_string != 'all')
2855         return q.order_by(Architecture.arch_string).all()
2856
2857     def get_sources(self, source):
2858         """
2859         Returns a query object representing DBSource that is part of C{suite}.
2860
2861           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2862
2863         @type source: string
2864         @param source: source package name
2865
2866         @rtype: sqlalchemy.orm.query.Query
2867         @return: a query of DBSource
2868
2869         """
2870
2871         session = object_session(self)
2872         return session.query(DBSource).filter_by(source = source). \
2873             with_parent(self)
2874
2875 __all__.append('Suite')
2876
2877 @session_wrapper
2878 def get_suite(suite, session=None):
2879     """
2880     Returns Suite object for given C{suite name}.
2881
2882     @type suite: string
2883     @param suite: The name of the suite
2884
2885     @type session: Session
2886     @param session: Optional SQLA session object (a temporary one will be
2887     generated if not supplied)
2888
2889     @rtype: Suite
2890     @return: Suite object for the requested suite name (None if not present)
2891     """
2892
2893     q = session.query(Suite).filter_by(suite_name=suite)
2894
2895     try:
2896         return q.one()
2897     except NoResultFound:
2898         return None
2899
2900 __all__.append('get_suite')
2901
2902 ################################################################################
2903
2904 # TODO: should be removed because the implementation is too trivial
2905 @session_wrapper
2906 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
2907     """
2908     Returns list of Architecture objects for given C{suite} name
2909
2910     @type suite: str
2911     @param suite: Suite name to search for
2912
2913     @type skipsrc: boolean
2914     @param skipsrc: Whether to skip returning the 'source' architecture entry
2915     (Default False)
2916
2917     @type skipall: boolean
2918     @param skipall: Whether to skip returning the 'all' architecture entry
2919     (Default False)
2920
2921     @type session: Session
2922     @param session: Optional SQL session object (a temporary one will be
2923     generated if not supplied)
2924
2925     @rtype: list
2926     @return: list of Architecture objects for the given name (may be empty)
2927     """
2928
2929     return get_suite(suite, session).get_architectures(skipsrc, skipall)
2930
2931 __all__.append('get_suite_architectures')
2932
2933 ################################################################################
2934
2935 class SuiteSrcFormat(object):
2936     def __init__(self, *args, **kwargs):
2937         pass
2938
2939     def __repr__(self):
2940         return '<SuiteSrcFormat (%s, %s)>' % (self.suite_id, self.src_format_id)
2941
2942 __all__.append('SuiteSrcFormat')
2943
2944 @session_wrapper
2945 def get_suite_src_formats(suite, session=None):
2946     """
2947     Returns list of allowed SrcFormat for C{suite}.
2948
2949     @type suite: str
2950     @param suite: Suite name to search for
2951
2952     @type session: Session
2953     @param session: Optional SQL session object (a temporary one will be
2954     generated if not supplied)
2955
2956     @rtype: list
2957     @return: the list of allowed source formats for I{suite}
2958     """
2959
2960     q = session.query(SrcFormat)
2961     q = q.join(SuiteSrcFormat)
2962     q = q.join(Suite).filter_by(suite_name=suite)
2963     q = q.order_by('format_name')
2964
2965     return q.all()
2966
2967 __all__.append('get_suite_src_formats')
2968
2969 ################################################################################
2970
2971 class Uid(ORMObject):
2972     def __init__(self, uid = None, name = None):
2973         self.uid = uid
2974         self.name = name
2975
2976     def __eq__(self, val):
2977         if isinstance(val, str):
2978             return (self.uid == val)
2979         # This signals to use the normal comparison operator
2980         return NotImplemented
2981
2982     def __ne__(self, val):
2983         if isinstance(val, str):
2984             return (self.uid != val)
2985         # This signals to use the normal comparison operator
2986         return NotImplemented
2987
2988     def properties(self):
2989         return ['uid', 'name', 'fingerprint']
2990
2991     def not_null_constraints(self):
2992         return ['uid']
2993
2994 __all__.append('Uid')
2995
2996 @session_wrapper
2997 def get_or_set_uid(uidname, session=None):
2998     """
2999     Returns uid object for given uidname.
3000
3001     If no matching uidname is found, a row is inserted.
3002
3003     @type uidname: string
3004     @param uidname: The uid to add
3005
3006     @type session: SQLAlchemy
3007     @param session: Optional SQL session object (a temporary one will be
3008     generated if not supplied).  If not passed, a commit will be performed at
3009     the end of the function, otherwise the caller is responsible for commiting.
3010
3011     @rtype: Uid
3012     @return: the uid object for the given uidname
3013     """
3014
3015     q = session.query(Uid).filter_by(uid=uidname)
3016
3017     try:
3018         ret = q.one()
3019     except NoResultFound:
3020         uid = Uid()
3021         uid.uid = uidname
3022         session.add(uid)
3023         session.commit_or_flush()
3024         ret = uid
3025
3026     return ret
3027
3028 __all__.append('get_or_set_uid')
3029
3030 @session_wrapper
3031 def get_uid_from_fingerprint(fpr, session=None):
3032     q = session.query(Uid)
3033     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
3034
3035     try:
3036         return q.one()
3037     except NoResultFound:
3038         return None
3039
3040 __all__.append('get_uid_from_fingerprint')
3041
3042 ################################################################################
3043
3044 class UploadBlock(object):
3045     def __init__(self, *args, **kwargs):
3046         pass
3047
3048     def __repr__(self):
3049         return '<UploadBlock %s (%s)>' % (self.source, self.upload_block_id)
3050
3051 __all__.append('UploadBlock')
3052
3053 ################################################################################
3054
3055 class MetadataKey(ORMObject):
3056     def __init__(self, key = None):
3057         self.key = key
3058
3059     def properties(self):
3060         return ['key']
3061
3062     def not_null_constraints(self):
3063         return ['key']
3064
3065 __all__.append('MetadataKey')
3066
3067 @session_wrapper
3068 def get_or_set_metadatakey(keyname, session=None):
3069     """
3070     Returns MetadataKey object for given uidname.
3071
3072     If no matching keyname is found, a row is inserted.
3073
3074     @type uidname: string
3075     @param uidname: The keyname to add
3076
3077     @type session: SQLAlchemy
3078     @param session: Optional SQL session object (a temporary one will be
3079     generated if not supplied).  If not passed, a commit will be performed at
3080     the end of the function, otherwise the caller is responsible for commiting.
3081
3082     @rtype: MetadataKey
3083     @return: the metadatakey object for the given keyname
3084     """
3085
3086     q = session.query(MetadataKey).filter_by(key=keyname)
3087
3088     try:
3089         ret = q.one()
3090     except NoResultFound:
3091         ret = MetadataKey(keyname)
3092         session.add(ret)
3093         session.commit_or_flush()
3094
3095     return ret
3096
3097 __all__.append('get_or_set_metadatakey')
3098
3099 ################################################################################
3100
3101 class BinaryMetadata(ORMObject):
3102     def __init__(self, key = None, value = None, binary = None):
3103         self.key = key
3104         self.value = value
3105         self.binary = binary
3106
3107     def properties(self):
3108         return ['binary', 'key', 'value']
3109
3110     def not_null_constraints(self):
3111         return ['value']
3112
3113 __all__.append('BinaryMetadata')
3114
3115 ################################################################################
3116
3117 class SourceMetadata(ORMObject):
3118     def __init__(self, key = None, value = None, source = None):
3119         self.key = key
3120         self.value = value
3121         self.source = source
3122
3123     def properties(self):
3124         return ['source', 'key', 'value']
3125
3126     def not_null_constraints(self):
3127         return ['value']
3128
3129 __all__.append('SourceMetadata')
3130
3131 ################################################################################
3132
3133 class VersionCheck(ORMObject):
3134     def __init__(self, *args, **kwargs):
3135         pass
3136
3137     def properties(self):
3138         #return ['suite_id', 'check', 'reference_id']
3139         return ['check']
3140
3141     def not_null_constraints(self):
3142         return ['suite', 'check', 'reference']
3143
3144 __all__.append('VersionCheck')
3145
3146 @session_wrapper
3147 def get_version_checks(suite_name, check = None, session = None):
3148     suite = get_suite(suite_name, session)
3149     if not suite:
3150         # Make sure that what we return is iterable so that list comprehensions
3151         # involving this don't cause a traceback
3152         return []
3153     q = session.query(VersionCheck).filter_by(suite=suite)
3154     if check:
3155         q = q.filter_by(check=check)
3156     return q.all()
3157
3158 __all__.append('get_version_checks')
3159
3160 ################################################################################
3161
3162 class DBConn(object):
3163     """
3164     database module init.
3165     """
3166     __shared_state = {}
3167
3168     def __init__(self, *args, **kwargs):
3169         self.__dict__ = self.__shared_state
3170
3171         if not getattr(self, 'initialised', False):
3172             self.initialised = True
3173             self.debug = kwargs.has_key('debug')
3174             self.__createconn()
3175
3176     def __setuptables(self):
3177         tables = (
3178             'architecture',
3179             'archive',
3180             'bin_associations',
3181             'bin_contents',
3182             'binaries',
3183             'binaries_metadata',
3184             'binary_acl',
3185             'binary_acl_map',
3186             'build_queue',
3187             'build_queue_files',
3188             'build_queue_policy_files',
3189             'changelogs_text',
3190             'changes',
3191             'component',
3192             'config',
3193             'changes_pending_binaries',
3194             'changes_pending_files',
3195             'changes_pending_source',
3196             'changes_pending_files_map',
3197             'changes_pending_source_files',
3198             'changes_pool_files',
3199             'dsc_files',
3200             'external_overrides',
3201             'extra_src_references',
3202             'files',
3203             'fingerprint',
3204             'keyrings',
3205             'keyring_acl_map',
3206             'location',
3207             'maintainer',
3208             'metadata_keys',
3209             'new_comments',
3210             # TODO: the maintainer column in table override should be removed.
3211             'override',
3212             'override_type',
3213             'policy_queue',
3214             'priority',
3215             'section',
3216             'source',
3217             'source_acl',
3218             'source_metadata',
3219             'src_associations',
3220             'src_contents',
3221             'src_format',
3222             'src_uploaders',
3223             'suite',
3224             'suite_architectures',
3225             'suite_build_queue_copy',
3226             'suite_src_formats',
3227             'uid',
3228             'upload_blocks',
3229             'version_check',
3230         )
3231
3232         views = (
3233             'almost_obsolete_all_associations',
3234             'almost_obsolete_src_associations',
3235             'any_associations_source',
3236             'bin_associations_binaries',
3237             'binaries_suite_arch',
3238             'binfiles_suite_component_arch',
3239             'changelogs',
3240             'file_arch_suite',
3241             'newest_all_associations',
3242             'newest_any_associations',
3243             'newest_source',
3244             'newest_src_association',
3245             'obsolete_all_associations',
3246             'obsolete_any_associations',
3247             'obsolete_any_by_all_associations',
3248             'obsolete_src_associations',
3249             'source_suite',
3250             'src_associations_bin',
3251             'src_associations_src',
3252             'suite_arch_by_name',
3253         )
3254
3255         for table_name in tables:
3256             table = Table(table_name, self.db_meta, \
3257                 autoload=True, useexisting=True)
3258             setattr(self, 'tbl_%s' % table_name, table)
3259
3260         for view_name in views:
3261             view = Table(view_name, self.db_meta, autoload=True)
3262             setattr(self, 'view_%s' % view_name, view)
3263
3264     def __setupmappers(self):
3265         mapper(Architecture, self.tbl_architecture,
3266             properties = dict(arch_id = self.tbl_architecture.c.id,
3267                suites = relation(Suite, secondary=self.tbl_suite_architectures,
3268                    order_by='suite_name',
3269                    backref=backref('architectures', order_by='arch_string'))),
3270             extension = validator)
3271
3272         mapper(Archive, self.tbl_archive,
3273                properties = dict(archive_id = self.tbl_archive.c.id,
3274                                  archive_name = self.tbl_archive.c.name))
3275
3276         mapper(BuildQueue, self.tbl_build_queue,
3277                properties = dict(queue_id = self.tbl_build_queue.c.id))
3278
3279         mapper(BuildQueueFile, self.tbl_build_queue_files,
3280                properties = dict(buildqueue = relation(BuildQueue, backref='queuefiles'),
3281                                  poolfile = relation(PoolFile, backref='buildqueueinstances')))
3282
3283         mapper(BuildQueuePolicyFile, self.tbl_build_queue_policy_files,
3284                properties = dict(
3285                 build_queue = relation(BuildQueue, backref='policy_queue_files'),
3286                 file = relation(ChangePendingFile, lazy='joined')))
3287
3288         mapper(DBBinary, self.tbl_binaries,
3289                properties = dict(binary_id = self.tbl_binaries.c.id,
3290                                  package = self.tbl_binaries.c.package,
3291                                  version = self.tbl_binaries.c.version,
3292                                  maintainer_id = self.tbl_binaries.c.maintainer,
3293                                  maintainer = relation(Maintainer),
3294                                  source_id = self.tbl_binaries.c.source,
3295                                  source = relation(DBSource, backref='binaries'),
3296                                  arch_id = self.tbl_binaries.c.architecture,
3297                                  architecture = relation(Architecture),
3298                                  poolfile_id = self.tbl_binaries.c.file,
3299                                  poolfile = relation(PoolFile, backref=backref('binary', uselist = False)),
3300                                  binarytype = self.tbl_binaries.c.type,
3301                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
3302                                  fingerprint = relation(Fingerprint),
3303                                  install_date = self.tbl_binaries.c.install_date,
3304                                  suites = relation(Suite, secondary=self.tbl_bin_associations,
3305                                      backref=backref('binaries', lazy='dynamic')),
3306                                  extra_sources = relation(DBSource, secondary=self.tbl_extra_src_references,
3307                                      backref=backref('extra_binary_references', lazy='dynamic')),
3308                                  key = relation(BinaryMetadata, cascade='all',
3309                                      collection_class=attribute_mapped_collection('key'))),
3310                 extension = validator)
3311
3312         mapper(BinaryACL, self.tbl_binary_acl,
3313                properties = dict(binary_acl_id = self.tbl_binary_acl.c.id))
3314
3315         mapper(BinaryACLMap, self.tbl_binary_acl_map,
3316                properties = dict(binary_acl_map_id = self.tbl_binary_acl_map.c.id,
3317                                  fingerprint = relation(Fingerprint, backref="binary_acl_map"),
3318                                  architecture = relation(Architecture)))
3319
3320         mapper(Component, self.tbl_component,
3321                properties = dict(component_id = self.tbl_component.c.id,
3322                                  component_name = self.tbl_component.c.name),
3323                extension = validator)
3324
3325         mapper(DBConfig, self.tbl_config,
3326                properties = dict(config_id = self.tbl_config.c.id))
3327
3328         mapper(DSCFile, self.tbl_dsc_files,
3329                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
3330                                  source_id = self.tbl_dsc_files.c.source,
3331                                  source = relation(DBSource),
3332                                  poolfile_id = self.tbl_dsc_files.c.file,
3333                                  poolfile = relation(PoolFile)))
3334
3335         mapper(ExternalOverride, self.tbl_external_overrides)
3336
3337         mapper(PoolFile, self.tbl_files,
3338                properties = dict(file_id = self.tbl_files.c.id,
3339                                  filesize = self.tbl_files.c.size,
3340                                  location_id = self.tbl_files.c.location,
3341                                  location = relation(Location,
3342                                      # using lazy='dynamic' in the back
3343                                      # reference because we have A LOT of
3344                                      # files in one location
3345                                      backref=backref('files', lazy='dynamic'))),
3346                 extension = validator)
3347
3348         mapper(Fingerprint, self.tbl_fingerprint,
3349                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
3350                                  uid_id = self.tbl_fingerprint.c.uid,
3351                                  uid = relation(Uid),
3352                                  keyring_id = self.tbl_fingerprint.c.keyring,
3353                                  keyring = relation(Keyring),
3354                                  source_acl = relation(SourceACL),
3355                                  binary_acl = relation(BinaryACL)),
3356                extension = validator)
3357
3358         mapper(Keyring, self.tbl_keyrings,
3359                properties = dict(keyring_name = self.tbl_keyrings.c.name,
3360                                  keyring_id = self.tbl_keyrings.c.id))
3361
3362         mapper(DBChange, self.tbl_changes,
3363                properties = dict(change_id = self.tbl_changes.c.id,
3364                                  poolfiles = relation(PoolFile,
3365                                                       secondary=self.tbl_changes_pool_files,
3366                                                       backref="changeslinks"),
3367                                  seen = self.tbl_changes.c.seen,
3368                                  source = self.tbl_changes.c.source,
3369                                  binaries = self.tbl_changes.c.binaries,
3370                                  architecture = self.tbl_changes.c.architecture,
3371                                  distribution = self.tbl_changes.c.distribution,
3372                                  urgency = self.tbl_changes.c.urgency,
3373                                  maintainer = self.tbl_changes.c.maintainer,
3374                                  changedby = self.tbl_changes.c.changedby,
3375                                  date = self.tbl_changes.c.date,
3376                                  version = self.tbl_changes.c.version,
3377                                  files = relation(ChangePendingFile,
3378                                                   secondary=self.tbl_changes_pending_files_map,
3379                                                   backref="changesfile"),
3380                                  in_queue_id = self.tbl_changes.c.in_queue,
3381                                  in_queue = relation(PolicyQueue,
3382                                                      primaryjoin=(self.tbl_changes.c.in_queue==self.tbl_policy_queue.c.id)),
3383                                  approved_for_id = self.tbl_changes.c.approved_for))
3384
3385         mapper(ChangePendingBinary, self.tbl_changes_pending_binaries,
3386                properties = dict(change_pending_binary_id = self.tbl_changes_pending_binaries.c.id))
3387
3388         mapper(ChangePendingFile, self.tbl_changes_pending_files,
3389                properties = dict(change_pending_file_id = self.tbl_changes_pending_files.c.id,
3390                                  filename = self.tbl_changes_pending_files.c.filename,
3391                                  size = self.tbl_changes_pending_files.c.size,
3392                                  md5sum = self.tbl_changes_pending_files.c.md5sum,
3393                                  sha1sum = self.tbl_changes_pending_files.c.sha1sum,
3394                                  sha256sum = self.tbl_changes_pending_files.c.sha256sum))
3395
3396         mapper(ChangePendingSource, self.tbl_changes_pending_source,
3397                properties = dict(change_pending_source_id = self.tbl_changes_pending_source.c.id,
3398                                  change = relation(DBChange),
3399                                  maintainer = relation(Maintainer,
3400                                                        primaryjoin=(self.tbl_changes_pending_source.c.maintainer_id==self.tbl_maintainer.c.id)),
3401                                  changedby = relation(Maintainer,
3402                                                       primaryjoin=(self.tbl_changes_pending_source.c.changedby_id==self.tbl_maintainer.c.id)),
3403                                  fingerprint = relation(Fingerprint),
3404                                  source_files = relation(ChangePendingFile,
3405                                                          secondary=self.tbl_changes_pending_source_files,
3406                                                          backref="pending_sources")))
3407
3408
3409         mapper(KeyringACLMap, self.tbl_keyring_acl_map,
3410                properties = dict(keyring_acl_map_id = self.tbl_keyring_acl_map.c.id,
3411                                  keyring = relation(Keyring, backref="keyring_acl_map"),
3412                                  architecture = relation(Architecture)))
3413
3414         mapper(Location, self.tbl_location,
3415                properties = dict(location_id = self.tbl_location.c.id,
3416                                  component_id = self.tbl_location.c.component,
3417                                  component = relation(Component, backref='location'),
3418                                  archive_id = self.tbl_location.c.archive,
3419                                  archive = relation(Archive),
3420                                  # FIXME: the 'type' column is old cruft and
3421                                  # should be removed in the future.
3422                                  archive_type = self.tbl_location.c.type),
3423                extension = validator)
3424
3425         mapper(Maintainer, self.tbl_maintainer,
3426                properties = dict(maintainer_id = self.tbl_maintainer.c.id,
3427                    maintains_sources = relation(DBSource, backref='maintainer',
3428                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.maintainer)),
3429                    changed_sources = relation(DBSource, backref='changedby',
3430                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.changedby))),
3431                 extension = validator)
3432
3433         mapper(NewComment, self.tbl_new_comments,
3434                properties = dict(comment_id = self.tbl_new_comments.c.id))
3435
3436         mapper(Override, self.tbl_override,
3437                properties = dict(suite_id = self.tbl_override.c.suite,
3438                                  suite = relation(Suite, \
3439                                     backref=backref('overrides', lazy='dynamic')),
3440                                  package = self.tbl_override.c.package,
3441                                  component_id = self.tbl_override.c.component,
3442                                  component = relation(Component, \
3443                                     backref=backref('overrides', lazy='dynamic')),
3444                                  priority_id = self.tbl_override.c.priority,
3445                                  priority = relation(Priority, \
3446                                     backref=backref('overrides', lazy='dynamic')),
3447                                  section_id = self.tbl_override.c.section,
3448                                  section = relation(Section, \
3449                                     backref=backref('overrides', lazy='dynamic')),
3450                                  overridetype_id = self.tbl_override.c.type,
3451                                  overridetype = relation(OverrideType, \
3452                                     backref=backref('overrides', lazy='dynamic'))))
3453
3454         mapper(OverrideType, self.tbl_override_type,
3455                properties = dict(overridetype = self.tbl_override_type.c.type,
3456                                  overridetype_id = self.tbl_override_type.c.id))
3457
3458         mapper(PolicyQueue, self.tbl_policy_queue,
3459                properties = dict(policy_queue_id = self.tbl_policy_queue.c.id))
3460
3461         mapper(Priority, self.tbl_priority,
3462                properties = dict(priority_id = self.tbl_priority.c.id))
3463
3464         mapper(Section, self.tbl_section,
3465                properties = dict(section_id = self.tbl_section.c.id,
3466                                  section=self.tbl_section.c.section))
3467
3468         mapper(DBSource, self.tbl_source,
3469                properties = dict(source_id = self.tbl_source.c.id,
3470                                  version = self.tbl_source.c.version,
3471                                  maintainer_id = self.tbl_source.c.maintainer,
3472                                  poolfile_id = self.tbl_source.c.file,
3473                                  poolfile = relation(PoolFile, backref=backref('source', uselist = False)),
3474                                  fingerprint_id = self.tbl_source.c.sig_fpr,
3475                                  fingerprint = relation(Fingerprint),
3476                                  changedby_id = self.tbl_source.c.changedby,
3477                                  srcfiles = relation(DSCFile,
3478                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
3479                                  suites = relation(Suite, secondary=self.tbl_src_associations,
3480                                      backref=backref('sources', lazy='dynamic')),
3481                                  uploaders = relation(Maintainer,
3482                                      secondary=self.tbl_src_uploaders),
3483                                  key = relation(SourceMetadata, cascade='all',
3484                                      collection_class=attribute_mapped_collection('key'))),
3485                extension = validator)
3486
3487         mapper(SourceACL, self.tbl_source_acl,
3488                properties = dict(source_acl_id = self.tbl_source_acl.c.id))
3489
3490         mapper(SrcFormat, self.tbl_src_format,
3491                properties = dict(src_format_id = self.tbl_src_format.c.id,
3492                                  format_name = self.tbl_src_format.c.format_name))
3493
3494         mapper(Suite, self.tbl_suite,
3495                properties = dict(suite_id = self.tbl_suite.c.id,
3496                                  policy_queue = relation(PolicyQueue),
3497                                  copy_queues = relation(BuildQueue,
3498                                      secondary=self.tbl_suite_build_queue_copy)),
3499                 extension = validator)
3500
3501         mapper(SuiteSrcFormat, self.tbl_suite_src_formats,
3502                properties = dict(suite_id = self.tbl_suite_src_formats.c.suite,
3503                                  suite = relation(Suite, backref='suitesrcformats'),
3504                                  src_format_id = self.tbl_suite_src_formats.c.src_format,
3505                                  src_format = relation(SrcFormat)))
3506
3507         mapper(Uid, self.tbl_uid,
3508                properties = dict(uid_id = self.tbl_uid.c.id,
3509                                  fingerprint = relation(Fingerprint)),
3510                extension = validator)
3511
3512         mapper(UploadBlock, self.tbl_upload_blocks,
3513                properties = dict(upload_block_id = self.tbl_upload_blocks.c.id,
3514                                  fingerprint = relation(Fingerprint, backref="uploadblocks"),
3515                                  uid = relation(Uid, backref="uploadblocks")))
3516
3517         mapper(BinContents, self.tbl_bin_contents,
3518             properties = dict(
3519                 binary = relation(DBBinary,
3520                     backref=backref('contents', lazy='dynamic', cascade='all')),
3521                 file = self.tbl_bin_contents.c.file))
3522
3523         mapper(SrcContents, self.tbl_src_contents,
3524             properties = dict(
3525                 source = relation(DBSource,
3526                     backref=backref('contents', lazy='dynamic', cascade='all')),
3527                 file = self.tbl_src_contents.c.file))
3528
3529         mapper(MetadataKey, self.tbl_metadata_keys,
3530             properties = dict(
3531                 key_id = self.tbl_metadata_keys.c.key_id,
3532                 key = self.tbl_metadata_keys.c.key))
3533
3534         mapper(BinaryMetadata, self.tbl_binaries_metadata,
3535             properties = dict(
3536                 binary_id = self.tbl_binaries_metadata.c.bin_id,
3537                 binary = relation(DBBinary),
3538                 key_id = self.tbl_binaries_metadata.c.key_id,
3539                 key = relation(MetadataKey),
3540                 value = self.tbl_binaries_metadata.c.value))
3541
3542         mapper(SourceMetadata, self.tbl_source_metadata,
3543             properties = dict(
3544                 source_id = self.tbl_source_metadata.c.src_id,
3545                 source = relation(DBSource),
3546                 key_id = self.tbl_source_metadata.c.key_id,
3547                 key = relation(MetadataKey),
3548                 value = self.tbl_source_metadata.c.value))
3549
3550         mapper(VersionCheck, self.tbl_version_check,
3551             properties = dict(
3552                 suite_id = self.tbl_version_check.c.suite,
3553                 suite = relation(Suite, primaryjoin=self.tbl_version_check.c.suite==self.tbl_suite.c.id),
3554                 reference_id = self.tbl_version_check.c.reference,
3555                 reference = relation(Suite, primaryjoin=self.tbl_version_check.c.reference==self.tbl_suite.c.id, lazy='joined')))
3556
3557     ## Connection functions
3558     def __createconn(self):
3559         from config import Config
3560         cnf = Config()
3561         if cnf.has_key("DB::Service"):
3562             connstr = "postgresql://service=%s" % cnf["DB::Service"]
3563         elif cnf.has_key("DB::Host"):
3564             # TCP/IP
3565             connstr = "postgresql://%s" % cnf["DB::Host"]
3566             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
3567                 connstr += ":%s" % cnf["DB::Port"]
3568             connstr += "/%s" % cnf["DB::Name"]
3569         else:
3570             # Unix Socket
3571             connstr = "postgresql:///%s" % cnf["DB::Name"]
3572             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
3573                 connstr += "?port=%s" % cnf["DB::Port"]
3574
3575         engine_args = { 'echo': self.debug }
3576         if cnf.has_key('DB::PoolSize'):
3577             engine_args['pool_size'] = int(cnf['DB::PoolSize'])
3578         if cnf.has_key('DB::MaxOverflow'):
3579             engine_args['max_overflow'] = int(cnf['DB::MaxOverflow'])
3580         if sa_major_version == '0.6' and cnf.has_key('DB::Unicode') and \
3581             cnf['DB::Unicode'] == 'false':
3582             engine_args['use_native_unicode'] = False
3583
3584         # Monkey patch a new dialect in in order to support service= syntax
3585         import sqlalchemy.dialects.postgresql
3586         from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
3587         class PGDialect_psycopg2_dak(PGDialect_psycopg2):
3588             def create_connect_args(self, url):
3589                 if str(url).startswith('postgresql://service='):
3590                     # Eww
3591                     servicename = str(url)[21:]
3592                     return (['service=%s' % servicename], {})
3593                 else:
3594                     return PGDialect_psycopg2.create_connect_args(self, url)
3595
3596         sqlalchemy.dialects.postgresql.base.dialect = PGDialect_psycopg2_dak
3597
3598         self.db_pg   = create_engine(connstr, **engine_args)
3599         self.db_meta = MetaData()
3600         self.db_meta.bind = self.db_pg
3601         self.db_smaker = sessionmaker(bind=self.db_pg,
3602                                       autoflush=True,
3603                                       autocommit=False)
3604
3605         self.__setuptables()
3606         self.__setupmappers()
3607         self.pid = os.getpid()
3608
3609     def session(self, work_mem = 0):
3610         '''
3611         Returns a new session object. If a work_mem parameter is provided a new
3612         transaction is started and the work_mem parameter is set for this
3613         transaction. The work_mem parameter is measured in MB. A default value
3614         will be used if the parameter is not set.
3615         '''
3616         # reinitialize DBConn in new processes
3617         if self.pid != os.getpid():
3618             clear_mappers()
3619             self.__createconn()
3620         session = self.db_smaker()
3621         if work_mem > 0:
3622             session.execute("SET LOCAL work_mem TO '%d MB'" % work_mem)
3623         return session
3624
3625 __all__.append('DBConn')
3626
3627