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