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