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