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