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