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