]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
daklib/dbconn.py: DBBinary.get_component_name is gone, use PoolFile.component instead
[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 from daklib.gpg import SignedFile
46
47 try:
48     # python >= 2.6
49     import json
50 except:
51     # python <= 2.5
52     import simplejson as json
53
54 from datetime import datetime, timedelta
55 from errno import ENOENT
56 from tempfile import mkstemp, mkdtemp
57 from subprocess import Popen, PIPE
58 from tarfile import TarFile
59
60 from inspect import getargspec
61
62 import sqlalchemy
63 from sqlalchemy import create_engine, Table, MetaData, Column, Integer, desc, \
64     Text, ForeignKey
65 from sqlalchemy.orm import sessionmaker, mapper, relation, object_session, \
66     backref, MapperExtension, EXT_CONTINUE, object_mapper, clear_mappers
67 from sqlalchemy import types as sqltypes
68 from sqlalchemy.orm.collections import attribute_mapped_collection
69 from sqlalchemy.ext.associationproxy import association_proxy
70
71 # Don't remove this, we re-export the exceptions to scripts which import us
72 from sqlalchemy.exc import *
73 from sqlalchemy.orm.exc import NoResultFound
74
75 # Only import Config until Queue stuff is changed to store its config
76 # in the database
77 from config import Config
78 from textutils import fix_maintainer
79 from dak_exceptions import DBUpdateError, NoSourceFieldError, FileExistsError
80
81 # suppress some deprecation warnings in squeeze related to sqlalchemy
82 import warnings
83 warnings.filterwarnings('ignore', \
84     "The SQLAlchemy PostgreSQL dialect has been renamed from 'postgres' to 'postgresql'.*", \
85     SADeprecationWarning)
86 warnings.filterwarnings('ignore', \
87     "Predicate of partial index .* ignored during reflection", \
88     SAWarning)
89
90
91 ################################################################################
92
93 # Patch in support for the debversion field type so that it works during
94 # reflection
95
96 try:
97     # that is for sqlalchemy 0.6
98     UserDefinedType = sqltypes.UserDefinedType
99 except:
100     # this one for sqlalchemy 0.5
101     UserDefinedType = sqltypes.TypeEngine
102
103 class DebVersion(UserDefinedType):
104     def get_col_spec(self):
105         return "DEBVERSION"
106
107     def bind_processor(self, dialect):
108         return None
109
110     # ' = None' is needed for sqlalchemy 0.5:
111     def result_processor(self, dialect, coltype = None):
112         return None
113
114 sa_major_version = sqlalchemy.__version__[0:3]
115 if sa_major_version in ["0.5", "0.6", "0.7"]:
116     from sqlalchemy.databases import postgres
117     postgres.ischema_names['debversion'] = DebVersion
118 else:
119     raise Exception("dak only ported to SQLA versions 0.5 to 0.7.  See daklib/dbconn.py")
120
121 ################################################################################
122
123 __all__ = ['IntegrityError', 'SQLAlchemyError', 'DebVersion']
124
125 ################################################################################
126
127 def session_wrapper(fn):
128     """
129     Wrapper around common ".., session=None):" handling. If the wrapped
130     function is called without passing 'session', we create a local one
131     and destroy it when the function ends.
132
133     Also attaches a commit_or_flush method to the session; if we created a
134     local session, this is a synonym for session.commit(), otherwise it is a
135     synonym for session.flush().
136     """
137
138     def wrapped(*args, **kwargs):
139         private_transaction = False
140
141         # Find the session object
142         session = kwargs.get('session')
143
144         if session is None:
145             if len(args) <= len(getargspec(fn)[0]) - 1:
146                 # No session specified as last argument or in kwargs
147                 private_transaction = True
148                 session = kwargs['session'] = DBConn().session()
149             else:
150                 # Session is last argument in args
151                 session = args[-1]
152                 if session is None:
153                     args = list(args)
154                     session = args[-1] = DBConn().session()
155                     private_transaction = True
156
157         if private_transaction:
158             session.commit_or_flush = session.commit
159         else:
160             session.commit_or_flush = session.flush
161
162         try:
163             return fn(*args, **kwargs)
164         finally:
165             if private_transaction:
166                 # We created a session; close it.
167                 session.close()
168
169     wrapped.__doc__ = fn.__doc__
170     wrapped.func_name = fn.func_name
171
172     return wrapped
173
174 __all__.append('session_wrapper')
175
176 ################################################################################
177
178 class ORMObject(object):
179     """
180     ORMObject is a base class for all ORM classes mapped by SQLalchemy. All
181     derived classes must implement the properties() method.
182     """
183
184     def properties(self):
185         '''
186         This method should be implemented by all derived classes and returns a
187         list of the important properties. The properties 'created' and
188         'modified' will be added automatically. A suffix '_count' should be
189         added to properties that are lists or query objects. The most important
190         property name should be returned as the first element in the list
191         because it is used by repr().
192         '''
193         return []
194
195     def json(self):
196         '''
197         Returns a JSON representation of the object based on the properties
198         returned from the properties() method.
199         '''
200         data = {}
201         # add created and modified
202         all_properties = self.properties() + ['created', 'modified']
203         for property in all_properties:
204             # check for list or query
205             if property[-6:] == '_count':
206                 real_property = property[:-6]
207                 if not hasattr(self, real_property):
208                     continue
209                 value = getattr(self, real_property)
210                 if hasattr(value, '__len__'):
211                     # list
212                     value = len(value)
213                 elif hasattr(value, 'count'):
214                     # query (but not during validation)
215                     if self.in_validation:
216                         continue
217                     value = value.count()
218                 else:
219                     raise KeyError('Do not understand property %s.' % property)
220             else:
221                 if not hasattr(self, property):
222                     continue
223                 # plain object
224                 value = getattr(self, property)
225                 if value is None:
226                     # skip None
227                     continue
228                 elif isinstance(value, ORMObject):
229                     # use repr() for ORMObject types
230                     value = repr(value)
231                 else:
232                     # we want a string for all other types because json cannot
233                     # encode everything
234                     value = str(value)
235             data[property] = value
236         return json.dumps(data)
237
238     def classname(self):
239         '''
240         Returns the name of the class.
241         '''
242         return type(self).__name__
243
244     def __repr__(self):
245         '''
246         Returns a short string representation of the object using the first
247         element from the properties() method.
248         '''
249         primary_property = self.properties()[0]
250         value = getattr(self, primary_property)
251         return '<%s %s>' % (self.classname(), str(value))
252
253     def __str__(self):
254         '''
255         Returns a human readable form of the object using the properties()
256         method.
257         '''
258         return '<%s %s>' % (self.classname(), self.json())
259
260     def not_null_constraints(self):
261         '''
262         Returns a list of properties that must be not NULL. Derived classes
263         should override this method if needed.
264         '''
265         return []
266
267     validation_message = \
268         "Validation failed because property '%s' must not be empty in object\n%s"
269
270     in_validation = False
271
272     def validate(self):
273         '''
274         This function validates the not NULL constraints as returned by
275         not_null_constraints(). It raises the DBUpdateError exception if
276         validation fails.
277         '''
278         for property in self.not_null_constraints():
279             # TODO: It is a bit awkward that the mapper configuration allow
280             # directly setting the numeric _id columns. We should get rid of it
281             # in the long run.
282             if hasattr(self, property + '_id') and \
283                 getattr(self, property + '_id') is not None:
284                 continue
285             if not hasattr(self, property) or getattr(self, property) is None:
286                 # str() might lead to races due to a 2nd flush
287                 self.in_validation = True
288                 message = self.validation_message % (property, str(self))
289                 self.in_validation = False
290                 raise DBUpdateError(message)
291
292     @classmethod
293     @session_wrapper
294     def get(cls, primary_key,  session = None):
295         '''
296         This is a support function that allows getting an object by its primary
297         key.
298
299         Architecture.get(3[, session])
300
301         instead of the more verbose
302
303         session.query(Architecture).get(3)
304         '''
305         return session.query(cls).get(primary_key)
306
307     def session(self, replace = False):
308         '''
309         Returns the current session that is associated with the object. May
310         return None is object is in detached state.
311         '''
312
313         return object_session(self)
314
315     def clone(self, session = None):
316         '''
317         Clones the current object in a new session and returns the new clone. A
318         fresh session is created if the optional session parameter is not
319         provided. The function will fail if a session is provided and has
320         unflushed changes.
321
322         RATIONALE: SQLAlchemy's session is not thread safe. This method clones
323         an existing object to allow several threads to work with their own
324         instances of an ORMObject.
325
326         WARNING: Only persistent (committed) objects can be cloned. Changes
327         made to the original object that are not committed yet will get lost.
328         The session of the new object will always be rolled back to avoid
329         ressource leaks.
330         '''
331
332         if self.session() is None:
333             raise RuntimeError( \
334                 'Method clone() failed for detached object:\n%s' % self)
335         self.session().flush()
336         mapper = object_mapper(self)
337         primary_key = mapper.primary_key_from_instance(self)
338         object_class = self.__class__
339         if session is None:
340             session = DBConn().session()
341         elif len(session.new) + len(session.dirty) + len(session.deleted) > 0:
342             raise RuntimeError( \
343                 'Method clone() failed due to unflushed changes in session.')
344         new_object = session.query(object_class).get(primary_key)
345         session.rollback()
346         if new_object is None:
347             raise RuntimeError( \
348                 'Method clone() failed for non-persistent object:\n%s' % self)
349         return new_object
350
351 __all__.append('ORMObject')
352
353 ################################################################################
354
355 class Validator(MapperExtension):
356     '''
357     This class calls the validate() method for each instance for the
358     'before_update' and 'before_insert' events. A global object validator is
359     used for configuring the individual mappers.
360     '''
361
362     def before_update(self, mapper, connection, instance):
363         instance.validate()
364         return EXT_CONTINUE
365
366     def before_insert(self, mapper, connection, instance):
367         instance.validate()
368         return EXT_CONTINUE
369
370 validator = Validator()
371
372 ################################################################################
373
374 class Architecture(ORMObject):
375     def __init__(self, arch_string = None, description = None):
376         self.arch_string = arch_string
377         self.description = description
378
379     def __eq__(self, val):
380         if isinstance(val, str):
381             return (self.arch_string== val)
382         # This signals to use the normal comparison operator
383         return NotImplemented
384
385     def __ne__(self, val):
386         if isinstance(val, str):
387             return (self.arch_string != val)
388         # This signals to use the normal comparison operator
389         return NotImplemented
390
391     def properties(self):
392         return ['arch_string', 'arch_id', 'suites_count']
393
394     def not_null_constraints(self):
395         return ['arch_string']
396
397 __all__.append('Architecture')
398
399 @session_wrapper
400 def get_architecture(architecture, session=None):
401     """
402     Returns database id for given C{architecture}.
403
404     @type architecture: string
405     @param architecture: The name of the architecture
406
407     @type session: Session
408     @param session: Optional SQLA session object (a temporary one will be
409     generated if not supplied)
410
411     @rtype: Architecture
412     @return: Architecture object for the given arch (None if not present)
413     """
414
415     q = session.query(Architecture).filter_by(arch_string=architecture)
416
417     try:
418         return q.one()
419     except NoResultFound:
420         return None
421
422 __all__.append('get_architecture')
423
424 # TODO: should be removed because the implementation is too trivial
425 @session_wrapper
426 def get_architecture_suites(architecture, session=None):
427     """
428     Returns list of Suite objects for given C{architecture} name
429
430     @type architecture: str
431     @param architecture: Architecture name to search for
432
433     @type session: Session
434     @param session: Optional SQL session object (a temporary one will be
435     generated if not supplied)
436
437     @rtype: list
438     @return: list of Suite objects for the given name (may be empty)
439     """
440
441     return get_architecture(architecture, session).suites
442
443 __all__.append('get_architecture_suites')
444
445 ################################################################################
446
447 class Archive(object):
448     def __init__(self, *args, **kwargs):
449         pass
450
451     def __repr__(self):
452         return '<Archive %s>' % self.archive_name
453
454 __all__.append('Archive')
455
456 @session_wrapper
457 def get_archive(archive, session=None):
458     """
459     returns database id for given C{archive}.
460
461     @type archive: string
462     @param archive: the name of the arhive
463
464     @type session: Session
465     @param session: Optional SQLA session object (a temporary one will be
466     generated if not supplied)
467
468     @rtype: Archive
469     @return: Archive object for the given name (None if not present)
470
471     """
472     archive = archive.lower()
473
474     q = session.query(Archive).filter_by(archive_name=archive)
475
476     try:
477         return q.one()
478     except NoResultFound:
479         return None
480
481 __all__.append('get_archive')
482
483 ################################################################################
484
485 class ArchiveFile(object):
486     def __init__(self, archive=None, component=None, file=None):
487         self.archive = archive
488         self.component = component
489         self.file = file
490     @property
491     def path(self):
492         return os.path.join(self.archive.path, 'pool', self.component.component_name, self.file.filename)
493
494 __all__.append('ArchiveFile')
495
496 ################################################################################
497
498 class BinContents(ORMObject):
499     def __init__(self, file = None, binary = None):
500         self.file = file
501         self.binary = binary
502
503     def properties(self):
504         return ['file', 'binary']
505
506 __all__.append('BinContents')
507
508 ################################################################################
509
510 def subprocess_setup():
511     # Python installs a SIGPIPE handler by default. This is usually not what
512     # non-Python subprocesses expect.
513     signal.signal(signal.SIGPIPE, signal.SIG_DFL)
514
515 class DBBinary(ORMObject):
516     def __init__(self, package = None, source = None, version = None, \
517         maintainer = None, architecture = None, poolfile = None, \
518         binarytype = 'deb', fingerprint=None):
519         self.package = package
520         self.source = source
521         self.version = version
522         self.maintainer = maintainer
523         self.architecture = architecture
524         self.poolfile = poolfile
525         self.binarytype = binarytype
526         self.fingerprint = fingerprint
527
528     @property
529     def pkid(self):
530         return self.binary_id
531
532     def properties(self):
533         return ['package', 'version', 'maintainer', 'source', 'architecture', \
534             'poolfile', 'binarytype', 'fingerprint', 'install_date', \
535             'suites_count', 'binary_id', 'contents_count', 'extra_sources']
536
537     def not_null_constraints(self):
538         return ['package', 'version', 'maintainer', 'source',  'poolfile', \
539             'binarytype']
540
541     metadata = association_proxy('key', 'value')
542
543     def scan_contents(self):
544         '''
545         Yields the contents of the package. Only regular files are yielded and
546         the path names are normalized after converting them from either utf-8
547         or iso8859-1 encoding. It yields the string ' <EMPTY PACKAGE>' if the
548         package does not contain any regular file.
549         '''
550         fullpath = self.poolfile.fullpath
551         dpkg = Popen(['dpkg-deb', '--fsys-tarfile', fullpath], stdout = PIPE,
552             preexec_fn = subprocess_setup)
553         tar = TarFile.open(fileobj = dpkg.stdout, mode = 'r|')
554         for member in tar.getmembers():
555             if not member.isdir():
556                 name = normpath(member.name)
557                 # enforce proper utf-8 encoding
558                 try:
559                     name.decode('utf-8')
560                 except UnicodeDecodeError:
561                     name = name.decode('iso8859-1').encode('utf-8')
562                 yield name
563         tar.close()
564         dpkg.stdout.close()
565         dpkg.wait()
566
567     def read_control(self):
568         '''
569         Reads the control information from a binary.
570
571         @rtype: text
572         @return: stanza text of the control section.
573         '''
574         import utils
575         fullpath = self.poolfile.fullpath
576         deb_file = open(fullpath, 'r')
577         stanza = utils.deb_extract_control(deb_file)
578         deb_file.close()
579
580         return stanza
581
582     def read_control_fields(self):
583         '''
584         Reads the control information from a binary and return
585         as a dictionary.
586
587         @rtype: dict
588         @return: fields of the control section as a dictionary.
589         '''
590         import apt_pkg
591         stanza = self.read_control()
592         return apt_pkg.TagSection(stanza)
593
594 __all__.append('DBBinary')
595
596 @session_wrapper
597 def get_suites_binary_in(package, session=None):
598     """
599     Returns list of Suite objects which given C{package} name is in
600
601     @type package: str
602     @param package: DBBinary package name to search for
603
604     @rtype: list
605     @return: list of Suite objects for the given package
606     """
607
608     return session.query(Suite).filter(Suite.binaries.any(DBBinary.package == package)).all()
609
610 __all__.append('get_suites_binary_in')
611
612 @session_wrapper
613 def get_component_by_package_suite(package, suite_list, arch_list=[], session=None):
614     '''
615     Returns the component name of the newest binary package in suite_list or
616     None if no package is found. The result can be optionally filtered by a list
617     of architecture names.
618
619     @type package: str
620     @param package: DBBinary package name to search for
621
622     @type suite_list: list of str
623     @param suite_list: list of suite_name items
624
625     @type arch_list: list of str
626     @param arch_list: optional list of arch_string items that defaults to []
627
628     @rtype: str or NoneType
629     @return: name of component or None
630     '''
631
632     q = session.query(DBBinary).filter_by(package = package). \
633         join(DBBinary.suites).filter(Suite.suite_name.in_(suite_list))
634     if len(arch_list) > 0:
635         q = q.join(DBBinary.architecture). \
636             filter(Architecture.arch_string.in_(arch_list))
637     binary = q.order_by(desc(DBBinary.version)).first()
638     if binary is None:
639         return None
640     else:
641         return binary.poolfile.component.component_name
642
643 __all__.append('get_component_by_package_suite')
644
645 ################################################################################
646
647 class BinaryACL(object):
648     def __init__(self, *args, **kwargs):
649         pass
650
651     def __repr__(self):
652         return '<BinaryACL %s>' % self.binary_acl_id
653
654 __all__.append('BinaryACL')
655
656 ################################################################################
657
658 class BinaryACLMap(object):
659     def __init__(self, *args, **kwargs):
660         pass
661
662     def __repr__(self):
663         return '<BinaryACLMap %s>' % self.binary_acl_map_id
664
665 __all__.append('BinaryACLMap')
666
667 ################################################################################
668
669 class BuildQueue(object):
670     def __init__(self, *args, **kwargs):
671         pass
672
673     def __repr__(self):
674         return '<BuildQueue %s>' % self.queue_name
675
676 __all__.append('BuildQueue')
677
678 ################################################################################
679
680 class Component(ORMObject):
681     def __init__(self, component_name = None):
682         self.component_name = component_name
683
684     def __eq__(self, val):
685         if isinstance(val, str):
686             return (self.component_name == val)
687         # This signals to use the normal comparison operator
688         return NotImplemented
689
690     def __ne__(self, val):
691         if isinstance(val, str):
692             return (self.component_name != val)
693         # This signals to use the normal comparison operator
694         return NotImplemented
695
696     def properties(self):
697         return ['component_name', 'component_id', 'description', \
698             'meets_dfsg', 'overrides_count']
699
700     def not_null_constraints(self):
701         return ['component_name']
702
703
704 __all__.append('Component')
705
706 @session_wrapper
707 def get_component(component, session=None):
708     """
709     Returns database id for given C{component}.
710
711     @type component: string
712     @param component: The name of the override type
713
714     @rtype: int
715     @return: the database id for the given component
716
717     """
718     component = component.lower()
719
720     q = session.query(Component).filter_by(component_name=component)
721
722     try:
723         return q.one()
724     except NoResultFound:
725         return None
726
727 __all__.append('get_component')
728
729 @session_wrapper
730 def get_mapped_component(component_name, session=None):
731     """get component after mappings
732
733     Evaluate component mappings from ComponentMappings in dak.conf for the
734     given component name.
735
736     @todo: ansgar wants to get rid of this. It's currently only used for
737            the security archive
738
739     @type  component_name: str
740     @param component_name: component name
741
742     @param session: database session
743
744     @rtype:  L{daklib.dbconn.Component} or C{None}
745     @return: component after applying maps or C{None}
746     """
747     cnf = Config()
748     for m in cnf.value_list("ComponentMappings"):
749         (src, dst) = m.split()
750         if component_name == src:
751             component_name = dst
752     component = session.query(Component).filter_by(component_name=component_name).first()
753     return component
754
755 __all__.append('get_mapped_component')
756
757 @session_wrapper
758 def get_component_names(session=None):
759     """
760     Returns list of strings of component names.
761
762     @rtype: list
763     @return: list of strings of component names
764     """
765
766     return [ x.component_name for x in session.query(Component).all() ]
767
768 __all__.append('get_component_names')
769
770 ################################################################################
771
772 class DBConfig(object):
773     def __init__(self, *args, **kwargs):
774         pass
775
776     def __repr__(self):
777         return '<DBConfig %s>' % self.name
778
779 __all__.append('DBConfig')
780
781 ################################################################################
782
783 @session_wrapper
784 def get_or_set_contents_file_id(filename, session=None):
785     """
786     Returns database id for given filename.
787
788     If no matching file is found, a row is inserted.
789
790     @type filename: string
791     @param filename: The filename
792     @type session: SQLAlchemy
793     @param session: Optional SQL session object (a temporary one will be
794     generated if not supplied).  If not passed, a commit will be performed at
795     the end of the function, otherwise the caller is responsible for commiting.
796
797     @rtype: int
798     @return: the database id for the given component
799     """
800
801     q = session.query(ContentFilename).filter_by(filename=filename)
802
803     try:
804         ret = q.one().cafilename_id
805     except NoResultFound:
806         cf = ContentFilename()
807         cf.filename = filename
808         session.add(cf)
809         session.commit_or_flush()
810         ret = cf.cafilename_id
811
812     return ret
813
814 __all__.append('get_or_set_contents_file_id')
815
816 @session_wrapper
817 def get_contents(suite, overridetype, section=None, session=None):
818     """
819     Returns contents for a suite / overridetype combination, limiting
820     to a section if not None.
821
822     @type suite: Suite
823     @param suite: Suite object
824
825     @type overridetype: OverrideType
826     @param overridetype: OverrideType object
827
828     @type section: Section
829     @param section: Optional section object to limit results to
830
831     @type session: SQLAlchemy
832     @param session: Optional SQL session object (a temporary one will be
833     generated if not supplied)
834
835     @rtype: ResultsProxy
836     @return: ResultsProxy object set up to return tuples of (filename, section,
837     package, arch_id)
838     """
839
840     # find me all of the contents for a given suite
841     contents_q = """SELECT (p.path||'/'||n.file) AS fn,
842                             s.section,
843                             b.package,
844                             b.architecture
845                    FROM content_associations c join content_file_paths p ON (c.filepath=p.id)
846                    JOIN content_file_names n ON (c.filename=n.id)
847                    JOIN binaries b ON (b.id=c.binary_pkg)
848                    JOIN override o ON (o.package=b.package)
849                    JOIN section s ON (s.id=o.section)
850                    WHERE o.suite = :suiteid AND o.type = :overridetypeid
851                    AND b.type=:overridetypename"""
852
853     vals = {'suiteid': suite.suite_id,
854             'overridetypeid': overridetype.overridetype_id,
855             'overridetypename': overridetype.overridetype}
856
857     if section is not None:
858         contents_q += " AND s.id = :sectionid"
859         vals['sectionid'] = section.section_id
860
861     contents_q += " ORDER BY fn"
862
863     return session.execute(contents_q, vals)
864
865 __all__.append('get_contents')
866
867 ################################################################################
868
869 class ContentFilepath(object):
870     def __init__(self, *args, **kwargs):
871         pass
872
873     def __repr__(self):
874         return '<ContentFilepath %s>' % self.filepath
875
876 __all__.append('ContentFilepath')
877
878 @session_wrapper
879 def get_or_set_contents_path_id(filepath, session=None):
880     """
881     Returns database id for given path.
882
883     If no matching file is found, a row is inserted.
884
885     @type filepath: string
886     @param filepath: The filepath
887
888     @type session: SQLAlchemy
889     @param session: Optional SQL session object (a temporary one will be
890     generated if not supplied).  If not passed, a commit will be performed at
891     the end of the function, otherwise the caller is responsible for commiting.
892
893     @rtype: int
894     @return: the database id for the given path
895     """
896
897     q = session.query(ContentFilepath).filter_by(filepath=filepath)
898
899     try:
900         ret = q.one().cafilepath_id
901     except NoResultFound:
902         cf = ContentFilepath()
903         cf.filepath = filepath
904         session.add(cf)
905         session.commit_or_flush()
906         ret = cf.cafilepath_id
907
908     return ret
909
910 __all__.append('get_or_set_contents_path_id')
911
912 ################################################################################
913
914 class ContentAssociation(object):
915     def __init__(self, *args, **kwargs):
916         pass
917
918     def __repr__(self):
919         return '<ContentAssociation %s>' % self.ca_id
920
921 __all__.append('ContentAssociation')
922
923 def insert_content_paths(binary_id, fullpaths, session=None):
924     """
925     Make sure given path is associated with given binary id
926
927     @type binary_id: int
928     @param binary_id: the id of the binary
929     @type fullpaths: list
930     @param fullpaths: the list of paths of the file being associated with the binary
931     @type session: SQLAlchemy session
932     @param session: Optional SQLAlchemy session.  If this is passed, the caller
933     is responsible for ensuring a transaction has begun and committing the
934     results or rolling back based on the result code.  If not passed, a commit
935     will be performed at the end of the function, otherwise the caller is
936     responsible for commiting.
937
938     @return: True upon success
939     """
940
941     privatetrans = False
942     if session is None:
943         session = DBConn().session()
944         privatetrans = True
945
946     try:
947         # Insert paths
948         def generate_path_dicts():
949             for fullpath in fullpaths:
950                 if fullpath.startswith( './' ):
951                     fullpath = fullpath[2:]
952
953                 yield {'filename':fullpath, 'id': binary_id }
954
955         for d in generate_path_dicts():
956             session.execute( "INSERT INTO bin_contents ( file, binary_id ) VALUES ( :filename, :id )",
957                          d )
958
959         session.commit()
960         if privatetrans:
961             session.close()
962         return True
963
964     except:
965         traceback.print_exc()
966
967         # Only rollback if we set up the session ourself
968         if privatetrans:
969             session.rollback()
970             session.close()
971
972         return False
973
974 __all__.append('insert_content_paths')
975
976 ################################################################################
977
978 class DSCFile(object):
979     def __init__(self, *args, **kwargs):
980         pass
981
982     def __repr__(self):
983         return '<DSCFile %s>' % self.dscfile_id
984
985 __all__.append('DSCFile')
986
987 @session_wrapper
988 def get_dscfiles(dscfile_id=None, source_id=None, poolfile_id=None, session=None):
989     """
990     Returns a list of DSCFiles which may be empty
991
992     @type dscfile_id: int (optional)
993     @param dscfile_id: the dscfile_id of the DSCFiles to find
994
995     @type source_id: int (optional)
996     @param source_id: the source id related to the DSCFiles to find
997
998     @type poolfile_id: int (optional)
999     @param poolfile_id: the poolfile id related to the DSCFiles to find
1000
1001     @rtype: list
1002     @return: Possibly empty list of DSCFiles
1003     """
1004
1005     q = session.query(DSCFile)
1006
1007     if dscfile_id is not None:
1008         q = q.filter_by(dscfile_id=dscfile_id)
1009
1010     if source_id is not None:
1011         q = q.filter_by(source_id=source_id)
1012
1013     if poolfile_id is not None:
1014         q = q.filter_by(poolfile_id=poolfile_id)
1015
1016     return q.all()
1017
1018 __all__.append('get_dscfiles')
1019
1020 ################################################################################
1021
1022 class ExternalOverride(ORMObject):
1023     def __init__(self, *args, **kwargs):
1024         pass
1025
1026     def __repr__(self):
1027         return '<ExternalOverride %s = %s: %s>' % (self.package, self.key, self.value)
1028
1029 __all__.append('ExternalOverride')
1030
1031 ################################################################################
1032
1033 class PoolFile(ORMObject):
1034     def __init__(self, filename = None, filesize = -1, \
1035         md5sum = None):
1036         self.filename = filename
1037         self.filesize = filesize
1038         self.md5sum = md5sum
1039
1040     @property
1041     def fullpath(self):
1042         session = DBConn().session().object_session(self)
1043         af = session.query(ArchiveFile).join(Archive).filter(ArchiveFile.file == self).first()
1044         return af.path
1045
1046     @property
1047     def component(self):
1048         session = DBConn().session().object_session(self)
1049         component_id = session.query(ArchiveFile.component_id).filter(ArchiveFile.file == self) \
1050                               .group_by(ArchiveFile.component_id).one()
1051         return session.query(Component).get(component_id)
1052
1053     @property
1054     def basename(self):
1055         return os.path.basename(self.filename)
1056
1057     def is_valid(self, filesize = -1, md5sum = None):
1058         return self.filesize == long(filesize) and self.md5sum == md5sum
1059
1060     def properties(self):
1061         return ['filename', 'file_id', 'filesize', 'md5sum', 'sha1sum', \
1062             'sha256sum', 'source', 'binary', 'last_used']
1063
1064     def not_null_constraints(self):
1065         return ['filename', 'md5sum']
1066
1067     def identical_to(self, filename):
1068         """
1069         compare size and hash with the given file
1070
1071         @rtype: bool
1072         @return: true if the given file has the same size and hash as this object; false otherwise
1073         """
1074         st = os.stat(filename)
1075         if self.filesize != st.st_size:
1076             return False
1077
1078         f = open(filename, "r")
1079         sha256sum = apt_pkg.sha256sum(f)
1080         if sha256sum != self.sha256sum:
1081             return False
1082
1083         return True
1084
1085 __all__.append('PoolFile')
1086
1087 @session_wrapper
1088 def get_poolfile_like_name(filename, session=None):
1089     """
1090     Returns an array of PoolFile objects which are like the given name
1091
1092     @type filename: string
1093     @param filename: the filename of the file to check against the DB
1094
1095     @rtype: array
1096     @return: array of PoolFile objects
1097     """
1098
1099     # TODO: There must be a way of properly using bind parameters with %FOO%
1100     q = session.query(PoolFile).filter(PoolFile.filename.like('%%/%s' % filename))
1101
1102     return q.all()
1103
1104 __all__.append('get_poolfile_like_name')
1105
1106 ################################################################################
1107
1108 class Fingerprint(ORMObject):
1109     def __init__(self, fingerprint = None):
1110         self.fingerprint = fingerprint
1111
1112     def properties(self):
1113         return ['fingerprint', 'fingerprint_id', 'keyring', 'uid', \
1114             'binary_reject']
1115
1116     def not_null_constraints(self):
1117         return ['fingerprint']
1118
1119 __all__.append('Fingerprint')
1120
1121 @session_wrapper
1122 def get_fingerprint(fpr, session=None):
1123     """
1124     Returns Fingerprint object for given fpr.
1125
1126     @type fpr: string
1127     @param fpr: The fpr to find / add
1128
1129     @type session: SQLAlchemy
1130     @param session: Optional SQL session object (a temporary one will be
1131     generated if not supplied).
1132
1133     @rtype: Fingerprint
1134     @return: the Fingerprint object for the given fpr or None
1135     """
1136
1137     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1138
1139     try:
1140         ret = q.one()
1141     except NoResultFound:
1142         ret = None
1143
1144     return ret
1145
1146 __all__.append('get_fingerprint')
1147
1148 @session_wrapper
1149 def get_or_set_fingerprint(fpr, session=None):
1150     """
1151     Returns Fingerprint object for given fpr.
1152
1153     If no matching fpr is found, a row is inserted.
1154
1155     @type fpr: string
1156     @param fpr: The fpr to find / add
1157
1158     @type session: SQLAlchemy
1159     @param session: Optional SQL session object (a temporary one will be
1160     generated if not supplied).  If not passed, a commit will be performed at
1161     the end of the function, otherwise the caller is responsible for commiting.
1162     A flush will be performed either way.
1163
1164     @rtype: Fingerprint
1165     @return: the Fingerprint object for the given fpr
1166     """
1167
1168     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1169
1170     try:
1171         ret = q.one()
1172     except NoResultFound:
1173         fingerprint = Fingerprint()
1174         fingerprint.fingerprint = fpr
1175         session.add(fingerprint)
1176         session.commit_or_flush()
1177         ret = fingerprint
1178
1179     return ret
1180
1181 __all__.append('get_or_set_fingerprint')
1182
1183 ################################################################################
1184
1185 # Helper routine for Keyring class
1186 def get_ldap_name(entry):
1187     name = []
1188     for k in ["cn", "mn", "sn"]:
1189         ret = entry.get(k)
1190         if ret and ret[0] != "" and ret[0] != "-":
1191             name.append(ret[0])
1192     return " ".join(name)
1193
1194 ################################################################################
1195
1196 class Keyring(object):
1197     gpg_invocation = "gpg --no-default-keyring --keyring %s" +\
1198                      " --with-colons --fingerprint --fingerprint"
1199
1200     keys = {}
1201     fpr_lookup = {}
1202
1203     def __init__(self, *args, **kwargs):
1204         pass
1205
1206     def __repr__(self):
1207         return '<Keyring %s>' % self.keyring_name
1208
1209     def de_escape_gpg_str(self, txt):
1210         esclist = re.split(r'(\\x..)', txt)
1211         for x in range(1,len(esclist),2):
1212             esclist[x] = "%c" % (int(esclist[x][2:],16))
1213         return "".join(esclist)
1214
1215     def parse_address(self, uid):
1216         """parses uid and returns a tuple of real name and email address"""
1217         import email.Utils
1218         (name, address) = email.Utils.parseaddr(uid)
1219         name = re.sub(r"\s*[(].*[)]", "", name)
1220         name = self.de_escape_gpg_str(name)
1221         if name == "":
1222             name = uid
1223         return (name, address)
1224
1225     def load_keys(self, keyring):
1226         if not self.keyring_id:
1227             raise Exception('Must be initialized with database information')
1228
1229         k = os.popen(self.gpg_invocation % keyring, "r")
1230         key = None
1231         signingkey = False
1232
1233         for line in k:
1234             field = line.split(":")
1235             if field[0] == "pub":
1236                 key = field[4]
1237                 self.keys[key] = {}
1238                 (name, addr) = self.parse_address(field[9])
1239                 if "@" in addr:
1240                     self.keys[key]["email"] = addr
1241                     self.keys[key]["name"] = name
1242                 self.keys[key]["fingerprints"] = []
1243                 signingkey = True
1244             elif key and field[0] == "sub" and len(field) >= 12:
1245                 signingkey = ("s" in field[11])
1246             elif key and field[0] == "uid":
1247                 (name, addr) = self.parse_address(field[9])
1248                 if "email" not in self.keys[key] and "@" in addr:
1249                     self.keys[key]["email"] = addr
1250                     self.keys[key]["name"] = name
1251             elif signingkey and field[0] == "fpr":
1252                 self.keys[key]["fingerprints"].append(field[9])
1253                 self.fpr_lookup[field[9]] = key
1254
1255     def import_users_from_ldap(self, session):
1256         import ldap
1257         cnf = Config()
1258
1259         LDAPDn = cnf["Import-LDAP-Fingerprints::LDAPDn"]
1260         LDAPServer = cnf["Import-LDAP-Fingerprints::LDAPServer"]
1261
1262         l = ldap.open(LDAPServer)
1263         l.simple_bind_s("","")
1264         Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
1265                "(&(keyfingerprint=*)(gidnumber=%s))" % (cnf["Import-Users-From-Passwd::ValidGID"]),
1266                ["uid", "keyfingerprint", "cn", "mn", "sn"])
1267
1268         ldap_fin_uid_id = {}
1269
1270         byuid = {}
1271         byname = {}
1272
1273         for i in Attrs:
1274             entry = i[1]
1275             uid = entry["uid"][0]
1276             name = get_ldap_name(entry)
1277             fingerprints = entry["keyFingerPrint"]
1278             keyid = None
1279             for f in fingerprints:
1280                 key = self.fpr_lookup.get(f, None)
1281                 if key not in self.keys:
1282                     continue
1283                 self.keys[key]["uid"] = uid
1284
1285                 if keyid != None:
1286                     continue
1287                 keyid = get_or_set_uid(uid, session).uid_id
1288                 byuid[keyid] = (uid, name)
1289                 byname[uid] = (keyid, name)
1290
1291         return (byname, byuid)
1292
1293     def generate_users_from_keyring(self, format, session):
1294         byuid = {}
1295         byname = {}
1296         any_invalid = False
1297         for x in self.keys.keys():
1298             if "email" not in self.keys[x]:
1299                 any_invalid = True
1300                 self.keys[x]["uid"] = format % "invalid-uid"
1301             else:
1302                 uid = format % self.keys[x]["email"]
1303                 keyid = get_or_set_uid(uid, session).uid_id
1304                 byuid[keyid] = (uid, self.keys[x]["name"])
1305                 byname[uid] = (keyid, self.keys[x]["name"])
1306                 self.keys[x]["uid"] = uid
1307
1308         if any_invalid:
1309             uid = format % "invalid-uid"
1310             keyid = get_or_set_uid(uid, session).uid_id
1311             byuid[keyid] = (uid, "ungeneratable user id")
1312             byname[uid] = (keyid, "ungeneratable user id")
1313
1314         return (byname, byuid)
1315
1316 __all__.append('Keyring')
1317
1318 @session_wrapper
1319 def get_keyring(keyring, session=None):
1320     """
1321     If C{keyring} does not have an entry in the C{keyrings} table yet, return None
1322     If C{keyring} already has an entry, simply return the existing Keyring
1323
1324     @type keyring: string
1325     @param keyring: the keyring name
1326
1327     @rtype: Keyring
1328     @return: the Keyring object for this keyring
1329     """
1330
1331     q = session.query(Keyring).filter_by(keyring_name=keyring)
1332
1333     try:
1334         return q.one()
1335     except NoResultFound:
1336         return None
1337
1338 __all__.append('get_keyring')
1339
1340 @session_wrapper
1341 def get_active_keyring_paths(session=None):
1342     """
1343     @rtype: list
1344     @return: list of active keyring paths
1345     """
1346     return [ x.keyring_name for x in session.query(Keyring).filter(Keyring.active == True).order_by(desc(Keyring.priority)).all() ]
1347
1348 __all__.append('get_active_keyring_paths')
1349
1350 @session_wrapper
1351 def get_primary_keyring_path(session=None):
1352     """
1353     Get the full path to the highest priority active keyring
1354
1355     @rtype: str or None
1356     @return: path to the active keyring with the highest priority or None if no
1357              keyring is configured
1358     """
1359     keyrings = get_active_keyring_paths()
1360
1361     if len(keyrings) > 0:
1362         return keyrings[0]
1363     else:
1364         return None
1365
1366 __all__.append('get_primary_keyring_path')
1367
1368 ################################################################################
1369
1370 class KeyringACLMap(object):
1371     def __init__(self, *args, **kwargs):
1372         pass
1373
1374     def __repr__(self):
1375         return '<KeyringACLMap %s>' % self.keyring_acl_map_id
1376
1377 __all__.append('KeyringACLMap')
1378
1379 ################################################################################
1380
1381 class DBChange(object):
1382     def __init__(self, *args, **kwargs):
1383         pass
1384
1385     def __repr__(self):
1386         return '<DBChange %s>' % self.changesname
1387
1388 __all__.append('DBChange')
1389
1390 @session_wrapper
1391 def get_dbchange(filename, session=None):
1392     """
1393     returns DBChange object for given C{filename}.
1394
1395     @type filename: string
1396     @param filename: the name of the file
1397
1398     @type session: Session
1399     @param session: Optional SQLA session object (a temporary one will be
1400     generated if not supplied)
1401
1402     @rtype: DBChange
1403     @return:  DBChange object for the given filename (C{None} if not present)
1404
1405     """
1406     q = session.query(DBChange).filter_by(changesname=filename)
1407
1408     try:
1409         return q.one()
1410     except NoResultFound:
1411         return None
1412
1413 __all__.append('get_dbchange')
1414
1415 ################################################################################
1416
1417 class Maintainer(ORMObject):
1418     def __init__(self, name = None):
1419         self.name = name
1420
1421     def properties(self):
1422         return ['name', 'maintainer_id']
1423
1424     def not_null_constraints(self):
1425         return ['name']
1426
1427     def get_split_maintainer(self):
1428         if not hasattr(self, 'name') or self.name is None:
1429             return ('', '', '', '')
1430
1431         return fix_maintainer(self.name.strip())
1432
1433 __all__.append('Maintainer')
1434
1435 @session_wrapper
1436 def get_or_set_maintainer(name, session=None):
1437     """
1438     Returns Maintainer object for given maintainer name.
1439
1440     If no matching maintainer name is found, a row is inserted.
1441
1442     @type name: string
1443     @param name: The maintainer name to add
1444
1445     @type session: SQLAlchemy
1446     @param session: Optional SQL session object (a temporary one will be
1447     generated if not supplied).  If not passed, a commit will be performed at
1448     the end of the function, otherwise the caller is responsible for commiting.
1449     A flush will be performed either way.
1450
1451     @rtype: Maintainer
1452     @return: the Maintainer object for the given maintainer
1453     """
1454
1455     q = session.query(Maintainer).filter_by(name=name)
1456     try:
1457         ret = q.one()
1458     except NoResultFound:
1459         maintainer = Maintainer()
1460         maintainer.name = name
1461         session.add(maintainer)
1462         session.commit_or_flush()
1463         ret = maintainer
1464
1465     return ret
1466
1467 __all__.append('get_or_set_maintainer')
1468
1469 @session_wrapper
1470 def get_maintainer(maintainer_id, session=None):
1471     """
1472     Return the name of the maintainer behind C{maintainer_id} or None if that
1473     maintainer_id is invalid.
1474
1475     @type maintainer_id: int
1476     @param maintainer_id: the id of the maintainer
1477
1478     @rtype: Maintainer
1479     @return: the Maintainer with this C{maintainer_id}
1480     """
1481
1482     return session.query(Maintainer).get(maintainer_id)
1483
1484 __all__.append('get_maintainer')
1485
1486 ################################################################################
1487
1488 class NewComment(object):
1489     def __init__(self, *args, **kwargs):
1490         pass
1491
1492     def __repr__(self):
1493         return '''<NewComment for '%s %s' (%s)>''' % (self.package, self.version, self.comment_id)
1494
1495 __all__.append('NewComment')
1496
1497 @session_wrapper
1498 def has_new_comment(package, version, session=None):
1499     """
1500     Returns true if the given combination of C{package}, C{version} has a comment.
1501
1502     @type package: string
1503     @param package: name of the package
1504
1505     @type version: string
1506     @param version: package version
1507
1508     @type session: Session
1509     @param session: Optional SQLA session object (a temporary one will be
1510     generated if not supplied)
1511
1512     @rtype: boolean
1513     @return: true/false
1514     """
1515
1516     q = session.query(NewComment)
1517     q = q.filter_by(package=package)
1518     q = q.filter_by(version=version)
1519
1520     return bool(q.count() > 0)
1521
1522 __all__.append('has_new_comment')
1523
1524 @session_wrapper
1525 def get_new_comments(package=None, version=None, comment_id=None, session=None):
1526     """
1527     Returns (possibly empty) list of NewComment objects for the given
1528     parameters
1529
1530     @type package: string (optional)
1531     @param package: name of the package
1532
1533     @type version: string (optional)
1534     @param version: package version
1535
1536     @type comment_id: int (optional)
1537     @param comment_id: An id of a comment
1538
1539     @type session: Session
1540     @param session: Optional SQLA session object (a temporary one will be
1541     generated if not supplied)
1542
1543     @rtype: list
1544     @return: A (possibly empty) list of NewComment objects will be returned
1545     """
1546
1547     q = session.query(NewComment)
1548     if package is not None: q = q.filter_by(package=package)
1549     if version is not None: q = q.filter_by(version=version)
1550     if comment_id is not None: q = q.filter_by(comment_id=comment_id)
1551
1552     return q.all()
1553
1554 __all__.append('get_new_comments')
1555
1556 ################################################################################
1557
1558 class Override(ORMObject):
1559     def __init__(self, package = None, suite = None, component = None, overridetype = None, \
1560         section = None, priority = None):
1561         self.package = package
1562         self.suite = suite
1563         self.component = component
1564         self.overridetype = overridetype
1565         self.section = section
1566         self.priority = priority
1567
1568     def properties(self):
1569         return ['package', 'suite', 'component', 'overridetype', 'section', \
1570             'priority']
1571
1572     def not_null_constraints(self):
1573         return ['package', 'suite', 'component', 'overridetype', 'section']
1574
1575 __all__.append('Override')
1576
1577 @session_wrapper
1578 def get_override(package, suite=None, component=None, overridetype=None, session=None):
1579     """
1580     Returns Override object for the given parameters
1581
1582     @type package: string
1583     @param package: The name of the package
1584
1585     @type suite: string, list or None
1586     @param suite: The name of the suite (or suites if a list) to limit to.  If
1587                   None, don't limit.  Defaults to None.
1588
1589     @type component: string, list or None
1590     @param component: The name of the component (or components if a list) to
1591                       limit to.  If None, don't limit.  Defaults to None.
1592
1593     @type overridetype: string, list or None
1594     @param overridetype: The name of the overridetype (or overridetypes if a list) to
1595                          limit to.  If None, don't limit.  Defaults to None.
1596
1597     @type session: Session
1598     @param session: Optional SQLA session object (a temporary one will be
1599     generated if not supplied)
1600
1601     @rtype: list
1602     @return: A (possibly empty) list of Override objects will be returned
1603     """
1604
1605     q = session.query(Override)
1606     q = q.filter_by(package=package)
1607
1608     if suite is not None:
1609         if not isinstance(suite, list): suite = [suite]
1610         q = q.join(Suite).filter(Suite.suite_name.in_(suite))
1611
1612     if component is not None:
1613         if not isinstance(component, list): component = [component]
1614         q = q.join(Component).filter(Component.component_name.in_(component))
1615
1616     if overridetype is not None:
1617         if not isinstance(overridetype, list): overridetype = [overridetype]
1618         q = q.join(OverrideType).filter(OverrideType.overridetype.in_(overridetype))
1619
1620     return q.all()
1621
1622 __all__.append('get_override')
1623
1624
1625 ################################################################################
1626
1627 class OverrideType(ORMObject):
1628     def __init__(self, overridetype = None):
1629         self.overridetype = overridetype
1630
1631     def properties(self):
1632         return ['overridetype', 'overridetype_id', 'overrides_count']
1633
1634     def not_null_constraints(self):
1635         return ['overridetype']
1636
1637 __all__.append('OverrideType')
1638
1639 @session_wrapper
1640 def get_override_type(override_type, session=None):
1641     """
1642     Returns OverrideType object for given C{override type}.
1643
1644     @type override_type: string
1645     @param override_type: The name of the override type
1646
1647     @type session: Session
1648     @param session: Optional SQLA session object (a temporary one will be
1649     generated if not supplied)
1650
1651     @rtype: int
1652     @return: the database id for the given override type
1653     """
1654
1655     q = session.query(OverrideType).filter_by(overridetype=override_type)
1656
1657     try:
1658         return q.one()
1659     except NoResultFound:
1660         return None
1661
1662 __all__.append('get_override_type')
1663
1664 ################################################################################
1665
1666 class PolicyQueue(object):
1667     def __init__(self, *args, **kwargs):
1668         pass
1669
1670     def __repr__(self):
1671         return '<PolicyQueue %s>' % self.queue_name
1672
1673 __all__.append('PolicyQueue')
1674
1675 @session_wrapper
1676 def get_policy_queue(queuename, session=None):
1677     """
1678     Returns PolicyQueue object for given C{queue name}
1679
1680     @type queuename: string
1681     @param queuename: The name of the queue
1682
1683     @type session: Session
1684     @param session: Optional SQLA session object (a temporary one will be
1685     generated if not supplied)
1686
1687     @rtype: PolicyQueue
1688     @return: PolicyQueue object for the given queue
1689     """
1690
1691     q = session.query(PolicyQueue).filter_by(queue_name=queuename)
1692
1693     try:
1694         return q.one()
1695     except NoResultFound:
1696         return None
1697
1698 __all__.append('get_policy_queue')
1699
1700 ################################################################################
1701
1702 class PolicyQueueUpload(object):
1703     def __cmp__(self, other):
1704         ret = cmp(self.changes.source, other.changes.source)
1705         if ret == 0:
1706             ret = apt_pkg.version_compare(self.changes.version, other.changes.version)
1707         if ret == 0:
1708             if self.source is not None and other.source is None:
1709                 ret = -1
1710             elif self.source is None and other.source is not None:
1711                 ret = 1
1712         if ret == 0:
1713             ret = cmp(self.changes.changesname, other.changes.changesname)
1714         return ret
1715
1716 __all__.append('PolicyQueueUpload')
1717
1718 ################################################################################
1719
1720 class PolicyQueueByhandFile(object):
1721     pass
1722
1723 __all__.append('PolicyQueueByhandFile')
1724
1725 ################################################################################
1726
1727 class Priority(ORMObject):
1728     def __init__(self, priority = None, level = None):
1729         self.priority = priority
1730         self.level = level
1731
1732     def properties(self):
1733         return ['priority', 'priority_id', 'level', 'overrides_count']
1734
1735     def not_null_constraints(self):
1736         return ['priority', 'level']
1737
1738     def __eq__(self, val):
1739         if isinstance(val, str):
1740             return (self.priority == val)
1741         # This signals to use the normal comparison operator
1742         return NotImplemented
1743
1744     def __ne__(self, val):
1745         if isinstance(val, str):
1746             return (self.priority != val)
1747         # This signals to use the normal comparison operator
1748         return NotImplemented
1749
1750 __all__.append('Priority')
1751
1752 @session_wrapper
1753 def get_priority(priority, session=None):
1754     """
1755     Returns Priority object for given C{priority name}.
1756
1757     @type priority: string
1758     @param priority: The name of the priority
1759
1760     @type session: Session
1761     @param session: Optional SQLA session object (a temporary one will be
1762     generated if not supplied)
1763
1764     @rtype: Priority
1765     @return: Priority object for the given priority
1766     """
1767
1768     q = session.query(Priority).filter_by(priority=priority)
1769
1770     try:
1771         return q.one()
1772     except NoResultFound:
1773         return None
1774
1775 __all__.append('get_priority')
1776
1777 @session_wrapper
1778 def get_priorities(session=None):
1779     """
1780     Returns dictionary of priority names -> id mappings
1781
1782     @type session: Session
1783     @param session: Optional SQL session object (a temporary one will be
1784     generated if not supplied)
1785
1786     @rtype: dictionary
1787     @return: dictionary of priority names -> id mappings
1788     """
1789
1790     ret = {}
1791     q = session.query(Priority)
1792     for x in q.all():
1793         ret[x.priority] = x.priority_id
1794
1795     return ret
1796
1797 __all__.append('get_priorities')
1798
1799 ################################################################################
1800
1801 class Section(ORMObject):
1802     def __init__(self, section = None):
1803         self.section = section
1804
1805     def properties(self):
1806         return ['section', 'section_id', 'overrides_count']
1807
1808     def not_null_constraints(self):
1809         return ['section']
1810
1811     def __eq__(self, val):
1812         if isinstance(val, str):
1813             return (self.section == val)
1814         # This signals to use the normal comparison operator
1815         return NotImplemented
1816
1817     def __ne__(self, val):
1818         if isinstance(val, str):
1819             return (self.section != val)
1820         # This signals to use the normal comparison operator
1821         return NotImplemented
1822
1823 __all__.append('Section')
1824
1825 @session_wrapper
1826 def get_section(section, session=None):
1827     """
1828     Returns Section object for given C{section name}.
1829
1830     @type section: string
1831     @param section: The name of the section
1832
1833     @type session: Session
1834     @param session: Optional SQLA session object (a temporary one will be
1835     generated if not supplied)
1836
1837     @rtype: Section
1838     @return: Section object for the given section name
1839     """
1840
1841     q = session.query(Section).filter_by(section=section)
1842
1843     try:
1844         return q.one()
1845     except NoResultFound:
1846         return None
1847
1848 __all__.append('get_section')
1849
1850 @session_wrapper
1851 def get_sections(session=None):
1852     """
1853     Returns dictionary of section names -> id mappings
1854
1855     @type session: Session
1856     @param session: Optional SQL session object (a temporary one will be
1857     generated if not supplied)
1858
1859     @rtype: dictionary
1860     @return: dictionary of section names -> id mappings
1861     """
1862
1863     ret = {}
1864     q = session.query(Section)
1865     for x in q.all():
1866         ret[x.section] = x.section_id
1867
1868     return ret
1869
1870 __all__.append('get_sections')
1871
1872 ################################################################################
1873
1874 class SrcContents(ORMObject):
1875     def __init__(self, file = None, source = None):
1876         self.file = file
1877         self.source = source
1878
1879     def properties(self):
1880         return ['file', 'source']
1881
1882 __all__.append('SrcContents')
1883
1884 ################################################################################
1885
1886 class DBSource(ORMObject):
1887     def __init__(self, source = None, version = None, maintainer = None, \
1888         changedby = None, poolfile = None, install_date = None, fingerprint = None):
1889         self.source = source
1890         self.version = version
1891         self.maintainer = maintainer
1892         self.changedby = changedby
1893         self.poolfile = poolfile
1894         self.install_date = install_date
1895         self.fingerprint = fingerprint
1896
1897     @property
1898     def pkid(self):
1899         return self.source_id
1900
1901     def properties(self):
1902         return ['source', 'source_id', 'maintainer', 'changedby', \
1903             'fingerprint', 'poolfile', 'version', 'suites_count', \
1904             'install_date', 'binaries_count', 'uploaders_count']
1905
1906     def not_null_constraints(self):
1907         return ['source', 'version', 'install_date', 'maintainer', \
1908             'changedby', 'poolfile']
1909
1910     def read_control_fields(self):
1911         '''
1912         Reads the control information from a dsc
1913
1914         @rtype: tuple
1915         @return: fields is the dsc information in a dictionary form
1916         '''
1917         fullpath = self.poolfile.fullpath
1918         contents = open(fullpath, 'r').read()
1919         signed_file = SignedFile(contents, keyrings=[], require_signature=False)
1920         fields = apt_pkg.TagSection(signed_file.contents)
1921         return fields
1922
1923     metadata = association_proxy('key', 'value')
1924
1925     def scan_contents(self):
1926         '''
1927         Returns a set of names for non directories. The path names are
1928         normalized after converting them from either utf-8 or iso8859-1
1929         encoding.
1930         '''
1931         fullpath = self.poolfile.fullpath
1932         from daklib.contents import UnpackedSource
1933         unpacked = UnpackedSource(fullpath)
1934         fileset = set()
1935         for name in unpacked.get_all_filenames():
1936             # enforce proper utf-8 encoding
1937             try:
1938                 name.decode('utf-8')
1939             except UnicodeDecodeError:
1940                 name = name.decode('iso8859-1').encode('utf-8')
1941             fileset.add(name)
1942         return fileset
1943
1944 __all__.append('DBSource')
1945
1946 @session_wrapper
1947 def source_exists(source, source_version, suites = ["any"], session=None):
1948     """
1949     Ensure that source exists somewhere in the archive for the binary
1950     upload being processed.
1951       1. exact match     => 1.0-3
1952       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
1953
1954     @type source: string
1955     @param source: source name
1956
1957     @type source_version: string
1958     @param source_version: expected source version
1959
1960     @type suites: list
1961     @param suites: list of suites to check in, default I{any}
1962
1963     @type session: Session
1964     @param session: Optional SQLA session object (a temporary one will be
1965     generated if not supplied)
1966
1967     @rtype: int
1968     @return: returns 1 if a source with expected version is found, otherwise 0
1969
1970     """
1971
1972     cnf = Config()
1973     ret = True
1974
1975     from daklib.regexes import re_bin_only_nmu
1976     orig_source_version = re_bin_only_nmu.sub('', source_version)
1977
1978     for suite in suites:
1979         q = session.query(DBSource).filter_by(source=source). \
1980             filter(DBSource.version.in_([source_version, orig_source_version]))
1981         if suite != "any":
1982             # source must exist in 'suite' or a suite that is enhanced by 'suite'
1983             s = get_suite(suite, session)
1984             if s:
1985                 enhances_vcs = session.query(VersionCheck).filter(VersionCheck.suite==s).filter_by(check='Enhances')
1986                 considered_suites = [ vc.reference for vc in enhances_vcs ]
1987                 considered_suites.append(s)
1988
1989                 q = q.filter(DBSource.suites.any(Suite.suite_id.in_([s.suite_id for s in considered_suites])))
1990
1991         if q.count() > 0:
1992             continue
1993
1994         # No source found so return not ok
1995         ret = False
1996
1997     return ret
1998
1999 __all__.append('source_exists')
2000
2001 @session_wrapper
2002 def get_suites_source_in(source, session=None):
2003     """
2004     Returns list of Suite objects which given C{source} name is in
2005
2006     @type source: str
2007     @param source: DBSource package name to search for
2008
2009     @rtype: list
2010     @return: list of Suite objects for the given source
2011     """
2012
2013     return session.query(Suite).filter(Suite.sources.any(source=source)).all()
2014
2015 __all__.append('get_suites_source_in')
2016
2017 @session_wrapper
2018 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
2019     """
2020     Returns list of DBSource objects for given C{source} name and other parameters
2021
2022     @type source: str
2023     @param source: DBSource package name to search for
2024
2025     @type version: str or None
2026     @param version: DBSource version name to search for or None if not applicable
2027
2028     @type dm_upload_allowed: bool
2029     @param dm_upload_allowed: If None, no effect.  If True or False, only
2030     return packages with that dm_upload_allowed setting
2031
2032     @type session: Session
2033     @param session: Optional SQL session object (a temporary one will be
2034     generated if not supplied)
2035
2036     @rtype: list
2037     @return: list of DBSource objects for the given name (may be empty)
2038     """
2039
2040     q = session.query(DBSource).filter_by(source=source)
2041
2042     if version is not None:
2043         q = q.filter_by(version=version)
2044
2045     if dm_upload_allowed is not None:
2046         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
2047
2048     return q.all()
2049
2050 __all__.append('get_sources_from_name')
2051
2052 # FIXME: This function fails badly if it finds more than 1 source package and
2053 # its implementation is trivial enough to be inlined.
2054 @session_wrapper
2055 def get_source_in_suite(source, suite, session=None):
2056     """
2057     Returns a DBSource object for a combination of C{source} and C{suite}.
2058
2059       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2060       - B{suite} - a suite name, eg. I{unstable}
2061
2062     @type source: string
2063     @param source: source package name
2064
2065     @type suite: string
2066     @param suite: the suite name
2067
2068     @rtype: string
2069     @return: the version for I{source} in I{suite}
2070
2071     """
2072
2073     q = get_suite(suite, session).get_sources(source)
2074     try:
2075         return q.one()
2076     except NoResultFound:
2077         return None
2078
2079 __all__.append('get_source_in_suite')
2080
2081 @session_wrapper
2082 def import_metadata_into_db(obj, session=None):
2083     """
2084     This routine works on either DBBinary or DBSource objects and imports
2085     their metadata into the database
2086     """
2087     fields = obj.read_control_fields()
2088     for k in fields.keys():
2089         try:
2090             # Try raw ASCII
2091             val = str(fields[k])
2092         except UnicodeEncodeError:
2093             # Fall back to UTF-8
2094             try:
2095                 val = fields[k].encode('utf-8')
2096             except UnicodeEncodeError:
2097                 # Finally try iso8859-1
2098                 val = fields[k].encode('iso8859-1')
2099                 # Otherwise we allow the exception to percolate up and we cause
2100                 # a reject as someone is playing silly buggers
2101
2102         obj.metadata[get_or_set_metadatakey(k, session)] = val
2103
2104     session.commit_or_flush()
2105
2106 __all__.append('import_metadata_into_db')
2107
2108 ################################################################################
2109
2110 class SourceACL(object):
2111     def __init__(self, *args, **kwargs):
2112         pass
2113
2114     def __repr__(self):
2115         return '<SourceACL %s>' % self.source_acl_id
2116
2117 __all__.append('SourceACL')
2118
2119 ################################################################################
2120
2121 class SrcFormat(object):
2122     def __init__(self, *args, **kwargs):
2123         pass
2124
2125     def __repr__(self):
2126         return '<SrcFormat %s>' % (self.format_name)
2127
2128 __all__.append('SrcFormat')
2129
2130 ################################################################################
2131
2132 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
2133                  ('SuiteID', 'suite_id'),
2134                  ('Version', 'version'),
2135                  ('Origin', 'origin'),
2136                  ('Label', 'label'),
2137                  ('Description', 'description'),
2138                  ('Untouchable', 'untouchable'),
2139                  ('Announce', 'announce'),
2140                  ('Codename', 'codename'),
2141                  ('OverrideCodename', 'overridecodename'),
2142                  ('ValidTime', 'validtime'),
2143                  ('Priority', 'priority'),
2144                  ('NotAutomatic', 'notautomatic'),
2145                  ('CopyChanges', 'copychanges'),
2146                  ('OverrideSuite', 'overridesuite')]
2147
2148 # Why the heck don't we have any UNIQUE constraints in table suite?
2149 # TODO: Add UNIQUE constraints for appropriate columns.
2150 class Suite(ORMObject):
2151     def __init__(self, suite_name = None, version = None):
2152         self.suite_name = suite_name
2153         self.version = version
2154
2155     def properties(self):
2156         return ['suite_name', 'version', 'sources_count', 'binaries_count', \
2157             'overrides_count']
2158
2159     def not_null_constraints(self):
2160         return ['suite_name']
2161
2162     def __eq__(self, val):
2163         if isinstance(val, str):
2164             return (self.suite_name == val)
2165         # This signals to use the normal comparison operator
2166         return NotImplemented
2167
2168     def __ne__(self, val):
2169         if isinstance(val, str):
2170             return (self.suite_name != val)
2171         # This signals to use the normal comparison operator
2172         return NotImplemented
2173
2174     def details(self):
2175         ret = []
2176         for disp, field in SUITE_FIELDS:
2177             val = getattr(self, field, None)
2178             if val is not None:
2179                 ret.append("%s: %s" % (disp, val))
2180
2181         return "\n".join(ret)
2182
2183     def get_architectures(self, skipsrc=False, skipall=False):
2184         """
2185         Returns list of Architecture objects
2186
2187         @type skipsrc: boolean
2188         @param skipsrc: Whether to skip returning the 'source' architecture entry
2189         (Default False)
2190
2191         @type skipall: boolean
2192         @param skipall: Whether to skip returning the 'all' architecture entry
2193         (Default False)
2194
2195         @rtype: list
2196         @return: list of Architecture objects for the given name (may be empty)
2197         """
2198
2199         q = object_session(self).query(Architecture).with_parent(self)
2200         if skipsrc:
2201             q = q.filter(Architecture.arch_string != 'source')
2202         if skipall:
2203             q = q.filter(Architecture.arch_string != 'all')
2204         return q.order_by(Architecture.arch_string).all()
2205
2206     def get_sources(self, source):
2207         """
2208         Returns a query object representing DBSource that is part of C{suite}.
2209
2210           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2211
2212         @type source: string
2213         @param source: source package name
2214
2215         @rtype: sqlalchemy.orm.query.Query
2216         @return: a query of DBSource
2217
2218         """
2219
2220         session = object_session(self)
2221         return session.query(DBSource).filter_by(source = source). \
2222             with_parent(self)
2223
2224     def get_overridesuite(self):
2225         if self.overridesuite is None:
2226             return self
2227         else:
2228             return object_session(self).query(Suite).filter_by(suite_name=self.overridesuite).one()
2229
2230     @property
2231     def path(self):
2232         return os.path.join(self.archive.path, 'dists', self.suite_name)
2233
2234 __all__.append('Suite')
2235
2236 @session_wrapper
2237 def get_suite(suite, session=None):
2238     """
2239     Returns Suite object for given C{suite name}.
2240
2241     @type suite: string
2242     @param suite: The name of the suite
2243
2244     @type session: Session
2245     @param session: Optional SQLA session object (a temporary one will be
2246     generated if not supplied)
2247
2248     @rtype: Suite
2249     @return: Suite object for the requested suite name (None if not present)
2250     """
2251
2252     q = session.query(Suite).filter_by(suite_name=suite)
2253
2254     try:
2255         return q.one()
2256     except NoResultFound:
2257         return None
2258
2259 __all__.append('get_suite')
2260
2261 ################################################################################
2262
2263 @session_wrapper
2264 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
2265     """
2266     Returns list of Architecture objects for given C{suite} name. The list is
2267     empty if suite does not exist.
2268
2269     @type suite: str
2270     @param suite: Suite name to search for
2271
2272     @type skipsrc: boolean
2273     @param skipsrc: Whether to skip returning the 'source' architecture entry
2274     (Default False)
2275
2276     @type skipall: boolean
2277     @param skipall: Whether to skip returning the 'all' architecture entry
2278     (Default False)
2279
2280     @type session: Session
2281     @param session: Optional SQL session object (a temporary one will be
2282     generated if not supplied)
2283
2284     @rtype: list
2285     @return: list of Architecture objects for the given name (may be empty)
2286     """
2287
2288     try:
2289         return get_suite(suite, session).get_architectures(skipsrc, skipall)
2290     except AttributeError:
2291         return []
2292
2293 __all__.append('get_suite_architectures')
2294
2295 ################################################################################
2296
2297 class Uid(ORMObject):
2298     def __init__(self, uid = None, name = None):
2299         self.uid = uid
2300         self.name = name
2301
2302     def __eq__(self, val):
2303         if isinstance(val, str):
2304             return (self.uid == val)
2305         # This signals to use the normal comparison operator
2306         return NotImplemented
2307
2308     def __ne__(self, val):
2309         if isinstance(val, str):
2310             return (self.uid != val)
2311         # This signals to use the normal comparison operator
2312         return NotImplemented
2313
2314     def properties(self):
2315         return ['uid', 'name', 'fingerprint']
2316
2317     def not_null_constraints(self):
2318         return ['uid']
2319
2320 __all__.append('Uid')
2321
2322 @session_wrapper
2323 def get_or_set_uid(uidname, session=None):
2324     """
2325     Returns uid object for given uidname.
2326
2327     If no matching uidname is found, a row is inserted.
2328
2329     @type uidname: string
2330     @param uidname: The uid to add
2331
2332     @type session: SQLAlchemy
2333     @param session: Optional SQL session object (a temporary one will be
2334     generated if not supplied).  If not passed, a commit will be performed at
2335     the end of the function, otherwise the caller is responsible for commiting.
2336
2337     @rtype: Uid
2338     @return: the uid object for the given uidname
2339     """
2340
2341     q = session.query(Uid).filter_by(uid=uidname)
2342
2343     try:
2344         ret = q.one()
2345     except NoResultFound:
2346         uid = Uid()
2347         uid.uid = uidname
2348         session.add(uid)
2349         session.commit_or_flush()
2350         ret = uid
2351
2352     return ret
2353
2354 __all__.append('get_or_set_uid')
2355
2356 @session_wrapper
2357 def get_uid_from_fingerprint(fpr, session=None):
2358     q = session.query(Uid)
2359     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
2360
2361     try:
2362         return q.one()
2363     except NoResultFound:
2364         return None
2365
2366 __all__.append('get_uid_from_fingerprint')
2367
2368 ################################################################################
2369
2370 class UploadBlock(object):
2371     def __init__(self, *args, **kwargs):
2372         pass
2373
2374     def __repr__(self):
2375         return '<UploadBlock %s (%s)>' % (self.source, self.upload_block_id)
2376
2377 __all__.append('UploadBlock')
2378
2379 ################################################################################
2380
2381 class MetadataKey(ORMObject):
2382     def __init__(self, key = None):
2383         self.key = key
2384
2385     def properties(self):
2386         return ['key']
2387
2388     def not_null_constraints(self):
2389         return ['key']
2390
2391 __all__.append('MetadataKey')
2392
2393 @session_wrapper
2394 def get_or_set_metadatakey(keyname, session=None):
2395     """
2396     Returns MetadataKey object for given uidname.
2397
2398     If no matching keyname is found, a row is inserted.
2399
2400     @type uidname: string
2401     @param uidname: The keyname to add
2402
2403     @type session: SQLAlchemy
2404     @param session: Optional SQL session object (a temporary one will be
2405     generated if not supplied).  If not passed, a commit will be performed at
2406     the end of the function, otherwise the caller is responsible for commiting.
2407
2408     @rtype: MetadataKey
2409     @return: the metadatakey object for the given keyname
2410     """
2411
2412     q = session.query(MetadataKey).filter_by(key=keyname)
2413
2414     try:
2415         ret = q.one()
2416     except NoResultFound:
2417         ret = MetadataKey(keyname)
2418         session.add(ret)
2419         session.commit_or_flush()
2420
2421     return ret
2422
2423 __all__.append('get_or_set_metadatakey')
2424
2425 ################################################################################
2426
2427 class BinaryMetadata(ORMObject):
2428     def __init__(self, key = None, value = None, binary = None):
2429         self.key = key
2430         self.value = value
2431         self.binary = binary
2432
2433     def properties(self):
2434         return ['binary', 'key', 'value']
2435
2436     def not_null_constraints(self):
2437         return ['value']
2438
2439 __all__.append('BinaryMetadata')
2440
2441 ################################################################################
2442
2443 class SourceMetadata(ORMObject):
2444     def __init__(self, key = None, value = None, source = None):
2445         self.key = key
2446         self.value = value
2447         self.source = source
2448
2449     def properties(self):
2450         return ['source', 'key', 'value']
2451
2452     def not_null_constraints(self):
2453         return ['value']
2454
2455 __all__.append('SourceMetadata')
2456
2457 ################################################################################
2458
2459 class VersionCheck(ORMObject):
2460     def __init__(self, *args, **kwargs):
2461         pass
2462
2463     def properties(self):
2464         #return ['suite_id', 'check', 'reference_id']
2465         return ['check']
2466
2467     def not_null_constraints(self):
2468         return ['suite', 'check', 'reference']
2469
2470 __all__.append('VersionCheck')
2471
2472 @session_wrapper
2473 def get_version_checks(suite_name, check = None, session = None):
2474     suite = get_suite(suite_name, session)
2475     if not suite:
2476         # Make sure that what we return is iterable so that list comprehensions
2477         # involving this don't cause a traceback
2478         return []
2479     q = session.query(VersionCheck).filter_by(suite=suite)
2480     if check:
2481         q = q.filter_by(check=check)
2482     return q.all()
2483
2484 __all__.append('get_version_checks')
2485
2486 ################################################################################
2487
2488 class DBConn(object):
2489     """
2490     database module init.
2491     """
2492     __shared_state = {}
2493
2494     def __init__(self, *args, **kwargs):
2495         self.__dict__ = self.__shared_state
2496
2497         if not getattr(self, 'initialised', False):
2498             self.initialised = True
2499             self.debug = kwargs.has_key('debug')
2500             self.__createconn()
2501
2502     def __setuptables(self):
2503         tables = (
2504             'architecture',
2505             'archive',
2506             'bin_associations',
2507             'bin_contents',
2508             'binaries',
2509             'binaries_metadata',
2510             'binary_acl',
2511             'binary_acl_map',
2512             'build_queue',
2513             'changelogs_text',
2514             'changes',
2515             'component',
2516             'config',
2517             'dsc_files',
2518             'external_overrides',
2519             'extra_src_references',
2520             'files',
2521             'files_archive_map',
2522             'fingerprint',
2523             'keyrings',
2524             'keyring_acl_map',
2525             'maintainer',
2526             'metadata_keys',
2527             'new_comments',
2528             # TODO: the maintainer column in table override should be removed.
2529             'override',
2530             'override_type',
2531             'policy_queue',
2532             'policy_queue_upload',
2533             'policy_queue_upload_binaries_map',
2534             'policy_queue_byhand_file',
2535             'priority',
2536             'section',
2537             'source',
2538             'source_acl',
2539             'source_metadata',
2540             'src_associations',
2541             'src_contents',
2542             'src_format',
2543             'src_uploaders',
2544             'suite',
2545             'suite_architectures',
2546             'suite_build_queue_copy',
2547             'suite_src_formats',
2548             'uid',
2549             'upload_blocks',
2550             'version_check',
2551         )
2552
2553         views = (
2554             'almost_obsolete_all_associations',
2555             'almost_obsolete_src_associations',
2556             'any_associations_source',
2557             'bin_associations_binaries',
2558             'binaries_suite_arch',
2559             'changelogs',
2560             'file_arch_suite',
2561             'newest_all_associations',
2562             'newest_any_associations',
2563             'newest_source',
2564             'newest_src_association',
2565             'obsolete_all_associations',
2566             'obsolete_any_associations',
2567             'obsolete_any_by_all_associations',
2568             'obsolete_src_associations',
2569             'source_suite',
2570             'src_associations_bin',
2571             'src_associations_src',
2572             'suite_arch_by_name',
2573         )
2574
2575         for table_name in tables:
2576             table = Table(table_name, self.db_meta, \
2577                 autoload=True, useexisting=True)
2578             setattr(self, 'tbl_%s' % table_name, table)
2579
2580         for view_name in views:
2581             view = Table(view_name, self.db_meta, autoload=True)
2582             setattr(self, 'view_%s' % view_name, view)
2583
2584     def __setupmappers(self):
2585         mapper(Architecture, self.tbl_architecture,
2586             properties = dict(arch_id = self.tbl_architecture.c.id,
2587                suites = relation(Suite, secondary=self.tbl_suite_architectures,
2588                    order_by=self.tbl_suite.c.suite_name,
2589                    backref=backref('architectures', order_by=self.tbl_architecture.c.arch_string))),
2590             extension = validator)
2591
2592         mapper(Archive, self.tbl_archive,
2593                properties = dict(archive_id = self.tbl_archive.c.id,
2594                                  archive_name = self.tbl_archive.c.name))
2595
2596         mapper(ArchiveFile, self.tbl_files_archive_map,
2597                properties = dict(archive = relation(Archive, backref='files'),
2598                                  component = relation(Component),
2599                                  file = relation(PoolFile, backref='archives')))
2600
2601         mapper(BuildQueue, self.tbl_build_queue,
2602                properties = dict(queue_id = self.tbl_build_queue.c.id,
2603                                  suite = relation(Suite, primaryjoin=(self.tbl_build_queue.c.suite_id==self.tbl_suite.c.id))))
2604
2605         mapper(DBBinary, self.tbl_binaries,
2606                properties = dict(binary_id = self.tbl_binaries.c.id,
2607                                  package = self.tbl_binaries.c.package,
2608                                  version = self.tbl_binaries.c.version,
2609                                  maintainer_id = self.tbl_binaries.c.maintainer,
2610                                  maintainer = relation(Maintainer),
2611                                  source_id = self.tbl_binaries.c.source,
2612                                  source = relation(DBSource, backref='binaries'),
2613                                  arch_id = self.tbl_binaries.c.architecture,
2614                                  architecture = relation(Architecture),
2615                                  poolfile_id = self.tbl_binaries.c.file,
2616                                  poolfile = relation(PoolFile),
2617                                  binarytype = self.tbl_binaries.c.type,
2618                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
2619                                  fingerprint = relation(Fingerprint),
2620                                  install_date = self.tbl_binaries.c.install_date,
2621                                  suites = relation(Suite, secondary=self.tbl_bin_associations,
2622                                      backref=backref('binaries', lazy='dynamic')),
2623                                  extra_sources = relation(DBSource, secondary=self.tbl_extra_src_references,
2624                                      backref=backref('extra_binary_references', lazy='dynamic')),
2625                                  key = relation(BinaryMetadata, cascade='all',
2626                                      collection_class=attribute_mapped_collection('key'))),
2627                 extension = validator)
2628
2629         mapper(BinaryACL, self.tbl_binary_acl,
2630                properties = dict(binary_acl_id = self.tbl_binary_acl.c.id))
2631
2632         mapper(BinaryACLMap, self.tbl_binary_acl_map,
2633                properties = dict(binary_acl_map_id = self.tbl_binary_acl_map.c.id,
2634                                  fingerprint = relation(Fingerprint, backref="binary_acl_map"),
2635                                  architecture = relation(Architecture)))
2636
2637         mapper(Component, self.tbl_component,
2638                properties = dict(component_id = self.tbl_component.c.id,
2639                                  component_name = self.tbl_component.c.name),
2640                extension = validator)
2641
2642         mapper(DBConfig, self.tbl_config,
2643                properties = dict(config_id = self.tbl_config.c.id))
2644
2645         mapper(DSCFile, self.tbl_dsc_files,
2646                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
2647                                  source_id = self.tbl_dsc_files.c.source,
2648                                  source = relation(DBSource),
2649                                  poolfile_id = self.tbl_dsc_files.c.file,
2650                                  poolfile = relation(PoolFile)))
2651
2652         mapper(ExternalOverride, self.tbl_external_overrides,
2653                 properties = dict(
2654                     suite_id = self.tbl_external_overrides.c.suite,
2655                     suite = relation(Suite),
2656                     component_id = self.tbl_external_overrides.c.component,
2657                     component = relation(Component)))
2658
2659         mapper(PoolFile, self.tbl_files,
2660                properties = dict(file_id = self.tbl_files.c.id,
2661                                  filesize = self.tbl_files.c.size),
2662                 extension = validator)
2663
2664         mapper(Fingerprint, self.tbl_fingerprint,
2665                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
2666                                  uid_id = self.tbl_fingerprint.c.uid,
2667                                  uid = relation(Uid),
2668                                  keyring_id = self.tbl_fingerprint.c.keyring,
2669                                  keyring = relation(Keyring),
2670                                  source_acl = relation(SourceACL),
2671                                  binary_acl = relation(BinaryACL)),
2672                extension = validator)
2673
2674         mapper(Keyring, self.tbl_keyrings,
2675                properties = dict(keyring_name = self.tbl_keyrings.c.name,
2676                                  keyring_id = self.tbl_keyrings.c.id))
2677
2678         mapper(DBChange, self.tbl_changes,
2679                properties = dict(change_id = self.tbl_changes.c.id,
2680                                  seen = self.tbl_changes.c.seen,
2681                                  source = self.tbl_changes.c.source,
2682                                  binaries = self.tbl_changes.c.binaries,
2683                                  architecture = self.tbl_changes.c.architecture,
2684                                  distribution = self.tbl_changes.c.distribution,
2685                                  urgency = self.tbl_changes.c.urgency,
2686                                  maintainer = self.tbl_changes.c.maintainer,
2687                                  changedby = self.tbl_changes.c.changedby,
2688                                  date = self.tbl_changes.c.date,
2689                                  version = self.tbl_changes.c.version))
2690
2691         mapper(KeyringACLMap, self.tbl_keyring_acl_map,
2692                properties = dict(keyring_acl_map_id = self.tbl_keyring_acl_map.c.id,
2693                                  keyring = relation(Keyring, backref="keyring_acl_map"),
2694                                  architecture = relation(Architecture)))
2695
2696         mapper(Maintainer, self.tbl_maintainer,
2697                properties = dict(maintainer_id = self.tbl_maintainer.c.id,
2698                    maintains_sources = relation(DBSource, backref='maintainer',
2699                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.maintainer)),
2700                    changed_sources = relation(DBSource, backref='changedby',
2701                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.changedby))),
2702                 extension = validator)
2703
2704         mapper(NewComment, self.tbl_new_comments,
2705                properties = dict(comment_id = self.tbl_new_comments.c.id))
2706
2707         mapper(Override, self.tbl_override,
2708                properties = dict(suite_id = self.tbl_override.c.suite,
2709                                  suite = relation(Suite, \
2710                                     backref=backref('overrides', lazy='dynamic')),
2711                                  package = self.tbl_override.c.package,
2712                                  component_id = self.tbl_override.c.component,
2713                                  component = relation(Component, \
2714                                     backref=backref('overrides', lazy='dynamic')),
2715                                  priority_id = self.tbl_override.c.priority,
2716                                  priority = relation(Priority, \
2717                                     backref=backref('overrides', lazy='dynamic')),
2718                                  section_id = self.tbl_override.c.section,
2719                                  section = relation(Section, \
2720                                     backref=backref('overrides', lazy='dynamic')),
2721                                  overridetype_id = self.tbl_override.c.type,
2722                                  overridetype = relation(OverrideType, \
2723                                     backref=backref('overrides', lazy='dynamic'))))
2724
2725         mapper(OverrideType, self.tbl_override_type,
2726                properties = dict(overridetype = self.tbl_override_type.c.type,
2727                                  overridetype_id = self.tbl_override_type.c.id))
2728
2729         mapper(PolicyQueue, self.tbl_policy_queue,
2730                properties = dict(policy_queue_id = self.tbl_policy_queue.c.id,
2731                                  suite = relation(Suite, primaryjoin=(self.tbl_policy_queue.c.suite_id == self.tbl_suite.c.id))))
2732
2733         mapper(PolicyQueueUpload, self.tbl_policy_queue_upload,
2734                properties = dict(
2735                    changes = relation(DBChange),
2736                    policy_queue = relation(PolicyQueue, backref='uploads'),
2737                    target_suite = relation(Suite),
2738                    source = relation(DBSource),
2739                    binaries = relation(DBBinary, secondary=self.tbl_policy_queue_upload_binaries_map),
2740                 ))
2741
2742         mapper(PolicyQueueByhandFile, self.tbl_policy_queue_byhand_file,
2743                properties = dict(
2744                    upload = relation(PolicyQueueUpload, backref='byhand'),
2745                    )
2746                )
2747
2748         mapper(Priority, self.tbl_priority,
2749                properties = dict(priority_id = self.tbl_priority.c.id))
2750
2751         mapper(Section, self.tbl_section,
2752                properties = dict(section_id = self.tbl_section.c.id,
2753                                  section=self.tbl_section.c.section))
2754
2755         mapper(DBSource, self.tbl_source,
2756                properties = dict(source_id = self.tbl_source.c.id,
2757                                  version = self.tbl_source.c.version,
2758                                  maintainer_id = self.tbl_source.c.maintainer,
2759                                  poolfile_id = self.tbl_source.c.file,
2760                                  poolfile = relation(PoolFile),
2761                                  fingerprint_id = self.tbl_source.c.sig_fpr,
2762                                  fingerprint = relation(Fingerprint),
2763                                  changedby_id = self.tbl_source.c.changedby,
2764                                  srcfiles = relation(DSCFile,
2765                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
2766                                  suites = relation(Suite, secondary=self.tbl_src_associations,
2767                                      backref=backref('sources', lazy='dynamic')),
2768                                  uploaders = relation(Maintainer,
2769                                      secondary=self.tbl_src_uploaders),
2770                                  key = relation(SourceMetadata, cascade='all',
2771                                      collection_class=attribute_mapped_collection('key'))),
2772                extension = validator)
2773
2774         mapper(SourceACL, self.tbl_source_acl,
2775                properties = dict(source_acl_id = self.tbl_source_acl.c.id))
2776
2777         mapper(SrcFormat, self.tbl_src_format,
2778                properties = dict(src_format_id = self.tbl_src_format.c.id,
2779                                  format_name = self.tbl_src_format.c.format_name))
2780
2781         mapper(Suite, self.tbl_suite,
2782                properties = dict(suite_id = self.tbl_suite.c.id,
2783                                  policy_queue = relation(PolicyQueue, primaryjoin=(self.tbl_suite.c.policy_queue_id == self.tbl_policy_queue.c.id)),
2784                                  copy_queues = relation(BuildQueue,
2785                                      secondary=self.tbl_suite_build_queue_copy),
2786                                  srcformats = relation(SrcFormat, secondary=self.tbl_suite_src_formats,
2787                                      backref=backref('suites', lazy='dynamic')),
2788                                  archive = relation(Archive, backref='suites')),
2789                 extension = validator)
2790
2791         mapper(Uid, self.tbl_uid,
2792                properties = dict(uid_id = self.tbl_uid.c.id,
2793                                  fingerprint = relation(Fingerprint)),
2794                extension = validator)
2795
2796         mapper(UploadBlock, self.tbl_upload_blocks,
2797                properties = dict(upload_block_id = self.tbl_upload_blocks.c.id,
2798                                  fingerprint = relation(Fingerprint, backref="uploadblocks"),
2799                                  uid = relation(Uid, backref="uploadblocks")))
2800
2801         mapper(BinContents, self.tbl_bin_contents,
2802             properties = dict(
2803                 binary = relation(DBBinary,
2804                     backref=backref('contents', lazy='dynamic', cascade='all')),
2805                 file = self.tbl_bin_contents.c.file))
2806
2807         mapper(SrcContents, self.tbl_src_contents,
2808             properties = dict(
2809                 source = relation(DBSource,
2810                     backref=backref('contents', lazy='dynamic', cascade='all')),
2811                 file = self.tbl_src_contents.c.file))
2812
2813         mapper(MetadataKey, self.tbl_metadata_keys,
2814             properties = dict(
2815                 key_id = self.tbl_metadata_keys.c.key_id,
2816                 key = self.tbl_metadata_keys.c.key))
2817
2818         mapper(BinaryMetadata, self.tbl_binaries_metadata,
2819             properties = dict(
2820                 binary_id = self.tbl_binaries_metadata.c.bin_id,
2821                 binary = relation(DBBinary),
2822                 key_id = self.tbl_binaries_metadata.c.key_id,
2823                 key = relation(MetadataKey),
2824                 value = self.tbl_binaries_metadata.c.value))
2825
2826         mapper(SourceMetadata, self.tbl_source_metadata,
2827             properties = dict(
2828                 source_id = self.tbl_source_metadata.c.src_id,
2829                 source = relation(DBSource),
2830                 key_id = self.tbl_source_metadata.c.key_id,
2831                 key = relation(MetadataKey),
2832                 value = self.tbl_source_metadata.c.value))
2833
2834         mapper(VersionCheck, self.tbl_version_check,
2835             properties = dict(
2836                 suite_id = self.tbl_version_check.c.suite,
2837                 suite = relation(Suite, primaryjoin=self.tbl_version_check.c.suite==self.tbl_suite.c.id),
2838                 reference_id = self.tbl_version_check.c.reference,
2839                 reference = relation(Suite, primaryjoin=self.tbl_version_check.c.reference==self.tbl_suite.c.id, lazy='joined')))
2840
2841     ## Connection functions
2842     def __createconn(self):
2843         from config import Config
2844         cnf = Config()
2845         if cnf.has_key("DB::Service"):
2846             connstr = "postgresql://service=%s" % cnf["DB::Service"]
2847         elif cnf.has_key("DB::Host"):
2848             # TCP/IP
2849             connstr = "postgresql://%s" % cnf["DB::Host"]
2850             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
2851                 connstr += ":%s" % cnf["DB::Port"]
2852             connstr += "/%s" % cnf["DB::Name"]
2853         else:
2854             # Unix Socket
2855             connstr = "postgresql:///%s" % cnf["DB::Name"]
2856             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
2857                 connstr += "?port=%s" % cnf["DB::Port"]
2858
2859         engine_args = { 'echo': self.debug }
2860         if cnf.has_key('DB::PoolSize'):
2861             engine_args['pool_size'] = int(cnf['DB::PoolSize'])
2862         if cnf.has_key('DB::MaxOverflow'):
2863             engine_args['max_overflow'] = int(cnf['DB::MaxOverflow'])
2864         if sa_major_version == '0.6' and cnf.has_key('DB::Unicode') and \
2865             cnf['DB::Unicode'] == 'false':
2866             engine_args['use_native_unicode'] = False
2867
2868         # Monkey patch a new dialect in in order to support service= syntax
2869         import sqlalchemy.dialects.postgresql
2870         from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
2871         class PGDialect_psycopg2_dak(PGDialect_psycopg2):
2872             def create_connect_args(self, url):
2873                 if str(url).startswith('postgresql://service='):
2874                     # Eww
2875                     servicename = str(url)[21:]
2876                     return (['service=%s' % servicename], {})
2877                 else:
2878                     return PGDialect_psycopg2.create_connect_args(self, url)
2879
2880         sqlalchemy.dialects.postgresql.base.dialect = PGDialect_psycopg2_dak
2881
2882         try:
2883             self.db_pg   = create_engine(connstr, **engine_args)
2884             self.db_meta = MetaData()
2885             self.db_meta.bind = self.db_pg
2886             self.db_smaker = sessionmaker(bind=self.db_pg,
2887                                           autoflush=True,
2888                                           autocommit=False)
2889
2890             self.__setuptables()
2891             self.__setupmappers()
2892
2893         except OperationalError as e:
2894             import utils
2895             utils.fubar("Cannot connect to database (%s)" % str(e))
2896
2897         self.pid = os.getpid()
2898
2899     def session(self, work_mem = 0):
2900         '''
2901         Returns a new session object. If a work_mem parameter is provided a new
2902         transaction is started and the work_mem parameter is set for this
2903         transaction. The work_mem parameter is measured in MB. A default value
2904         will be used if the parameter is not set.
2905         '''
2906         # reinitialize DBConn in new processes
2907         if self.pid != os.getpid():
2908             clear_mappers()
2909             self.__createconn()
2910         session = self.db_smaker()
2911         if work_mem > 0:
2912             session.execute("SET LOCAL work_mem TO '%d MB'" % work_mem)
2913         return session
2914
2915 __all__.append('DBConn')
2916
2917