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