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