]> 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', \
915             'location_count', 'meets_dfsg', 'overrides_count']
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(ORMObject):
1877     def __init__(self, overridetype = None):
1878         self.overridetype = overridetype
1879
1880     def properties(self):
1881         return ['overridetype', 'overridetype_id', 'overrides_count']
1882
1883     def not_null_constraints(self):
1884         return ['overridetype']
1885
1886 __all__.append('OverrideType')
1887
1888 @session_wrapper
1889 def get_override_type(override_type, session=None):
1890     """
1891     Returns OverrideType object for given C{override type}.
1892
1893     @type override_type: string
1894     @param override_type: The name of the override type
1895
1896     @type session: Session
1897     @param session: Optional SQLA session object (a temporary one will be
1898     generated if not supplied)
1899
1900     @rtype: int
1901     @return: the database id for the given override type
1902     """
1903
1904     q = session.query(OverrideType).filter_by(overridetype=override_type)
1905
1906     try:
1907         return q.one()
1908     except NoResultFound:
1909         return None
1910
1911 __all__.append('get_override_type')
1912
1913 ################################################################################
1914
1915 class DebContents(object):
1916     def __init__(self, *args, **kwargs):
1917         pass
1918
1919     def __repr__(self):
1920         return '<DebConetnts %s: %s>' % (self.package.package,self.file)
1921
1922 __all__.append('DebContents')
1923
1924
1925 class UdebContents(object):
1926     def __init__(self, *args, **kwargs):
1927         pass
1928
1929     def __repr__(self):
1930         return '<UdebConetnts %s: %s>' % (self.package.package,self.file)
1931
1932 __all__.append('UdebContents')
1933
1934 class PendingBinContents(object):
1935     def __init__(self, *args, **kwargs):
1936         pass
1937
1938     def __repr__(self):
1939         return '<PendingBinContents %s>' % self.contents_id
1940
1941 __all__.append('PendingBinContents')
1942
1943 def insert_pending_content_paths(package,
1944                                  is_udeb,
1945                                  fullpaths,
1946                                  session=None):
1947     """
1948     Make sure given paths are temporarily associated with given
1949     package
1950
1951     @type package: dict
1952     @param package: the package to associate with should have been read in from the binary control file
1953     @type fullpaths: list
1954     @param fullpaths: the list of paths of the file being associated with the binary
1955     @type session: SQLAlchemy session
1956     @param session: Optional SQLAlchemy session.  If this is passed, the caller
1957     is responsible for ensuring a transaction has begun and committing the
1958     results or rolling back based on the result code.  If not passed, a commit
1959     will be performed at the end of the function
1960
1961     @return: True upon success, False if there is a problem
1962     """
1963
1964     privatetrans = False
1965
1966     if session is None:
1967         session = DBConn().session()
1968         privatetrans = True
1969
1970     try:
1971         arch = get_architecture(package['Architecture'], session)
1972         arch_id = arch.arch_id
1973
1974         # Remove any already existing recorded files for this package
1975         q = session.query(PendingBinContents)
1976         q = q.filter_by(package=package['Package'])
1977         q = q.filter_by(version=package['Version'])
1978         q = q.filter_by(architecture=arch_id)
1979         q.delete()
1980
1981         for fullpath in fullpaths:
1982
1983             if fullpath.startswith( "./" ):
1984                 fullpath = fullpath[2:]
1985
1986             pca = PendingBinContents()
1987             pca.package = package['Package']
1988             pca.version = package['Version']
1989             pca.file = fullpath
1990             pca.architecture = arch_id
1991
1992             if isudeb:
1993                 pca.type = 8 # gross
1994             else:
1995                 pca.type = 7 # also gross
1996             session.add(pca)
1997
1998         # Only commit if we set up the session ourself
1999         if privatetrans:
2000             session.commit()
2001             session.close()
2002         else:
2003             session.flush()
2004
2005         return True
2006     except Exception, e:
2007         traceback.print_exc()
2008
2009         # Only rollback if we set up the session ourself
2010         if privatetrans:
2011             session.rollback()
2012             session.close()
2013
2014         return False
2015
2016 __all__.append('insert_pending_content_paths')
2017
2018 ################################################################################
2019
2020 class PolicyQueue(object):
2021     def __init__(self, *args, **kwargs):
2022         pass
2023
2024     def __repr__(self):
2025         return '<PolicyQueue %s>' % self.queue_name
2026
2027 __all__.append('PolicyQueue')
2028
2029 @session_wrapper
2030 def get_policy_queue(queuename, session=None):
2031     """
2032     Returns PolicyQueue object for given C{queue name}
2033
2034     @type queuename: string
2035     @param queuename: The name of the queue
2036
2037     @type session: Session
2038     @param session: Optional SQLA session object (a temporary one will be
2039     generated if not supplied)
2040
2041     @rtype: PolicyQueue
2042     @return: PolicyQueue object for the given queue
2043     """
2044
2045     q = session.query(PolicyQueue).filter_by(queue_name=queuename)
2046
2047     try:
2048         return q.one()
2049     except NoResultFound:
2050         return None
2051
2052 __all__.append('get_policy_queue')
2053
2054 @session_wrapper
2055 def get_policy_queue_from_path(pathname, session=None):
2056     """
2057     Returns PolicyQueue object for given C{path name}
2058
2059     @type queuename: string
2060     @param queuename: The path
2061
2062     @type session: Session
2063     @param session: Optional SQLA session object (a temporary one will be
2064     generated if not supplied)
2065
2066     @rtype: PolicyQueue
2067     @return: PolicyQueue object for the given queue
2068     """
2069
2070     q = session.query(PolicyQueue).filter_by(path=pathname)
2071
2072     try:
2073         return q.one()
2074     except NoResultFound:
2075         return None
2076
2077 __all__.append('get_policy_queue_from_path')
2078
2079 ################################################################################
2080
2081 class Priority(ORMObject):
2082     def __init__(self, priority = None, level = None):
2083         self.priority = priority
2084         self.level = level
2085
2086     def properties(self):
2087         return ['priority', 'priority_id', 'level', 'overrides_count']
2088
2089     def not_null_constraints(self):
2090         return ['priority', 'level']
2091
2092     def __eq__(self, val):
2093         if isinstance(val, str):
2094             return (self.priority == val)
2095         # This signals to use the normal comparison operator
2096         return NotImplemented
2097
2098     def __ne__(self, val):
2099         if isinstance(val, str):
2100             return (self.priority != val)
2101         # This signals to use the normal comparison operator
2102         return NotImplemented
2103
2104 __all__.append('Priority')
2105
2106 @session_wrapper
2107 def get_priority(priority, session=None):
2108     """
2109     Returns Priority object for given C{priority name}.
2110
2111     @type priority: string
2112     @param priority: The name of the priority
2113
2114     @type session: Session
2115     @param session: Optional SQLA session object (a temporary one will be
2116     generated if not supplied)
2117
2118     @rtype: Priority
2119     @return: Priority object for the given priority
2120     """
2121
2122     q = session.query(Priority).filter_by(priority=priority)
2123
2124     try:
2125         return q.one()
2126     except NoResultFound:
2127         return None
2128
2129 __all__.append('get_priority')
2130
2131 @session_wrapper
2132 def get_priorities(session=None):
2133     """
2134     Returns dictionary of priority names -> id mappings
2135
2136     @type session: Session
2137     @param session: Optional SQL session object (a temporary one will be
2138     generated if not supplied)
2139
2140     @rtype: dictionary
2141     @return: dictionary of priority names -> id mappings
2142     """
2143
2144     ret = {}
2145     q = session.query(Priority)
2146     for x in q.all():
2147         ret[x.priority] = x.priority_id
2148
2149     return ret
2150
2151 __all__.append('get_priorities')
2152
2153 ################################################################################
2154
2155 class Section(ORMObject):
2156     def __init__(self, section = None):
2157         self.section = section
2158
2159     def properties(self):
2160         return ['section', 'section_id', 'overrides_count']
2161
2162     def not_null_constraints(self):
2163         return ['section']
2164
2165     def __eq__(self, val):
2166         if isinstance(val, str):
2167             return (self.section == val)
2168         # This signals to use the normal comparison operator
2169         return NotImplemented
2170
2171     def __ne__(self, val):
2172         if isinstance(val, str):
2173             return (self.section != val)
2174         # This signals to use the normal comparison operator
2175         return NotImplemented
2176
2177 __all__.append('Section')
2178
2179 @session_wrapper
2180 def get_section(section, session=None):
2181     """
2182     Returns Section object for given C{section name}.
2183
2184     @type section: string
2185     @param section: The name of the section
2186
2187     @type session: Session
2188     @param session: Optional SQLA session object (a temporary one will be
2189     generated if not supplied)
2190
2191     @rtype: Section
2192     @return: Section object for the given section name
2193     """
2194
2195     q = session.query(Section).filter_by(section=section)
2196
2197     try:
2198         return q.one()
2199     except NoResultFound:
2200         return None
2201
2202 __all__.append('get_section')
2203
2204 @session_wrapper
2205 def get_sections(session=None):
2206     """
2207     Returns dictionary of section names -> id mappings
2208
2209     @type session: Session
2210     @param session: Optional SQL session object (a temporary one will be
2211     generated if not supplied)
2212
2213     @rtype: dictionary
2214     @return: dictionary of section names -> id mappings
2215     """
2216
2217     ret = {}
2218     q = session.query(Section)
2219     for x in q.all():
2220         ret[x.section] = x.section_id
2221
2222     return ret
2223
2224 __all__.append('get_sections')
2225
2226 ################################################################################
2227
2228 class DBSource(ORMObject):
2229     def __init__(self, source = None, version = None, maintainer = None, \
2230         changedby = None, poolfile = None, install_date = None):
2231         self.source = source
2232         self.version = version
2233         self.maintainer = maintainer
2234         self.changedby = changedby
2235         self.poolfile = poolfile
2236         self.install_date = install_date
2237
2238     def properties(self):
2239         return ['source', 'source_id', 'maintainer', 'changedby', \
2240             'fingerprint', 'poolfile', 'version', 'suites_count', \
2241             'install_date', 'binaries_count']
2242
2243     def not_null_constraints(self):
2244         return ['source', 'version', 'install_date', 'maintainer', \
2245             'changedby', 'poolfile', 'install_date']
2246
2247 __all__.append('DBSource')
2248
2249 @session_wrapper
2250 def source_exists(source, source_version, suites = ["any"], session=None):
2251     """
2252     Ensure that source exists somewhere in the archive for the binary
2253     upload being processed.
2254       1. exact match     => 1.0-3
2255       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
2256
2257     @type source: string
2258     @param source: source name
2259
2260     @type source_version: string
2261     @param source_version: expected source version
2262
2263     @type suites: list
2264     @param suites: list of suites to check in, default I{any}
2265
2266     @type session: Session
2267     @param session: Optional SQLA session object (a temporary one will be
2268     generated if not supplied)
2269
2270     @rtype: int
2271     @return: returns 1 if a source with expected version is found, otherwise 0
2272
2273     """
2274
2275     cnf = Config()
2276     ret = True
2277
2278     from daklib.regexes import re_bin_only_nmu
2279     orig_source_version = re_bin_only_nmu.sub('', source_version)
2280
2281     for suite in suites:
2282         q = session.query(DBSource).filter_by(source=source). \
2283             filter(DBSource.version.in_([source_version, orig_source_version]))
2284         if suite != "any":
2285             # source must exist in suite X, or in some other suite that's
2286             # mapped to X, recursively... silent-maps are counted too,
2287             # unreleased-maps aren't.
2288             maps = cnf.ValueList("SuiteMappings")[:]
2289             maps.reverse()
2290             maps = [ m.split() for m in maps ]
2291             maps = [ (x[1], x[2]) for x in maps
2292                             if x[0] == "map" or x[0] == "silent-map" ]
2293             s = [suite]
2294             for (from_, to) in maps:
2295                 if from_ in s and to not in s:
2296                     s.append(to)
2297
2298             q = q.filter(DBSource.suites.any(Suite.suite_name.in_(s)))
2299
2300         if q.count() > 0:
2301             continue
2302
2303         # No source found so return not ok
2304         ret = False
2305
2306     return ret
2307
2308 __all__.append('source_exists')
2309
2310 @session_wrapper
2311 def get_suites_source_in(source, session=None):
2312     """
2313     Returns list of Suite objects which given C{source} name is in
2314
2315     @type source: str
2316     @param source: DBSource package name to search for
2317
2318     @rtype: list
2319     @return: list of Suite objects for the given source
2320     """
2321
2322     return session.query(Suite).filter(Suite.sources.any(source=source)).all()
2323
2324 __all__.append('get_suites_source_in')
2325
2326 @session_wrapper
2327 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
2328     """
2329     Returns list of DBSource objects for given C{source} name and other parameters
2330
2331     @type source: str
2332     @param source: DBSource package name to search for
2333
2334     @type version: str or None
2335     @param version: DBSource version name to search for or None if not applicable
2336
2337     @type dm_upload_allowed: bool
2338     @param dm_upload_allowed: If None, no effect.  If True or False, only
2339     return packages with that dm_upload_allowed setting
2340
2341     @type session: Session
2342     @param session: Optional SQL session object (a temporary one will be
2343     generated if not supplied)
2344
2345     @rtype: list
2346     @return: list of DBSource objects for the given name (may be empty)
2347     """
2348
2349     q = session.query(DBSource).filter_by(source=source)
2350
2351     if version is not None:
2352         q = q.filter_by(version=version)
2353
2354     if dm_upload_allowed is not None:
2355         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
2356
2357     return q.all()
2358
2359 __all__.append('get_sources_from_name')
2360
2361 # FIXME: This function fails badly if it finds more than 1 source package and
2362 # its implementation is trivial enough to be inlined.
2363 @session_wrapper
2364 def get_source_in_suite(source, suite, session=None):
2365     """
2366     Returns a DBSource object for a combination of C{source} and C{suite}.
2367
2368       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2369       - B{suite} - a suite name, eg. I{unstable}
2370
2371     @type source: string
2372     @param source: source package name
2373
2374     @type suite: string
2375     @param suite: the suite name
2376
2377     @rtype: string
2378     @return: the version for I{source} in I{suite}
2379
2380     """
2381
2382     q = get_suite(suite, session).get_sources(source)
2383     try:
2384         return q.one()
2385     except NoResultFound:
2386         return None
2387
2388 __all__.append('get_source_in_suite')
2389
2390 ################################################################################
2391
2392 @session_wrapper
2393 def add_dsc_to_db(u, filename, session=None):
2394     entry = u.pkg.files[filename]
2395     source = DBSource()
2396     pfs = []
2397
2398     source.source = u.pkg.dsc["source"]
2399     source.version = u.pkg.dsc["version"] # NB: not files[file]["version"], that has no epoch
2400     source.maintainer_id = get_or_set_maintainer(u.pkg.dsc["maintainer"], session).maintainer_id
2401     source.changedby_id = get_or_set_maintainer(u.pkg.changes["changed-by"], session).maintainer_id
2402     source.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2403     source.install_date = datetime.now().date()
2404
2405     dsc_component = entry["component"]
2406     dsc_location_id = entry["location id"]
2407
2408     source.dm_upload_allowed = (u.pkg.dsc.get("dm-upload-allowed", '') == "yes")
2409
2410     # Set up a new poolfile if necessary
2411     if not entry.has_key("files id") or not entry["files id"]:
2412         filename = entry["pool name"] + filename
2413         poolfile = add_poolfile(filename, entry, dsc_location_id, session)
2414         session.flush()
2415         pfs.append(poolfile)
2416         entry["files id"] = poolfile.file_id
2417
2418     source.poolfile_id = entry["files id"]
2419     session.add(source)
2420
2421     suite_names = u.pkg.changes["distribution"].keys()
2422     source.suites = session.query(Suite). \
2423         filter(Suite.suite_name.in_(suite_names)).all()
2424
2425     # Add the source files to the DB (files and dsc_files)
2426     dscfile = DSCFile()
2427     dscfile.source_id = source.source_id
2428     dscfile.poolfile_id = entry["files id"]
2429     session.add(dscfile)
2430
2431     for dsc_file, dentry in u.pkg.dsc_files.items():
2432         df = DSCFile()
2433         df.source_id = source.source_id
2434
2435         # If the .orig tarball is already in the pool, it's
2436         # files id is stored in dsc_files by check_dsc().
2437         files_id = dentry.get("files id", None)
2438
2439         # Find the entry in the files hash
2440         # TODO: Bail out here properly
2441         dfentry = None
2442         for f, e in u.pkg.files.items():
2443             if f == dsc_file:
2444                 dfentry = e
2445                 break
2446
2447         if files_id is None:
2448             filename = dfentry["pool name"] + dsc_file
2449
2450             (found, obj) = check_poolfile(filename, dentry["size"], dentry["md5sum"], dsc_location_id)
2451             # FIXME: needs to check for -1/-2 and or handle exception
2452             if found and obj is not None:
2453                 files_id = obj.file_id
2454                 pfs.append(obj)
2455
2456             # If still not found, add it
2457             if files_id is None:
2458                 # HACK: Force sha1sum etc into dentry
2459                 dentry["sha1sum"] = dfentry["sha1sum"]
2460                 dentry["sha256sum"] = dfentry["sha256sum"]
2461                 poolfile = add_poolfile(filename, dentry, dsc_location_id, session)
2462                 pfs.append(poolfile)
2463                 files_id = poolfile.file_id
2464         else:
2465             poolfile = get_poolfile_by_id(files_id, session)
2466             if poolfile is None:
2467                 utils.fubar("INTERNAL ERROR. Found no poolfile with id %d" % files_id)
2468             pfs.append(poolfile)
2469
2470         df.poolfile_id = files_id
2471         session.add(df)
2472
2473     # Add the src_uploaders to the DB
2474     uploader_ids = [source.maintainer_id]
2475     if u.pkg.dsc.has_key("uploaders"):
2476         for up in u.pkg.dsc["uploaders"].replace(">, ", ">\t").split("\t"):
2477             up = up.strip()
2478             uploader_ids.append(get_or_set_maintainer(up, session).maintainer_id)
2479
2480     added_ids = {}
2481     for up_id in uploader_ids:
2482         if added_ids.has_key(up_id):
2483             import utils
2484             utils.warn("Already saw uploader %s for source %s" % (up_id, source.source))
2485             continue
2486
2487         added_ids[up_id]=1
2488
2489         su = SrcUploader()
2490         su.maintainer_id = up_id
2491         su.source_id = source.source_id
2492         session.add(su)
2493
2494     session.flush()
2495
2496     return source, dsc_component, dsc_location_id, pfs
2497
2498 __all__.append('add_dsc_to_db')
2499
2500 @session_wrapper
2501 def add_deb_to_db(u, filename, session=None):
2502     """
2503     Contrary to what you might expect, this routine deals with both
2504     debs and udebs.  That info is in 'dbtype', whilst 'type' is
2505     'deb' for both of them
2506     """
2507     cnf = Config()
2508     entry = u.pkg.files[filename]
2509
2510     bin = DBBinary()
2511     bin.package = entry["package"]
2512     bin.version = entry["version"]
2513     bin.maintainer_id = get_or_set_maintainer(entry["maintainer"], session).maintainer_id
2514     bin.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2515     bin.arch_id = get_architecture(entry["architecture"], session).arch_id
2516     bin.binarytype = entry["dbtype"]
2517
2518     # Find poolfile id
2519     filename = entry["pool name"] + filename
2520     fullpath = os.path.join(cnf["Dir::Pool"], filename)
2521     if not entry.get("location id", None):
2522         entry["location id"] = get_location(cnf["Dir::Pool"], entry["component"], session=session).location_id
2523
2524     if entry.get("files id", None):
2525         poolfile = get_poolfile_by_id(bin.poolfile_id)
2526         bin.poolfile_id = entry["files id"]
2527     else:
2528         poolfile = add_poolfile(filename, entry, entry["location id"], session)
2529         bin.poolfile_id = entry["files id"] = poolfile.file_id
2530
2531     # Find source id
2532     bin_sources = get_sources_from_name(entry["source package"], entry["source version"], session=session)
2533     if len(bin_sources) != 1:
2534         raise NoSourceFieldError, "Unable to find a unique source id for %s (%s), %s, file %s, type %s, signed by %s" % \
2535                                   (bin.package, bin.version, entry["architecture"],
2536                                    filename, bin.binarytype, u.pkg.changes["fingerprint"])
2537
2538     bin.source_id = bin_sources[0].source_id
2539
2540     # Add and flush object so it has an ID
2541     session.add(bin)
2542
2543     suite_names = u.pkg.changes["distribution"].keys()
2544     bin.suites = session.query(Suite). \
2545         filter(Suite.suite_name.in_(suite_names)).all()
2546
2547     session.flush()
2548
2549     # Deal with contents - disabled for now
2550     #contents = copy_temporary_contents(bin.package, bin.version, bin.architecture.arch_string, os.path.basename(filename), None, session)
2551     #if not contents:
2552     #    print "REJECT\nCould not determine contents of package %s" % bin.package
2553     #    session.rollback()
2554     #    raise MissingContents, "No contents stored for package %s, and couldn't determine contents of %s" % (bin.package, filename)
2555
2556     return poolfile
2557
2558 __all__.append('add_deb_to_db')
2559
2560 ################################################################################
2561
2562 class SourceACL(object):
2563     def __init__(self, *args, **kwargs):
2564         pass
2565
2566     def __repr__(self):
2567         return '<SourceACL %s>' % self.source_acl_id
2568
2569 __all__.append('SourceACL')
2570
2571 ################################################################################
2572
2573 class SrcFormat(object):
2574     def __init__(self, *args, **kwargs):
2575         pass
2576
2577     def __repr__(self):
2578         return '<SrcFormat %s>' % (self.format_name)
2579
2580 __all__.append('SrcFormat')
2581
2582 ################################################################################
2583
2584 class SrcUploader(object):
2585     def __init__(self, *args, **kwargs):
2586         pass
2587
2588     def __repr__(self):
2589         return '<SrcUploader %s>' % self.uploader_id
2590
2591 __all__.append('SrcUploader')
2592
2593 ################################################################################
2594
2595 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
2596                  ('SuiteID', 'suite_id'),
2597                  ('Version', 'version'),
2598                  ('Origin', 'origin'),
2599                  ('Label', 'label'),
2600                  ('Description', 'description'),
2601                  ('Untouchable', 'untouchable'),
2602                  ('Announce', 'announce'),
2603                  ('Codename', 'codename'),
2604                  ('OverrideCodename', 'overridecodename'),
2605                  ('ValidTime', 'validtime'),
2606                  ('Priority', 'priority'),
2607                  ('NotAutomatic', 'notautomatic'),
2608                  ('CopyChanges', 'copychanges'),
2609                  ('OverrideSuite', 'overridesuite')]
2610
2611 # Why the heck don't we have any UNIQUE constraints in table suite?
2612 # TODO: Add UNIQUE constraints for appropriate columns.
2613 class Suite(ORMObject):
2614     def __init__(self, suite_name = None, version = None):
2615         self.suite_name = suite_name
2616         self.version = version
2617
2618     def properties(self):
2619         return ['suite_name', 'version', 'sources_count', 'binaries_count', \
2620             'overrides_count']
2621
2622     def not_null_constraints(self):
2623         return ['suite_name', 'version']
2624
2625     def __eq__(self, val):
2626         if isinstance(val, str):
2627             return (self.suite_name == val)
2628         # This signals to use the normal comparison operator
2629         return NotImplemented
2630
2631     def __ne__(self, val):
2632         if isinstance(val, str):
2633             return (self.suite_name != val)
2634         # This signals to use the normal comparison operator
2635         return NotImplemented
2636
2637     def details(self):
2638         ret = []
2639         for disp, field in SUITE_FIELDS:
2640             val = getattr(self, field, None)
2641             if val is not None:
2642                 ret.append("%s: %s" % (disp, val))
2643
2644         return "\n".join(ret)
2645
2646     def get_architectures(self, skipsrc=False, skipall=False):
2647         """
2648         Returns list of Architecture objects
2649
2650         @type skipsrc: boolean
2651         @param skipsrc: Whether to skip returning the 'source' architecture entry
2652         (Default False)
2653
2654         @type skipall: boolean
2655         @param skipall: Whether to skip returning the 'all' architecture entry
2656         (Default False)
2657
2658         @rtype: list
2659         @return: list of Architecture objects for the given name (may be empty)
2660         """
2661
2662         q = object_session(self).query(Architecture).with_parent(self)
2663         if skipsrc:
2664             q = q.filter(Architecture.arch_string != 'source')
2665         if skipall:
2666             q = q.filter(Architecture.arch_string != 'all')
2667         return q.order_by(Architecture.arch_string).all()
2668
2669     def get_sources(self, source):
2670         """
2671         Returns a query object representing DBSource that is part of C{suite}.
2672
2673           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2674
2675         @type source: string
2676         @param source: source package name
2677
2678         @rtype: sqlalchemy.orm.query.Query
2679         @return: a query of DBSource
2680
2681         """
2682
2683         session = object_session(self)
2684         return session.query(DBSource).filter_by(source = source). \
2685             with_parent(self)
2686
2687 __all__.append('Suite')
2688
2689 @session_wrapper
2690 def get_suite(suite, session=None):
2691     """
2692     Returns Suite object for given C{suite name}.
2693
2694     @type suite: string
2695     @param suite: The name of the suite
2696
2697     @type session: Session
2698     @param session: Optional SQLA session object (a temporary one will be
2699     generated if not supplied)
2700
2701     @rtype: Suite
2702     @return: Suite object for the requested suite name (None if not present)
2703     """
2704
2705     q = session.query(Suite).filter_by(suite_name=suite)
2706
2707     try:
2708         return q.one()
2709     except NoResultFound:
2710         return None
2711
2712 __all__.append('get_suite')
2713
2714 ################################################################################
2715
2716 # TODO: should be removed because the implementation is too trivial
2717 @session_wrapper
2718 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
2719     """
2720     Returns list of Architecture objects for given C{suite} name
2721
2722     @type suite: str
2723     @param suite: Suite name to search for
2724
2725     @type skipsrc: boolean
2726     @param skipsrc: Whether to skip returning the 'source' architecture entry
2727     (Default False)
2728
2729     @type skipall: boolean
2730     @param skipall: Whether to skip returning the 'all' architecture entry
2731     (Default False)
2732
2733     @type session: Session
2734     @param session: Optional SQL session object (a temporary one will be
2735     generated if not supplied)
2736
2737     @rtype: list
2738     @return: list of Architecture objects for the given name (may be empty)
2739     """
2740
2741     return get_suite(suite, session).get_architectures(skipsrc, skipall)
2742
2743 __all__.append('get_suite_architectures')
2744
2745 ################################################################################
2746
2747 class SuiteSrcFormat(object):
2748     def __init__(self, *args, **kwargs):
2749         pass
2750
2751     def __repr__(self):
2752         return '<SuiteSrcFormat (%s, %s)>' % (self.suite_id, self.src_format_id)
2753
2754 __all__.append('SuiteSrcFormat')
2755
2756 @session_wrapper
2757 def get_suite_src_formats(suite, session=None):
2758     """
2759     Returns list of allowed SrcFormat for C{suite}.
2760
2761     @type suite: str
2762     @param suite: Suite name to search for
2763
2764     @type session: Session
2765     @param session: Optional SQL session object (a temporary one will be
2766     generated if not supplied)
2767
2768     @rtype: list
2769     @return: the list of allowed source formats for I{suite}
2770     """
2771
2772     q = session.query(SrcFormat)
2773     q = q.join(SuiteSrcFormat)
2774     q = q.join(Suite).filter_by(suite_name=suite)
2775     q = q.order_by('format_name')
2776
2777     return q.all()
2778
2779 __all__.append('get_suite_src_formats')
2780
2781 ################################################################################
2782
2783 class Uid(ORMObject):
2784     def __init__(self, uid = None, name = None):
2785         self.uid = uid
2786         self.name = name
2787
2788     def __eq__(self, val):
2789         if isinstance(val, str):
2790             return (self.uid == val)
2791         # This signals to use the normal comparison operator
2792         return NotImplemented
2793
2794     def __ne__(self, val):
2795         if isinstance(val, str):
2796             return (self.uid != val)
2797         # This signals to use the normal comparison operator
2798         return NotImplemented
2799
2800     def properties(self):
2801         return ['uid', 'name', 'fingerprint']
2802
2803     def not_null_constraints(self):
2804         return ['uid']
2805
2806 __all__.append('Uid')
2807
2808 @session_wrapper
2809 def get_or_set_uid(uidname, session=None):
2810     """
2811     Returns uid object for given uidname.
2812
2813     If no matching uidname is found, a row is inserted.
2814
2815     @type uidname: string
2816     @param uidname: The uid to add
2817
2818     @type session: SQLAlchemy
2819     @param session: Optional SQL session object (a temporary one will be
2820     generated if not supplied).  If not passed, a commit will be performed at
2821     the end of the function, otherwise the caller is responsible for commiting.
2822
2823     @rtype: Uid
2824     @return: the uid object for the given uidname
2825     """
2826
2827     q = session.query(Uid).filter_by(uid=uidname)
2828
2829     try:
2830         ret = q.one()
2831     except NoResultFound:
2832         uid = Uid()
2833         uid.uid = uidname
2834         session.add(uid)
2835         session.commit_or_flush()
2836         ret = uid
2837
2838     return ret
2839
2840 __all__.append('get_or_set_uid')
2841
2842 @session_wrapper
2843 def get_uid_from_fingerprint(fpr, session=None):
2844     q = session.query(Uid)
2845     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
2846
2847     try:
2848         return q.one()
2849     except NoResultFound:
2850         return None
2851
2852 __all__.append('get_uid_from_fingerprint')
2853
2854 ################################################################################
2855
2856 class UploadBlock(object):
2857     def __init__(self, *args, **kwargs):
2858         pass
2859
2860     def __repr__(self):
2861         return '<UploadBlock %s (%s)>' % (self.source, self.upload_block_id)
2862
2863 __all__.append('UploadBlock')
2864
2865 ################################################################################
2866
2867 class DBConn(object):
2868     """
2869     database module init.
2870     """
2871     __shared_state = {}
2872
2873     def __init__(self, *args, **kwargs):
2874         self.__dict__ = self.__shared_state
2875
2876         if not getattr(self, 'initialised', False):
2877             self.initialised = True
2878             self.debug = kwargs.has_key('debug')
2879             self.__createconn()
2880
2881     def __setuptables(self):
2882         tables_with_primary = (
2883             'architecture',
2884             'archive',
2885             'bin_associations',
2886             'binaries',
2887             'binary_acl',
2888             'binary_acl_map',
2889             'build_queue',
2890             'build_queue_files',
2891             'changelogs_text',
2892             'changes',
2893             'component',
2894             'config',
2895             'changes_pending_binaries',
2896             'changes_pending_files',
2897             'changes_pending_source',
2898             'dsc_files',
2899             'files',
2900             'fingerprint',
2901             'keyrings',
2902             'keyring_acl_map',
2903             'location',
2904             'maintainer',
2905             'new_comments',
2906             'override_type',
2907             'pending_bin_contents',
2908             'policy_queue',
2909             'priority',
2910             'section',
2911             'source',
2912             'source_acl',
2913             'src_associations',
2914             'src_format',
2915             'src_uploaders',
2916             'suite',
2917             'uid',
2918             'upload_blocks',
2919         )
2920
2921         tables_no_primary = (
2922             'changes_pending_files_map',
2923             'changes_pending_source_files',
2924             'changes_pool_files',
2925             'deb_contents',
2926             'override',
2927             'suite_architectures',
2928             'suite_src_formats',
2929             'suite_build_queue_copy',
2930             'udeb_contents',
2931         )
2932
2933         views = (
2934             'almost_obsolete_all_associations',
2935             'almost_obsolete_src_associations',
2936             'any_associations_source',
2937             'bin_assoc_by_arch',
2938             'bin_associations_binaries',
2939             'binaries_suite_arch',
2940             'binfiles_suite_component_arch',
2941             'changelogs',
2942             'file_arch_suite',
2943             'newest_all_associations',
2944             'newest_any_associations',
2945             'newest_source',
2946             'newest_src_association',
2947             'obsolete_all_associations',
2948             'obsolete_any_associations',
2949             'obsolete_any_by_all_associations',
2950             'obsolete_src_associations',
2951             'source_suite',
2952             'src_associations_bin',
2953             'src_associations_src',
2954             'suite_arch_by_name',
2955         )
2956
2957         # Sqlalchemy version 0.5 fails to reflect the SERIAL type
2958         # correctly and that is why we have to use a workaround. It can
2959         # be removed as soon as we switch to version 0.6.
2960         for table_name in tables_with_primary:
2961             table = Table(table_name, self.db_meta, \
2962                 Column('id', Integer, primary_key = True), \
2963                 autoload=True, useexisting=True)
2964             setattr(self, 'tbl_%s' % table_name, table)
2965
2966         for table_name in tables_no_primary:
2967             table = Table(table_name, self.db_meta, autoload=True)
2968             setattr(self, 'tbl_%s' % table_name, table)
2969
2970         # bin_contents needs special attention until update #41 has been
2971         # applied
2972         self.tbl_bin_contents = Table('bin_contents', self.db_meta, \
2973             Column('file', Text, primary_key = True),
2974             Column('binary_id', Integer, ForeignKey('binaries.id'), \
2975                 primary_key = True),
2976             autoload=True, useexisting=True)
2977
2978         for view_name in views:
2979             view = Table(view_name, self.db_meta, autoload=True)
2980             setattr(self, 'view_%s' % view_name, view)
2981
2982     def __setupmappers(self):
2983         mapper(Architecture, self.tbl_architecture,
2984             properties = dict(arch_id = self.tbl_architecture.c.id,
2985                suites = relation(Suite, secondary=self.tbl_suite_architectures,
2986                    order_by='suite_name',
2987                    backref=backref('architectures', order_by='arch_string'))),
2988             extension = validator)
2989
2990         mapper(Archive, self.tbl_archive,
2991                properties = dict(archive_id = self.tbl_archive.c.id,
2992                                  archive_name = self.tbl_archive.c.name))
2993
2994         mapper(PendingBinContents, self.tbl_pending_bin_contents,
2995                properties = dict(contents_id =self.tbl_pending_bin_contents.c.id,
2996                                  filename = self.tbl_pending_bin_contents.c.filename,
2997                                  package = self.tbl_pending_bin_contents.c.package,
2998                                  version = self.tbl_pending_bin_contents.c.version,
2999                                  arch = self.tbl_pending_bin_contents.c.arch,
3000                                  otype = self.tbl_pending_bin_contents.c.type))
3001
3002         mapper(DebContents, self.tbl_deb_contents,
3003                properties = dict(binary_id=self.tbl_deb_contents.c.binary_id,
3004                                  package=self.tbl_deb_contents.c.package,
3005                                  suite=self.tbl_deb_contents.c.suite,
3006                                  arch=self.tbl_deb_contents.c.arch,
3007                                  section=self.tbl_deb_contents.c.section,
3008                                  filename=self.tbl_deb_contents.c.filename))
3009
3010         mapper(UdebContents, self.tbl_udeb_contents,
3011                properties = dict(binary_id=self.tbl_udeb_contents.c.binary_id,
3012                                  package=self.tbl_udeb_contents.c.package,
3013                                  suite=self.tbl_udeb_contents.c.suite,
3014                                  arch=self.tbl_udeb_contents.c.arch,
3015                                  section=self.tbl_udeb_contents.c.section,
3016                                  filename=self.tbl_udeb_contents.c.filename))
3017
3018         mapper(BuildQueue, self.tbl_build_queue,
3019                properties = dict(queue_id = self.tbl_build_queue.c.id))
3020
3021         mapper(BuildQueueFile, self.tbl_build_queue_files,
3022                properties = dict(buildqueue = relation(BuildQueue, backref='queuefiles'),
3023                                  poolfile = relation(PoolFile, backref='buildqueueinstances')))
3024
3025         mapper(DBBinary, self.tbl_binaries,
3026                properties = dict(binary_id = self.tbl_binaries.c.id,
3027                                  package = self.tbl_binaries.c.package,
3028                                  version = self.tbl_binaries.c.version,
3029                                  maintainer_id = self.tbl_binaries.c.maintainer,
3030                                  maintainer = relation(Maintainer),
3031                                  source_id = self.tbl_binaries.c.source,
3032                                  source = relation(DBSource, backref='binaries'),
3033                                  arch_id = self.tbl_binaries.c.architecture,
3034                                  architecture = relation(Architecture),
3035                                  poolfile_id = self.tbl_binaries.c.file,
3036                                  poolfile = relation(PoolFile, backref=backref('binary', uselist = False)),
3037                                  binarytype = self.tbl_binaries.c.type,
3038                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
3039                                  fingerprint = relation(Fingerprint),
3040                                  install_date = self.tbl_binaries.c.install_date,
3041                                  suites = relation(Suite, secondary=self.tbl_bin_associations,
3042                                      backref=backref('binaries', lazy='dynamic'))),
3043                 extension = validator)
3044
3045         mapper(BinaryACL, self.tbl_binary_acl,
3046                properties = dict(binary_acl_id = self.tbl_binary_acl.c.id))
3047
3048         mapper(BinaryACLMap, self.tbl_binary_acl_map,
3049                properties = dict(binary_acl_map_id = self.tbl_binary_acl_map.c.id,
3050                                  fingerprint = relation(Fingerprint, backref="binary_acl_map"),
3051                                  architecture = relation(Architecture)))
3052
3053         mapper(Component, self.tbl_component,
3054                properties = dict(component_id = self.tbl_component.c.id,
3055                                  component_name = self.tbl_component.c.name),
3056                extension = validator)
3057
3058         mapper(DBConfig, self.tbl_config,
3059                properties = dict(config_id = self.tbl_config.c.id))
3060
3061         mapper(DSCFile, self.tbl_dsc_files,
3062                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
3063                                  source_id = self.tbl_dsc_files.c.source,
3064                                  source = relation(DBSource),
3065                                  poolfile_id = self.tbl_dsc_files.c.file,
3066                                  poolfile = relation(PoolFile)))
3067
3068         mapper(PoolFile, self.tbl_files,
3069                properties = dict(file_id = self.tbl_files.c.id,
3070                                  filesize = self.tbl_files.c.size,
3071                                  location_id = self.tbl_files.c.location,
3072                                  location = relation(Location,
3073                                      # using lazy='dynamic' in the back
3074                                      # reference because we have A LOT of
3075                                      # files in one location
3076                                      backref=backref('files', lazy='dynamic'))),
3077                 extension = validator)
3078
3079         mapper(Fingerprint, self.tbl_fingerprint,
3080                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
3081                                  uid_id = self.tbl_fingerprint.c.uid,
3082                                  uid = relation(Uid),
3083                                  keyring_id = self.tbl_fingerprint.c.keyring,
3084                                  keyring = relation(Keyring),
3085                                  source_acl = relation(SourceACL),
3086                                  binary_acl = relation(BinaryACL)),
3087                extension = validator)
3088
3089         mapper(Keyring, self.tbl_keyrings,
3090                properties = dict(keyring_name = self.tbl_keyrings.c.name,
3091                                  keyring_id = self.tbl_keyrings.c.id))
3092
3093         mapper(DBChange, self.tbl_changes,
3094                properties = dict(change_id = self.tbl_changes.c.id,
3095                                  poolfiles = relation(PoolFile,
3096                                                       secondary=self.tbl_changes_pool_files,
3097                                                       backref="changeslinks"),
3098                                  seen = self.tbl_changes.c.seen,
3099                                  source = self.tbl_changes.c.source,
3100                                  binaries = self.tbl_changes.c.binaries,
3101                                  architecture = self.tbl_changes.c.architecture,
3102                                  distribution = self.tbl_changes.c.distribution,
3103                                  urgency = self.tbl_changes.c.urgency,
3104                                  maintainer = self.tbl_changes.c.maintainer,
3105                                  changedby = self.tbl_changes.c.changedby,
3106                                  date = self.tbl_changes.c.date,
3107                                  version = self.tbl_changes.c.version,
3108                                  files = relation(ChangePendingFile,
3109                                                   secondary=self.tbl_changes_pending_files_map,
3110                                                   backref="changesfile"),
3111                                  in_queue_id = self.tbl_changes.c.in_queue,
3112                                  in_queue = relation(PolicyQueue,
3113                                                      primaryjoin=(self.tbl_changes.c.in_queue==self.tbl_policy_queue.c.id)),
3114                                  approved_for_id = self.tbl_changes.c.approved_for))
3115
3116         mapper(ChangePendingBinary, self.tbl_changes_pending_binaries,
3117                properties = dict(change_pending_binary_id = self.tbl_changes_pending_binaries.c.id))
3118
3119         mapper(ChangePendingFile, self.tbl_changes_pending_files,
3120                properties = dict(change_pending_file_id = self.tbl_changes_pending_files.c.id,
3121                                  filename = self.tbl_changes_pending_files.c.filename,
3122                                  size = self.tbl_changes_pending_files.c.size,
3123                                  md5sum = self.tbl_changes_pending_files.c.md5sum,
3124                                  sha1sum = self.tbl_changes_pending_files.c.sha1sum,
3125                                  sha256sum = self.tbl_changes_pending_files.c.sha256sum))
3126
3127         mapper(ChangePendingSource, self.tbl_changes_pending_source,
3128                properties = dict(change_pending_source_id = self.tbl_changes_pending_source.c.id,
3129                                  change = relation(DBChange),
3130                                  maintainer = relation(Maintainer,
3131                                                        primaryjoin=(self.tbl_changes_pending_source.c.maintainer_id==self.tbl_maintainer.c.id)),
3132                                  changedby = relation(Maintainer,
3133                                                       primaryjoin=(self.tbl_changes_pending_source.c.changedby_id==self.tbl_maintainer.c.id)),
3134                                  fingerprint = relation(Fingerprint),
3135                                  source_files = relation(ChangePendingFile,
3136                                                          secondary=self.tbl_changes_pending_source_files,
3137                                                          backref="pending_sources")))
3138
3139
3140         mapper(KeyringACLMap, self.tbl_keyring_acl_map,
3141                properties = dict(keyring_acl_map_id = self.tbl_keyring_acl_map.c.id,
3142                                  keyring = relation(Keyring, backref="keyring_acl_map"),
3143                                  architecture = relation(Architecture)))
3144
3145         mapper(Location, self.tbl_location,
3146                properties = dict(location_id = self.tbl_location.c.id,
3147                                  component_id = self.tbl_location.c.component,
3148                                  component = relation(Component, backref='location'),
3149                                  archive_id = self.tbl_location.c.archive,
3150                                  archive = relation(Archive),
3151                                  # FIXME: the 'type' column is old cruft and
3152                                  # should be removed in the future.
3153                                  archive_type = self.tbl_location.c.type),
3154                extension = validator)
3155
3156         mapper(Maintainer, self.tbl_maintainer,
3157                properties = dict(maintainer_id = self.tbl_maintainer.c.id,
3158                    maintains_sources = relation(DBSource, backref='maintainer',
3159                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.maintainer)),
3160                    changed_sources = relation(DBSource, backref='changedby',
3161                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.changedby))),
3162                 extension = validator)
3163
3164         mapper(NewComment, self.tbl_new_comments,
3165                properties = dict(comment_id = self.tbl_new_comments.c.id))
3166
3167         mapper(Override, self.tbl_override,
3168                properties = dict(suite_id = self.tbl_override.c.suite,
3169                                  suite = relation(Suite, \
3170                                     backref=backref('overrides', lazy='dynamic')),
3171                                  package = self.tbl_override.c.package,
3172                                  component_id = self.tbl_override.c.component,
3173                                  component = relation(Component, \
3174                                     backref=backref('overrides', lazy='dynamic')),
3175                                  priority_id = self.tbl_override.c.priority,
3176                                  priority = relation(Priority, \
3177                                     backref=backref('overrides', lazy='dynamic')),
3178                                  section_id = self.tbl_override.c.section,
3179                                  section = relation(Section, \
3180                                     backref=backref('overrides', lazy='dynamic')),
3181                                  overridetype_id = self.tbl_override.c.type,
3182                                  overridetype = relation(OverrideType, \
3183                                     backref=backref('overrides', lazy='dynamic'))))
3184
3185         mapper(OverrideType, self.tbl_override_type,
3186                properties = dict(overridetype = self.tbl_override_type.c.type,
3187                                  overridetype_id = self.tbl_override_type.c.id))
3188
3189         mapper(PolicyQueue, self.tbl_policy_queue,
3190                properties = dict(policy_queue_id = self.tbl_policy_queue.c.id))
3191
3192         mapper(Priority, self.tbl_priority,
3193                properties = dict(priority_id = self.tbl_priority.c.id))
3194
3195         mapper(Section, self.tbl_section,
3196                properties = dict(section_id = self.tbl_section.c.id,
3197                                  section=self.tbl_section.c.section))
3198
3199         mapper(DBSource, self.tbl_source,
3200                properties = dict(source_id = self.tbl_source.c.id,
3201                                  version = self.tbl_source.c.version,
3202                                  maintainer_id = self.tbl_source.c.maintainer,
3203                                  poolfile_id = self.tbl_source.c.file,
3204                                  poolfile = relation(PoolFile, backref=backref('source', uselist = False)),
3205                                  fingerprint_id = self.tbl_source.c.sig_fpr,
3206                                  fingerprint = relation(Fingerprint),
3207                                  changedby_id = self.tbl_source.c.changedby,
3208                                  srcfiles = relation(DSCFile,
3209                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
3210                                  suites = relation(Suite, secondary=self.tbl_src_associations,
3211                                      backref=backref('sources', lazy='dynamic')),
3212                                  srcuploaders = relation(SrcUploader)),
3213                extension = validator)
3214
3215         mapper(SourceACL, self.tbl_source_acl,
3216                properties = dict(source_acl_id = self.tbl_source_acl.c.id))
3217
3218         mapper(SrcFormat, self.tbl_src_format,
3219                properties = dict(src_format_id = self.tbl_src_format.c.id,
3220                                  format_name = self.tbl_src_format.c.format_name))
3221
3222         mapper(SrcUploader, self.tbl_src_uploaders,
3223                properties = dict(uploader_id = self.tbl_src_uploaders.c.id,
3224                                  source_id = self.tbl_src_uploaders.c.source,
3225                                  source = relation(DBSource,
3226                                                    primaryjoin=(self.tbl_src_uploaders.c.source==self.tbl_source.c.id)),
3227                                  maintainer_id = self.tbl_src_uploaders.c.maintainer,
3228                                  maintainer = relation(Maintainer,
3229                                                        primaryjoin=(self.tbl_src_uploaders.c.maintainer==self.tbl_maintainer.c.id))))
3230
3231         mapper(Suite, self.tbl_suite,
3232                properties = dict(suite_id = self.tbl_suite.c.id,
3233                                  policy_queue = relation(PolicyQueue),
3234                                  copy_queues = relation(BuildQueue,
3235                                      secondary=self.tbl_suite_build_queue_copy)),
3236                 extension = validator)
3237
3238         mapper(SuiteSrcFormat, self.tbl_suite_src_formats,
3239                properties = dict(suite_id = self.tbl_suite_src_formats.c.suite,
3240                                  suite = relation(Suite, backref='suitesrcformats'),
3241                                  src_format_id = self.tbl_suite_src_formats.c.src_format,
3242                                  src_format = relation(SrcFormat)))
3243
3244         mapper(Uid, self.tbl_uid,
3245                properties = dict(uid_id = self.tbl_uid.c.id,
3246                                  fingerprint = relation(Fingerprint)),
3247                extension = validator)
3248
3249         mapper(UploadBlock, self.tbl_upload_blocks,
3250                properties = dict(upload_block_id = self.tbl_upload_blocks.c.id,
3251                                  fingerprint = relation(Fingerprint, backref="uploadblocks"),
3252                                  uid = relation(Uid, backref="uploadblocks")))
3253
3254         mapper(BinContents, self.tbl_bin_contents,
3255             properties = dict(
3256                 binary = relation(DBBinary,
3257                     backref=backref('contents', lazy='dynamic')),
3258                 file = self.tbl_bin_contents.c.file))
3259
3260     ## Connection functions
3261     def __createconn(self):
3262         from config import Config
3263         cnf = Config()
3264         if cnf["DB::Host"]:
3265             # TCP/IP
3266             connstr = "postgres://%s" % cnf["DB::Host"]
3267             if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
3268                 connstr += ":%s" % cnf["DB::Port"]
3269             connstr += "/%s" % cnf["DB::Name"]
3270         else:
3271             # Unix Socket
3272             connstr = "postgres:///%s" % cnf["DB::Name"]
3273             if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
3274                 connstr += "?port=%s" % cnf["DB::Port"]
3275
3276         self.db_pg   = create_engine(connstr, echo=self.debug)
3277         self.db_meta = MetaData()
3278         self.db_meta.bind = self.db_pg
3279         self.db_smaker = sessionmaker(bind=self.db_pg,
3280                                       autoflush=True,
3281                                       autocommit=False)
3282
3283         self.__setuptables()
3284         self.__setupmappers()
3285
3286     def session(self):
3287         return self.db_smaker()
3288
3289 __all__.append('DBConn')
3290
3291