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