]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
Start directory creation
[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     # If Changed-By isn't available, fall back to maintainer
2612     if u.pkg.changes.has_key("changed-by"):
2613         source.changedby_id = get_or_set_maintainer(u.pkg.changes["changed-by"], session).maintainer_id
2614     else:
2615         source.changedby_id = get_or_set_maintainer(u.pkg.dsc["maintainer"], session).maintainer_id
2616     source.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2617     source.install_date = datetime.now().date()
2618
2619     dsc_component = entry["component"]
2620     dsc_location_id = entry["location id"]
2621
2622     source.dm_upload_allowed = (u.pkg.dsc.get("dm-upload-allowed", '') == "yes")
2623
2624     # Set up a new poolfile if necessary
2625     if not entry.has_key("files id") or not entry["files id"]:
2626         filename = entry["pool name"] + filename
2627         poolfile = add_poolfile(filename, entry, dsc_location_id, session)
2628         session.flush()
2629         pfs.append(poolfile)
2630         entry["files id"] = poolfile.file_id
2631
2632     source.poolfile_id = entry["files id"]
2633     session.add(source)
2634
2635     suite_names = u.pkg.changes["distribution"].keys()
2636     source.suites = session.query(Suite). \
2637         filter(Suite.suite_name.in_(suite_names)).all()
2638
2639     # Add the source files to the DB (files and dsc_files)
2640     dscfile = DSCFile()
2641     dscfile.source_id = source.source_id
2642     dscfile.poolfile_id = entry["files id"]
2643     session.add(dscfile)
2644
2645     for dsc_file, dentry in u.pkg.dsc_files.items():
2646         df = DSCFile()
2647         df.source_id = source.source_id
2648
2649         # If the .orig tarball is already in the pool, it's
2650         # files id is stored in dsc_files by check_dsc().
2651         files_id = dentry.get("files id", None)
2652
2653         # Find the entry in the files hash
2654         # TODO: Bail out here properly
2655         dfentry = None
2656         for f, e in u.pkg.files.items():
2657             if f == dsc_file:
2658                 dfentry = e
2659                 break
2660
2661         if files_id is None:
2662             filename = dfentry["pool name"] + dsc_file
2663
2664             (found, obj) = check_poolfile(filename, dentry["size"], dentry["md5sum"], dsc_location_id)
2665             # FIXME: needs to check for -1/-2 and or handle exception
2666             if found and obj is not None:
2667                 files_id = obj.file_id
2668                 pfs.append(obj)
2669
2670             # If still not found, add it
2671             if files_id is None:
2672                 # HACK: Force sha1sum etc into dentry
2673                 dentry["sha1sum"] = dfentry["sha1sum"]
2674                 dentry["sha256sum"] = dfentry["sha256sum"]
2675                 poolfile = add_poolfile(filename, dentry, dsc_location_id, session)
2676                 pfs.append(poolfile)
2677                 files_id = poolfile.file_id
2678         else:
2679             poolfile = get_poolfile_by_id(files_id, session)
2680             if poolfile is None:
2681                 utils.fubar("INTERNAL ERROR. Found no poolfile with id %d" % files_id)
2682             pfs.append(poolfile)
2683
2684         df.poolfile_id = files_id
2685         session.add(df)
2686
2687     # Add the src_uploaders to the DB
2688     session.flush()
2689     session.refresh(source)
2690     source.uploaders = [source.maintainer]
2691     if u.pkg.dsc.has_key("uploaders"):
2692         for up in u.pkg.dsc["uploaders"].replace(">, ", ">\t").split("\t"):
2693             up = up.strip()
2694             source.uploaders.append(get_or_set_maintainer(up, session))
2695
2696     session.flush()
2697
2698     return source, dsc_component, dsc_location_id, pfs
2699
2700 __all__.append('add_dsc_to_db')
2701
2702 @session_wrapper
2703 def add_deb_to_db(u, filename, session=None):
2704     """
2705     Contrary to what you might expect, this routine deals with both
2706     debs and udebs.  That info is in 'dbtype', whilst 'type' is
2707     'deb' for both of them
2708     """
2709     cnf = Config()
2710     entry = u.pkg.files[filename]
2711
2712     bin = DBBinary()
2713     bin.package = entry["package"]
2714     bin.version = entry["version"]
2715     bin.maintainer_id = get_or_set_maintainer(entry["maintainer"], session).maintainer_id
2716     bin.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2717     bin.arch_id = get_architecture(entry["architecture"], session).arch_id
2718     bin.binarytype = entry["dbtype"]
2719
2720     # Find poolfile id
2721     filename = entry["pool name"] + filename
2722     fullpath = os.path.join(cnf["Dir::Pool"], filename)
2723     if not entry.get("location id", None):
2724         entry["location id"] = get_location(cnf["Dir::Pool"], entry["component"], session=session).location_id
2725
2726     if entry.get("files id", None):
2727         poolfile = get_poolfile_by_id(bin.poolfile_id)
2728         bin.poolfile_id = entry["files id"]
2729     else:
2730         poolfile = add_poolfile(filename, entry, entry["location id"], session)
2731         bin.poolfile_id = entry["files id"] = poolfile.file_id
2732
2733     # Find source id
2734     bin_sources = get_sources_from_name(entry["source package"], entry["source version"], session=session)
2735     if len(bin_sources) != 1:
2736         raise NoSourceFieldError, "Unable to find a unique source id for %s (%s), %s, file %s, type %s, signed by %s" % \
2737                                   (bin.package, bin.version, entry["architecture"],
2738                                    filename, bin.binarytype, u.pkg.changes["fingerprint"])
2739
2740     bin.source_id = bin_sources[0].source_id
2741
2742     if entry.has_key("built-using"):
2743         for srcname, version in entry["built-using"]:
2744             exsources = get_sources_from_name(srcname, version, session=session)
2745             if len(exsources) != 1:
2746                 raise NoSourceFieldError, "Unable to find source package (%s = %s) in Built-Using for %s (%s), %s, file %s, type %s, signed by %s" % \
2747                                           (srcname, version, bin.package, bin.version, entry["architecture"],
2748                                            filename, bin.binarytype, u.pkg.changes["fingerprint"])
2749
2750             bin.extra_sources.append(exsources[0])
2751
2752     # Add and flush object so it has an ID
2753     session.add(bin)
2754
2755     suite_names = u.pkg.changes["distribution"].keys()
2756     bin.suites = session.query(Suite). \
2757         filter(Suite.suite_name.in_(suite_names)).all()
2758
2759     session.flush()
2760
2761     # Deal with contents - disabled for now
2762     #contents = copy_temporary_contents(bin.package, bin.version, bin.architecture.arch_string, os.path.basename(filename), None, session)
2763     #if not contents:
2764     #    print "REJECT\nCould not determine contents of package %s" % bin.package
2765     #    session.rollback()
2766     #    raise MissingContents, "No contents stored for package %s, and couldn't determine contents of %s" % (bin.package, filename)
2767
2768     return bin, poolfile
2769
2770 __all__.append('add_deb_to_db')
2771
2772 ################################################################################
2773
2774 class SourceACL(object):
2775     def __init__(self, *args, **kwargs):
2776         pass
2777
2778     def __repr__(self):
2779         return '<SourceACL %s>' % self.source_acl_id
2780
2781 __all__.append('SourceACL')
2782
2783 ################################################################################
2784
2785 class SrcFormat(object):
2786     def __init__(self, *args, **kwargs):
2787         pass
2788
2789     def __repr__(self):
2790         return '<SrcFormat %s>' % (self.format_name)
2791
2792 __all__.append('SrcFormat')
2793
2794 ################################################################################
2795
2796 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
2797                  ('SuiteID', 'suite_id'),
2798                  ('Version', 'version'),
2799                  ('Origin', 'origin'),
2800                  ('Label', 'label'),
2801                  ('Description', 'description'),
2802                  ('Untouchable', 'untouchable'),
2803                  ('Announce', 'announce'),
2804                  ('Codename', 'codename'),
2805                  ('OverrideCodename', 'overridecodename'),
2806                  ('ValidTime', 'validtime'),
2807                  ('Priority', 'priority'),
2808                  ('NotAutomatic', 'notautomatic'),
2809                  ('CopyChanges', 'copychanges'),
2810                  ('OverrideSuite', 'overridesuite')]
2811
2812 # Why the heck don't we have any UNIQUE constraints in table suite?
2813 # TODO: Add UNIQUE constraints for appropriate columns.
2814 class Suite(ORMObject):
2815     def __init__(self, suite_name = None, version = None):
2816         self.suite_name = suite_name
2817         self.version = version
2818
2819     def properties(self):
2820         return ['suite_name', 'version', 'sources_count', 'binaries_count', \
2821             'overrides_count']
2822
2823     def not_null_constraints(self):
2824         return ['suite_name']
2825
2826     def __eq__(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 __ne__(self, val):
2833         if isinstance(val, str):
2834             return (self.suite_name != val)
2835         # This signals to use the normal comparison operator
2836         return NotImplemented
2837
2838     def details(self):
2839         ret = []
2840         for disp, field in SUITE_FIELDS:
2841             val = getattr(self, field, None)
2842             if val is not None:
2843                 ret.append("%s: %s" % (disp, val))
2844
2845         return "\n".join(ret)
2846
2847     def get_architectures(self, skipsrc=False, skipall=False):
2848         """
2849         Returns list of Architecture objects
2850
2851         @type skipsrc: boolean
2852         @param skipsrc: Whether to skip returning the 'source' architecture entry
2853         (Default False)
2854
2855         @type skipall: boolean
2856         @param skipall: Whether to skip returning the 'all' architecture entry
2857         (Default False)
2858
2859         @rtype: list
2860         @return: list of Architecture objects for the given name (may be empty)
2861         """
2862
2863         q = object_session(self).query(Architecture).with_parent(self)
2864         if skipsrc:
2865             q = q.filter(Architecture.arch_string != 'source')
2866         if skipall:
2867             q = q.filter(Architecture.arch_string != 'all')
2868         return q.order_by(Architecture.arch_string).all()
2869
2870     def get_sources(self, source):
2871         """
2872         Returns a query object representing DBSource that is part of C{suite}.
2873
2874           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2875
2876         @type source: string
2877         @param source: source package name
2878
2879         @rtype: sqlalchemy.orm.query.Query
2880         @return: a query of DBSource
2881
2882         """
2883
2884         session = object_session(self)
2885         return session.query(DBSource).filter_by(source = source). \
2886             with_parent(self)
2887
2888     def get_overridesuite(self):
2889         if self.overridesuite is None:
2890             return self
2891         else:
2892             return object_session(self).query(Suite).filter_by(suite_name=self.overridesuite).one()
2893
2894 __all__.append('Suite')
2895
2896 @session_wrapper
2897 def get_suite(suite, session=None):
2898     """
2899     Returns Suite object for given C{suite name}.
2900
2901     @type suite: string
2902     @param suite: The name of the suite
2903
2904     @type session: Session
2905     @param session: Optional SQLA session object (a temporary one will be
2906     generated if not supplied)
2907
2908     @rtype: Suite
2909     @return: Suite object for the requested suite name (None if not present)
2910     """
2911
2912     q = session.query(Suite).filter_by(suite_name=suite)
2913
2914     try:
2915         return q.one()
2916     except NoResultFound:
2917         return None
2918
2919 __all__.append('get_suite')
2920
2921 ################################################################################
2922
2923 # TODO: should be removed because the implementation is too trivial
2924 @session_wrapper
2925 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
2926     """
2927     Returns list of Architecture objects for given C{suite} name
2928
2929     @type suite: str
2930     @param suite: Suite name to search for
2931
2932     @type skipsrc: boolean
2933     @param skipsrc: Whether to skip returning the 'source' architecture entry
2934     (Default False)
2935
2936     @type skipall: boolean
2937     @param skipall: Whether to skip returning the 'all' architecture entry
2938     (Default False)
2939
2940     @type session: Session
2941     @param session: Optional SQL session object (a temporary one will be
2942     generated if not supplied)
2943
2944     @rtype: list
2945     @return: list of Architecture objects for the given name (may be empty)
2946     """
2947
2948     return get_suite(suite, session).get_architectures(skipsrc, skipall)
2949
2950 __all__.append('get_suite_architectures')
2951
2952 ################################################################################
2953
2954 class SuiteSrcFormat(object):
2955     def __init__(self, *args, **kwargs):
2956         pass
2957
2958     def __repr__(self):
2959         return '<SuiteSrcFormat (%s, %s)>' % (self.suite_id, self.src_format_id)
2960
2961 __all__.append('SuiteSrcFormat')
2962
2963 @session_wrapper
2964 def get_suite_src_formats(suite, session=None):
2965     """
2966     Returns list of allowed SrcFormat for C{suite}.
2967
2968     @type suite: str
2969     @param suite: Suite name to search for
2970
2971     @type session: Session
2972     @param session: Optional SQL session object (a temporary one will be
2973     generated if not supplied)
2974
2975     @rtype: list
2976     @return: the list of allowed source formats for I{suite}
2977     """
2978
2979     q = session.query(SrcFormat)
2980     q = q.join(SuiteSrcFormat)
2981     q = q.join(Suite).filter_by(suite_name=suite)
2982     q = q.order_by('format_name')
2983
2984     return q.all()
2985
2986 __all__.append('get_suite_src_formats')
2987
2988 ################################################################################
2989
2990 class Uid(ORMObject):
2991     def __init__(self, uid = None, name = None):
2992         self.uid = uid
2993         self.name = name
2994
2995     def __eq__(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 __ne__(self, val):
3002         if isinstance(val, str):
3003             return (self.uid != val)
3004         # This signals to use the normal comparison operator
3005         return NotImplemented
3006
3007     def properties(self):
3008         return ['uid', 'name', 'fingerprint']
3009
3010     def not_null_constraints(self):
3011         return ['uid']
3012
3013 __all__.append('Uid')
3014
3015 @session_wrapper
3016 def get_or_set_uid(uidname, session=None):
3017     """
3018     Returns uid object for given uidname.
3019
3020     If no matching uidname is found, a row is inserted.
3021
3022     @type uidname: string
3023     @param uidname: The uid to add
3024
3025     @type session: SQLAlchemy
3026     @param session: Optional SQL session object (a temporary one will be
3027     generated if not supplied).  If not passed, a commit will be performed at
3028     the end of the function, otherwise the caller is responsible for commiting.
3029
3030     @rtype: Uid
3031     @return: the uid object for the given uidname
3032     """
3033
3034     q = session.query(Uid).filter_by(uid=uidname)
3035
3036     try:
3037         ret = q.one()
3038     except NoResultFound:
3039         uid = Uid()
3040         uid.uid = uidname
3041         session.add(uid)
3042         session.commit_or_flush()
3043         ret = uid
3044
3045     return ret
3046
3047 __all__.append('get_or_set_uid')
3048
3049 @session_wrapper
3050 def get_uid_from_fingerprint(fpr, session=None):
3051     q = session.query(Uid)
3052     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
3053
3054     try:
3055         return q.one()
3056     except NoResultFound:
3057         return None
3058
3059 __all__.append('get_uid_from_fingerprint')
3060
3061 ################################################################################
3062
3063 class UploadBlock(object):
3064     def __init__(self, *args, **kwargs):
3065         pass
3066
3067     def __repr__(self):
3068         return '<UploadBlock %s (%s)>' % (self.source, self.upload_block_id)
3069
3070 __all__.append('UploadBlock')
3071
3072 ################################################################################
3073
3074 class MetadataKey(ORMObject):
3075     def __init__(self, key = None):
3076         self.key = key
3077
3078     def properties(self):
3079         return ['key']
3080
3081     def not_null_constraints(self):
3082         return ['key']
3083
3084 __all__.append('MetadataKey')
3085
3086 @session_wrapper
3087 def get_or_set_metadatakey(keyname, session=None):
3088     """
3089     Returns MetadataKey object for given uidname.
3090
3091     If no matching keyname is found, a row is inserted.
3092
3093     @type uidname: string
3094     @param uidname: The keyname to add
3095
3096     @type session: SQLAlchemy
3097     @param session: Optional SQL session object (a temporary one will be
3098     generated if not supplied).  If not passed, a commit will be performed at
3099     the end of the function, otherwise the caller is responsible for commiting.
3100
3101     @rtype: MetadataKey
3102     @return: the metadatakey object for the given keyname
3103     """
3104
3105     q = session.query(MetadataKey).filter_by(key=keyname)
3106
3107     try:
3108         ret = q.one()
3109     except NoResultFound:
3110         ret = MetadataKey(keyname)
3111         session.add(ret)
3112         session.commit_or_flush()
3113
3114     return ret
3115
3116 __all__.append('get_or_set_metadatakey')
3117
3118 ################################################################################
3119
3120 class BinaryMetadata(ORMObject):
3121     def __init__(self, key = None, value = None, binary = None):
3122         self.key = key
3123         self.value = value
3124         self.binary = binary
3125
3126     def properties(self):
3127         return ['binary', 'key', 'value']
3128
3129     def not_null_constraints(self):
3130         return ['value']
3131
3132 __all__.append('BinaryMetadata')
3133
3134 ################################################################################
3135
3136 class SourceMetadata(ORMObject):
3137     def __init__(self, key = None, value = None, source = None):
3138         self.key = key
3139         self.value = value
3140         self.source = source
3141
3142     def properties(self):
3143         return ['source', 'key', 'value']
3144
3145     def not_null_constraints(self):
3146         return ['value']
3147
3148 __all__.append('SourceMetadata')
3149
3150 ################################################################################
3151
3152 class VersionCheck(ORMObject):
3153     def __init__(self, *args, **kwargs):
3154         pass
3155
3156     def properties(self):
3157         #return ['suite_id', 'check', 'reference_id']
3158         return ['check']
3159
3160     def not_null_constraints(self):
3161         return ['suite', 'check', 'reference']
3162
3163 __all__.append('VersionCheck')
3164
3165 @session_wrapper
3166 def get_version_checks(suite_name, check = None, session = None):
3167     suite = get_suite(suite_name, session)
3168     if not suite:
3169         # Make sure that what we return is iterable so that list comprehensions
3170         # involving this don't cause a traceback
3171         return []
3172     q = session.query(VersionCheck).filter_by(suite=suite)
3173     if check:
3174         q = q.filter_by(check=check)
3175     return q.all()
3176
3177 __all__.append('get_version_checks')
3178
3179 ################################################################################
3180
3181 class DBConn(object):
3182     """
3183     database module init.
3184     """
3185     __shared_state = {}
3186
3187     def __init__(self, *args, **kwargs):
3188         self.__dict__ = self.__shared_state
3189
3190         if not getattr(self, 'initialised', False):
3191             self.initialised = True
3192             self.debug = kwargs.has_key('debug')
3193             self.__createconn()
3194
3195     def __setuptables(self):
3196         tables = (
3197             'architecture',
3198             'archive',
3199             'bin_associations',
3200             'bin_contents',
3201             'binaries',
3202             'binaries_metadata',
3203             'binary_acl',
3204             'binary_acl_map',
3205             'build_queue',
3206             'build_queue_files',
3207             'build_queue_policy_files',
3208             'changelogs_text',
3209             'changes',
3210             'component',
3211             'config',
3212             'changes_pending_binaries',
3213             'changes_pending_files',
3214             'changes_pending_source',
3215             'changes_pending_files_map',
3216             'changes_pending_source_files',
3217             'changes_pool_files',
3218             'dsc_files',
3219             'external_overrides',
3220             'extra_src_references',
3221             'files',
3222             'fingerprint',
3223             'keyrings',
3224             'keyring_acl_map',
3225             'location',
3226             'maintainer',
3227             'metadata_keys',
3228             'new_comments',
3229             # TODO: the maintainer column in table override should be removed.
3230             'override',
3231             'override_type',
3232             'policy_queue',
3233             'priority',
3234             'section',
3235             'source',
3236             'source_acl',
3237             'source_metadata',
3238             'src_associations',
3239             'src_contents',
3240             'src_format',
3241             'src_uploaders',
3242             'suite',
3243             'suite_architectures',
3244             'suite_build_queue_copy',
3245             'suite_src_formats',
3246             'uid',
3247             'upload_blocks',
3248             'version_check',
3249         )
3250
3251         views = (
3252             'almost_obsolete_all_associations',
3253             'almost_obsolete_src_associations',
3254             'any_associations_source',
3255             'bin_associations_binaries',
3256             'binaries_suite_arch',
3257             'binfiles_suite_component_arch',
3258             'changelogs',
3259             'file_arch_suite',
3260             'newest_all_associations',
3261             'newest_any_associations',
3262             'newest_source',
3263             'newest_src_association',
3264             'obsolete_all_associations',
3265             'obsolete_any_associations',
3266             'obsolete_any_by_all_associations',
3267             'obsolete_src_associations',
3268             'source_suite',
3269             'src_associations_bin',
3270             'src_associations_src',
3271             'suite_arch_by_name',
3272         )
3273
3274         for table_name in tables:
3275             table = Table(table_name, self.db_meta, \
3276                 autoload=True, useexisting=True)
3277             setattr(self, 'tbl_%s' % table_name, table)
3278
3279         for view_name in views:
3280             view = Table(view_name, self.db_meta, autoload=True)
3281             setattr(self, 'view_%s' % view_name, view)
3282
3283     def __setupmappers(self):
3284         mapper(Architecture, self.tbl_architecture,
3285             properties = dict(arch_id = self.tbl_architecture.c.id,
3286                suites = relation(Suite, secondary=self.tbl_suite_architectures,
3287                    order_by='suite_name',
3288                    backref=backref('architectures', order_by='arch_string'))),
3289             extension = validator)
3290
3291         mapper(Archive, self.tbl_archive,
3292                properties = dict(archive_id = self.tbl_archive.c.id,
3293                                  archive_name = self.tbl_archive.c.name))
3294
3295         mapper(BuildQueue, self.tbl_build_queue,
3296                properties = dict(queue_id = self.tbl_build_queue.c.id))
3297
3298         mapper(BuildQueueFile, self.tbl_build_queue_files,
3299                properties = dict(buildqueue = relation(BuildQueue, backref='queuefiles'),
3300                                  poolfile = relation(PoolFile, backref='buildqueueinstances')))
3301
3302         mapper(BuildQueuePolicyFile, self.tbl_build_queue_policy_files,
3303                properties = dict(
3304                 build_queue = relation(BuildQueue, backref='policy_queue_files'),
3305                 file = relation(ChangePendingFile, lazy='joined')))
3306
3307         mapper(DBBinary, self.tbl_binaries,
3308                properties = dict(binary_id = self.tbl_binaries.c.id,
3309                                  package = self.tbl_binaries.c.package,
3310                                  version = self.tbl_binaries.c.version,
3311                                  maintainer_id = self.tbl_binaries.c.maintainer,
3312                                  maintainer = relation(Maintainer),
3313                                  source_id = self.tbl_binaries.c.source,
3314                                  source = relation(DBSource, backref='binaries'),
3315                                  arch_id = self.tbl_binaries.c.architecture,
3316                                  architecture = relation(Architecture),
3317                                  poolfile_id = self.tbl_binaries.c.file,
3318                                  poolfile = relation(PoolFile, backref=backref('binary', uselist = False)),
3319                                  binarytype = self.tbl_binaries.c.type,
3320                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
3321                                  fingerprint = relation(Fingerprint),
3322                                  install_date = self.tbl_binaries.c.install_date,
3323                                  suites = relation(Suite, secondary=self.tbl_bin_associations,
3324                                      backref=backref('binaries', lazy='dynamic')),
3325                                  extra_sources = relation(DBSource, secondary=self.tbl_extra_src_references,
3326                                      backref=backref('extra_binary_references', lazy='dynamic')),
3327                                  key = relation(BinaryMetadata, cascade='all',
3328                                      collection_class=attribute_mapped_collection('key'))),
3329                 extension = validator)
3330
3331         mapper(BinaryACL, self.tbl_binary_acl,
3332                properties = dict(binary_acl_id = self.tbl_binary_acl.c.id))
3333
3334         mapper(BinaryACLMap, self.tbl_binary_acl_map,
3335                properties = dict(binary_acl_map_id = self.tbl_binary_acl_map.c.id,
3336                                  fingerprint = relation(Fingerprint, backref="binary_acl_map"),
3337                                  architecture = relation(Architecture)))
3338
3339         mapper(Component, self.tbl_component,
3340                properties = dict(component_id = self.tbl_component.c.id,
3341                                  component_name = self.tbl_component.c.name),
3342                extension = validator)
3343
3344         mapper(DBConfig, self.tbl_config,
3345                properties = dict(config_id = self.tbl_config.c.id))
3346
3347         mapper(DSCFile, self.tbl_dsc_files,
3348                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
3349                                  source_id = self.tbl_dsc_files.c.source,
3350                                  source = relation(DBSource),
3351                                  poolfile_id = self.tbl_dsc_files.c.file,
3352                                  poolfile = relation(PoolFile)))
3353
3354         mapper(ExternalOverride, self.tbl_external_overrides,
3355                 properties = dict(
3356                     suite_id = self.tbl_external_overrides.c.suite,
3357                     suite = relation(Suite),
3358                     component_id = self.tbl_external_overrides.c.component,
3359                     component = relation(Component)))
3360
3361         mapper(PoolFile, self.tbl_files,
3362                properties = dict(file_id = self.tbl_files.c.id,
3363                                  filesize = self.tbl_files.c.size,
3364                                  location_id = self.tbl_files.c.location,
3365                                  location = relation(Location,
3366                                      # using lazy='dynamic' in the back
3367                                      # reference because we have A LOT of
3368                                      # files in one location
3369                                      backref=backref('files', lazy='dynamic'))),
3370                 extension = validator)
3371
3372         mapper(Fingerprint, self.tbl_fingerprint,
3373                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
3374                                  uid_id = self.tbl_fingerprint.c.uid,
3375                                  uid = relation(Uid),
3376                                  keyring_id = self.tbl_fingerprint.c.keyring,
3377                                  keyring = relation(Keyring),
3378                                  source_acl = relation(SourceACL),
3379                                  binary_acl = relation(BinaryACL)),
3380                extension = validator)
3381
3382         mapper(Keyring, self.tbl_keyrings,
3383                properties = dict(keyring_name = self.tbl_keyrings.c.name,
3384                                  keyring_id = self.tbl_keyrings.c.id))
3385
3386         mapper(DBChange, self.tbl_changes,
3387                properties = dict(change_id = self.tbl_changes.c.id,
3388                                  poolfiles = relation(PoolFile,
3389                                                       secondary=self.tbl_changes_pool_files,
3390                                                       backref="changeslinks"),
3391                                  seen = self.tbl_changes.c.seen,
3392                                  source = self.tbl_changes.c.source,
3393                                  binaries = self.tbl_changes.c.binaries,
3394                                  architecture = self.tbl_changes.c.architecture,
3395                                  distribution = self.tbl_changes.c.distribution,
3396                                  urgency = self.tbl_changes.c.urgency,
3397                                  maintainer = self.tbl_changes.c.maintainer,
3398                                  changedby = self.tbl_changes.c.changedby,
3399                                  date = self.tbl_changes.c.date,
3400                                  version = self.tbl_changes.c.version,
3401                                  files = relation(ChangePendingFile,
3402                                                   secondary=self.tbl_changes_pending_files_map,
3403                                                   backref="changesfile"),
3404                                  in_queue_id = self.tbl_changes.c.in_queue,
3405                                  in_queue = relation(PolicyQueue,
3406                                                      primaryjoin=(self.tbl_changes.c.in_queue==self.tbl_policy_queue.c.id)),
3407                                  approved_for_id = self.tbl_changes.c.approved_for))
3408
3409         mapper(ChangePendingBinary, self.tbl_changes_pending_binaries,
3410                properties = dict(change_pending_binary_id = self.tbl_changes_pending_binaries.c.id))
3411
3412         mapper(ChangePendingFile, self.tbl_changes_pending_files,
3413                properties = dict(change_pending_file_id = self.tbl_changes_pending_files.c.id,
3414                                  filename = self.tbl_changes_pending_files.c.filename,
3415                                  size = self.tbl_changes_pending_files.c.size,
3416                                  md5sum = self.tbl_changes_pending_files.c.md5sum,
3417                                  sha1sum = self.tbl_changes_pending_files.c.sha1sum,
3418                                  sha256sum = self.tbl_changes_pending_files.c.sha256sum))
3419
3420         mapper(ChangePendingSource, self.tbl_changes_pending_source,
3421                properties = dict(change_pending_source_id = self.tbl_changes_pending_source.c.id,
3422                                  change = relation(DBChange),
3423                                  maintainer = relation(Maintainer,
3424                                                        primaryjoin=(self.tbl_changes_pending_source.c.maintainer_id==self.tbl_maintainer.c.id)),
3425                                  changedby = relation(Maintainer,
3426                                                       primaryjoin=(self.tbl_changes_pending_source.c.changedby_id==self.tbl_maintainer.c.id)),
3427                                  fingerprint = relation(Fingerprint),
3428                                  source_files = relation(ChangePendingFile,
3429                                                          secondary=self.tbl_changes_pending_source_files,
3430                                                          backref="pending_sources")))
3431
3432
3433         mapper(KeyringACLMap, self.tbl_keyring_acl_map,
3434                properties = dict(keyring_acl_map_id = self.tbl_keyring_acl_map.c.id,
3435                                  keyring = relation(Keyring, backref="keyring_acl_map"),
3436                                  architecture = relation(Architecture)))
3437
3438         mapper(Location, self.tbl_location,
3439                properties = dict(location_id = self.tbl_location.c.id,
3440                                  component_id = self.tbl_location.c.component,
3441                                  component = relation(Component, backref='location'),
3442                                  archive_id = self.tbl_location.c.archive,
3443                                  archive = relation(Archive),
3444                                  # FIXME: the 'type' column is old cruft and
3445                                  # should be removed in the future.
3446                                  archive_type = self.tbl_location.c.type),
3447                extension = validator)
3448
3449         mapper(Maintainer, self.tbl_maintainer,
3450                properties = dict(maintainer_id = self.tbl_maintainer.c.id,
3451                    maintains_sources = relation(DBSource, backref='maintainer',
3452                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.maintainer)),
3453                    changed_sources = relation(DBSource, backref='changedby',
3454                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.changedby))),
3455                 extension = validator)
3456
3457         mapper(NewComment, self.tbl_new_comments,
3458                properties = dict(comment_id = self.tbl_new_comments.c.id))
3459
3460         mapper(Override, self.tbl_override,
3461                properties = dict(suite_id = self.tbl_override.c.suite,
3462                                  suite = relation(Suite, \
3463                                     backref=backref('overrides', lazy='dynamic')),
3464                                  package = self.tbl_override.c.package,
3465                                  component_id = self.tbl_override.c.component,
3466                                  component = relation(Component, \
3467                                     backref=backref('overrides', lazy='dynamic')),
3468                                  priority_id = self.tbl_override.c.priority,
3469                                  priority = relation(Priority, \
3470                                     backref=backref('overrides', lazy='dynamic')),
3471                                  section_id = self.tbl_override.c.section,
3472                                  section = relation(Section, \
3473                                     backref=backref('overrides', lazy='dynamic')),
3474                                  overridetype_id = self.tbl_override.c.type,
3475                                  overridetype = relation(OverrideType, \
3476                                     backref=backref('overrides', lazy='dynamic'))))
3477
3478         mapper(OverrideType, self.tbl_override_type,
3479                properties = dict(overridetype = self.tbl_override_type.c.type,
3480                                  overridetype_id = self.tbl_override_type.c.id))
3481
3482         mapper(PolicyQueue, self.tbl_policy_queue,
3483                properties = dict(policy_queue_id = self.tbl_policy_queue.c.id))
3484
3485         mapper(Priority, self.tbl_priority,
3486                properties = dict(priority_id = self.tbl_priority.c.id))
3487
3488         mapper(Section, self.tbl_section,
3489                properties = dict(section_id = self.tbl_section.c.id,
3490                                  section=self.tbl_section.c.section))
3491
3492         mapper(DBSource, self.tbl_source,
3493                properties = dict(source_id = self.tbl_source.c.id,
3494                                  version = self.tbl_source.c.version,
3495                                  maintainer_id = self.tbl_source.c.maintainer,
3496                                  poolfile_id = self.tbl_source.c.file,
3497                                  poolfile = relation(PoolFile, backref=backref('source', uselist = False)),
3498                                  fingerprint_id = self.tbl_source.c.sig_fpr,
3499                                  fingerprint = relation(Fingerprint),
3500                                  changedby_id = self.tbl_source.c.changedby,
3501                                  srcfiles = relation(DSCFile,
3502                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
3503                                  suites = relation(Suite, secondary=self.tbl_src_associations,
3504                                      backref=backref('sources', lazy='dynamic')),
3505                                  uploaders = relation(Maintainer,
3506                                      secondary=self.tbl_src_uploaders),
3507                                  key = relation(SourceMetadata, cascade='all',
3508                                      collection_class=attribute_mapped_collection('key'))),
3509                extension = validator)
3510
3511         mapper(SourceACL, self.tbl_source_acl,
3512                properties = dict(source_acl_id = self.tbl_source_acl.c.id))
3513
3514         mapper(SrcFormat, self.tbl_src_format,
3515                properties = dict(src_format_id = self.tbl_src_format.c.id,
3516                                  format_name = self.tbl_src_format.c.format_name))
3517
3518         mapper(Suite, self.tbl_suite,
3519                properties = dict(suite_id = self.tbl_suite.c.id,
3520                                  policy_queue = relation(PolicyQueue),
3521                                  copy_queues = relation(BuildQueue,
3522                                      secondary=self.tbl_suite_build_queue_copy)),
3523                 extension = validator)
3524
3525         mapper(SuiteSrcFormat, self.tbl_suite_src_formats,
3526                properties = dict(suite_id = self.tbl_suite_src_formats.c.suite,
3527                                  suite = relation(Suite, backref='suitesrcformats'),
3528                                  src_format_id = self.tbl_suite_src_formats.c.src_format,
3529                                  src_format = relation(SrcFormat)))
3530
3531         mapper(Uid, self.tbl_uid,
3532                properties = dict(uid_id = self.tbl_uid.c.id,
3533                                  fingerprint = relation(Fingerprint)),
3534                extension = validator)
3535
3536         mapper(UploadBlock, self.tbl_upload_blocks,
3537                properties = dict(upload_block_id = self.tbl_upload_blocks.c.id,
3538                                  fingerprint = relation(Fingerprint, backref="uploadblocks"),
3539                                  uid = relation(Uid, backref="uploadblocks")))
3540
3541         mapper(BinContents, self.tbl_bin_contents,
3542             properties = dict(
3543                 binary = relation(DBBinary,
3544                     backref=backref('contents', lazy='dynamic', cascade='all')),
3545                 file = self.tbl_bin_contents.c.file))
3546
3547         mapper(SrcContents, self.tbl_src_contents,
3548             properties = dict(
3549                 source = relation(DBSource,
3550                     backref=backref('contents', lazy='dynamic', cascade='all')),
3551                 file = self.tbl_src_contents.c.file))
3552
3553         mapper(MetadataKey, self.tbl_metadata_keys,
3554             properties = dict(
3555                 key_id = self.tbl_metadata_keys.c.key_id,
3556                 key = self.tbl_metadata_keys.c.key))
3557
3558         mapper(BinaryMetadata, self.tbl_binaries_metadata,
3559             properties = dict(
3560                 binary_id = self.tbl_binaries_metadata.c.bin_id,
3561                 binary = relation(DBBinary),
3562                 key_id = self.tbl_binaries_metadata.c.key_id,
3563                 key = relation(MetadataKey),
3564                 value = self.tbl_binaries_metadata.c.value))
3565
3566         mapper(SourceMetadata, self.tbl_source_metadata,
3567             properties = dict(
3568                 source_id = self.tbl_source_metadata.c.src_id,
3569                 source = relation(DBSource),
3570                 key_id = self.tbl_source_metadata.c.key_id,
3571                 key = relation(MetadataKey),
3572                 value = self.tbl_source_metadata.c.value))
3573
3574         mapper(VersionCheck, self.tbl_version_check,
3575             properties = dict(
3576                 suite_id = self.tbl_version_check.c.suite,
3577                 suite = relation(Suite, primaryjoin=self.tbl_version_check.c.suite==self.tbl_suite.c.id),
3578                 reference_id = self.tbl_version_check.c.reference,
3579                 reference = relation(Suite, primaryjoin=self.tbl_version_check.c.reference==self.tbl_suite.c.id, lazy='joined')))
3580
3581     ## Connection functions
3582     def __createconn(self):
3583         from config import Config
3584         cnf = Config()
3585         if cnf.has_key("DB::Service"):
3586             connstr = "postgresql://service=%s" % cnf["DB::Service"]
3587         elif cnf.has_key("DB::Host"):
3588             # TCP/IP
3589             connstr = "postgresql://%s" % cnf["DB::Host"]
3590             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
3591                 connstr += ":%s" % cnf["DB::Port"]
3592             connstr += "/%s" % cnf["DB::Name"]
3593         else:
3594             # Unix Socket
3595             connstr = "postgresql:///%s" % cnf["DB::Name"]
3596             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
3597                 connstr += "?port=%s" % cnf["DB::Port"]
3598
3599         engine_args = { 'echo': self.debug }
3600         if cnf.has_key('DB::PoolSize'):
3601             engine_args['pool_size'] = int(cnf['DB::PoolSize'])
3602         if cnf.has_key('DB::MaxOverflow'):
3603             engine_args['max_overflow'] = int(cnf['DB::MaxOverflow'])
3604         if sa_major_version == '0.6' and cnf.has_key('DB::Unicode') and \
3605             cnf['DB::Unicode'] == 'false':
3606             engine_args['use_native_unicode'] = False
3607
3608         # Monkey patch a new dialect in in order to support service= syntax
3609         import sqlalchemy.dialects.postgresql
3610         from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
3611         class PGDialect_psycopg2_dak(PGDialect_psycopg2):
3612             def create_connect_args(self, url):
3613                 if str(url).startswith('postgresql://service='):
3614                     # Eww
3615                     servicename = str(url)[21:]
3616                     return (['service=%s' % servicename], {})
3617                 else:
3618                     return PGDialect_psycopg2.create_connect_args(self, url)
3619
3620         sqlalchemy.dialects.postgresql.base.dialect = PGDialect_psycopg2_dak
3621
3622         self.db_pg   = create_engine(connstr, **engine_args)
3623         self.db_meta = MetaData()
3624         self.db_meta.bind = self.db_pg
3625         self.db_smaker = sessionmaker(bind=self.db_pg,
3626                                       autoflush=True,
3627                                       autocommit=False)
3628
3629         self.__setuptables()
3630         self.__setupmappers()
3631         self.pid = os.getpid()
3632
3633     def session(self, work_mem = 0):
3634         '''
3635         Returns a new session object. If a work_mem parameter is provided a new
3636         transaction is started and the work_mem parameter is set for this
3637         transaction. The work_mem parameter is measured in MB. A default value
3638         will be used if the parameter is not set.
3639         '''
3640         # reinitialize DBConn in new processes
3641         if self.pid != os.getpid():
3642             clear_mappers()
3643             self.__createconn()
3644         session = self.db_smaker()
3645         if work_mem > 0:
3646             session.execute("SET LOCAL work_mem TO '%d MB'" % work_mem)
3647         return session
3648
3649 __all__.append('DBConn')
3650
3651