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