]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
Reset signal handler to default action in child processes
[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"]:
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.8.  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         import apt_pkg
576         stanza = self.read_control()
577         return apt_pkg.TagSection(stanza)
578
579 __all__.append('DBBinary')
580
581 @session_wrapper
582 def get_suites_binary_in(package, session=None):
583     """
584     Returns list of Suite objects which given C{package} name is in
585
586     @type package: str
587     @param package: DBBinary package name to search for
588
589     @rtype: list
590     @return: list of Suite objects for the given package
591     """
592
593     return session.query(Suite).filter(Suite.binaries.any(DBBinary.package == package)).all()
594
595 __all__.append('get_suites_binary_in')
596
597 @session_wrapper
598 def get_component_by_package_suite(package, suite_list, arch_list=[], session=None):
599     '''
600     Returns the component name of the newest binary package in suite_list or
601     None if no package is found. The result can be optionally filtered by a list
602     of architecture names.
603
604     @type package: str
605     @param package: DBBinary package name to search for
606
607     @type suite_list: list of str
608     @param suite_list: list of suite_name items
609
610     @type arch_list: list of str
611     @param arch_list: optional list of arch_string items that defaults to []
612
613     @rtype: str or NoneType
614     @return: name of component or None
615     '''
616
617     q = session.query(DBBinary).filter_by(package = package). \
618         join(DBBinary.suites).filter(Suite.suite_name.in_(suite_list))
619     if len(arch_list) > 0:
620         q = q.join(DBBinary.architecture). \
621             filter(Architecture.arch_string.in_(arch_list))
622     binary = q.order_by(desc(DBBinary.version)).first()
623     if binary is None:
624         return None
625     else:
626         return binary.poolfile.component.component_name
627
628 __all__.append('get_component_by_package_suite')
629
630 ################################################################################
631
632 class BuildQueue(object):
633     def __init__(self, *args, **kwargs):
634         pass
635
636     def __repr__(self):
637         return '<BuildQueue %s>' % self.queue_name
638
639 __all__.append('BuildQueue')
640
641 ################################################################################
642
643 class Component(ORMObject):
644     def __init__(self, component_name = None):
645         self.component_name = component_name
646
647     def __eq__(self, val):
648         if isinstance(val, str):
649             return (self.component_name == val)
650         # This signals to use the normal comparison operator
651         return NotImplemented
652
653     def __ne__(self, val):
654         if isinstance(val, str):
655             return (self.component_name != val)
656         # This signals to use the normal comparison operator
657         return NotImplemented
658
659     def properties(self):
660         return ['component_name', 'component_id', 'description', \
661             'meets_dfsg', 'overrides_count']
662
663     def not_null_constraints(self):
664         return ['component_name']
665
666
667 __all__.append('Component')
668
669 @session_wrapper
670 def get_component(component, session=None):
671     """
672     Returns database id for given C{component}.
673
674     @type component: string
675     @param component: The name of the override type
676
677     @rtype: int
678     @return: the database id for the given component
679
680     """
681     component = component.lower()
682
683     q = session.query(Component).filter_by(component_name=component)
684
685     try:
686         return q.one()
687     except NoResultFound:
688         return None
689
690 __all__.append('get_component')
691
692 @session_wrapper
693 def get_mapped_component(component_name, session=None):
694     """get component after mappings
695
696     Evaluate component mappings from ComponentMappings in dak.conf for the
697     given component name.
698
699     @todo: ansgar wants to get rid of this. It's currently only used for
700            the security archive
701
702     @type  component_name: str
703     @param component_name: component name
704
705     @param session: database session
706
707     @rtype:  L{daklib.dbconn.Component} or C{None}
708     @return: component after applying maps or C{None}
709     """
710     cnf = Config()
711     for m in cnf.value_list("ComponentMappings"):
712         (src, dst) = m.split()
713         if component_name == src:
714             component_name = dst
715     component = session.query(Component).filter_by(component_name=component_name).first()
716     return component
717
718 __all__.append('get_mapped_component')
719
720 @session_wrapper
721 def get_component_names(session=None):
722     """
723     Returns list of strings of component names.
724
725     @rtype: list
726     @return: list of strings of component names
727     """
728
729     return [ x.component_name for x in session.query(Component).all() ]
730
731 __all__.append('get_component_names')
732
733 ################################################################################
734
735 class DBConfig(object):
736     def __init__(self, *args, **kwargs):
737         pass
738
739     def __repr__(self):
740         return '<DBConfig %s>' % self.name
741
742 __all__.append('DBConfig')
743
744 ################################################################################
745
746 @session_wrapper
747 def get_or_set_contents_file_id(filename, session=None):
748     """
749     Returns database id for given filename.
750
751     If no matching file is found, a row is inserted.
752
753     @type filename: string
754     @param filename: The filename
755     @type session: SQLAlchemy
756     @param session: Optional SQL session object (a temporary one will be
757     generated if not supplied).  If not passed, a commit will be performed at
758     the end of the function, otherwise the caller is responsible for commiting.
759
760     @rtype: int
761     @return: the database id for the given component
762     """
763
764     q = session.query(ContentFilename).filter_by(filename=filename)
765
766     try:
767         ret = q.one().cafilename_id
768     except NoResultFound:
769         cf = ContentFilename()
770         cf.filename = filename
771         session.add(cf)
772         session.commit_or_flush()
773         ret = cf.cafilename_id
774
775     return ret
776
777 __all__.append('get_or_set_contents_file_id')
778
779 @session_wrapper
780 def get_contents(suite, overridetype, section=None, session=None):
781     """
782     Returns contents for a suite / overridetype combination, limiting
783     to a section if not None.
784
785     @type suite: Suite
786     @param suite: Suite object
787
788     @type overridetype: OverrideType
789     @param overridetype: OverrideType object
790
791     @type section: Section
792     @param section: Optional section object to limit results to
793
794     @type session: SQLAlchemy
795     @param session: Optional SQL session object (a temporary one will be
796     generated if not supplied)
797
798     @rtype: ResultsProxy
799     @return: ResultsProxy object set up to return tuples of (filename, section,
800     package, arch_id)
801     """
802
803     # find me all of the contents for a given suite
804     contents_q = """SELECT (p.path||'/'||n.file) AS fn,
805                             s.section,
806                             b.package,
807                             b.architecture
808                    FROM content_associations c join content_file_paths p ON (c.filepath=p.id)
809                    JOIN content_file_names n ON (c.filename=n.id)
810                    JOIN binaries b ON (b.id=c.binary_pkg)
811                    JOIN override o ON (o.package=b.package)
812                    JOIN section s ON (s.id=o.section)
813                    WHERE o.suite = :suiteid AND o.type = :overridetypeid
814                    AND b.type=:overridetypename"""
815
816     vals = {'suiteid': suite.suite_id,
817             'overridetypeid': overridetype.overridetype_id,
818             'overridetypename': overridetype.overridetype}
819
820     if section is not None:
821         contents_q += " AND s.id = :sectionid"
822         vals['sectionid'] = section.section_id
823
824     contents_q += " ORDER BY fn"
825
826     return session.execute(contents_q, vals)
827
828 __all__.append('get_contents')
829
830 ################################################################################
831
832 class ContentFilepath(object):
833     def __init__(self, *args, **kwargs):
834         pass
835
836     def __repr__(self):
837         return '<ContentFilepath %s>' % self.filepath
838
839 __all__.append('ContentFilepath')
840
841 @session_wrapper
842 def get_or_set_contents_path_id(filepath, session=None):
843     """
844     Returns database id for given path.
845
846     If no matching file is found, a row is inserted.
847
848     @type filepath: string
849     @param filepath: The filepath
850
851     @type session: SQLAlchemy
852     @param session: Optional SQL session object (a temporary one will be
853     generated if not supplied).  If not passed, a commit will be performed at
854     the end of the function, otherwise the caller is responsible for commiting.
855
856     @rtype: int
857     @return: the database id for the given path
858     """
859
860     q = session.query(ContentFilepath).filter_by(filepath=filepath)
861
862     try:
863         ret = q.one().cafilepath_id
864     except NoResultFound:
865         cf = ContentFilepath()
866         cf.filepath = filepath
867         session.add(cf)
868         session.commit_or_flush()
869         ret = cf.cafilepath_id
870
871     return ret
872
873 __all__.append('get_or_set_contents_path_id')
874
875 ################################################################################
876
877 class ContentAssociation(object):
878     def __init__(self, *args, **kwargs):
879         pass
880
881     def __repr__(self):
882         return '<ContentAssociation %s>' % self.ca_id
883
884 __all__.append('ContentAssociation')
885
886 def insert_content_paths(binary_id, fullpaths, session=None):
887     """
888     Make sure given path is associated with given binary id
889
890     @type binary_id: int
891     @param binary_id: the id of the binary
892     @type fullpaths: list
893     @param fullpaths: the list of paths of the file being associated with the binary
894     @type session: SQLAlchemy session
895     @param session: Optional SQLAlchemy session.  If this is passed, the caller
896     is responsible for ensuring a transaction has begun and committing the
897     results or rolling back based on the result code.  If not passed, a commit
898     will be performed at the end of the function, otherwise the caller is
899     responsible for commiting.
900
901     @return: True upon success
902     """
903
904     privatetrans = False
905     if session is None:
906         session = DBConn().session()
907         privatetrans = True
908
909     try:
910         # Insert paths
911         def generate_path_dicts():
912             for fullpath in fullpaths:
913                 if fullpath.startswith( './' ):
914                     fullpath = fullpath[2:]
915
916                 yield {'filename':fullpath, 'id': binary_id }
917
918         for d in generate_path_dicts():
919             session.execute( "INSERT INTO bin_contents ( file, binary_id ) VALUES ( :filename, :id )",
920                          d )
921
922         session.commit()
923         if privatetrans:
924             session.close()
925         return True
926
927     except:
928         traceback.print_exc()
929
930         # Only rollback if we set up the session ourself
931         if privatetrans:
932             session.rollback()
933             session.close()
934
935         return False
936
937 __all__.append('insert_content_paths')
938
939 ################################################################################
940
941 class DSCFile(object):
942     def __init__(self, *args, **kwargs):
943         pass
944
945     def __repr__(self):
946         return '<DSCFile %s>' % self.dscfile_id
947
948 __all__.append('DSCFile')
949
950 @session_wrapper
951 def get_dscfiles(dscfile_id=None, source_id=None, poolfile_id=None, session=None):
952     """
953     Returns a list of DSCFiles which may be empty
954
955     @type dscfile_id: int (optional)
956     @param dscfile_id: the dscfile_id of the DSCFiles to find
957
958     @type source_id: int (optional)
959     @param source_id: the source id related to the DSCFiles to find
960
961     @type poolfile_id: int (optional)
962     @param poolfile_id: the poolfile id related to the DSCFiles to find
963
964     @rtype: list
965     @return: Possibly empty list of DSCFiles
966     """
967
968     q = session.query(DSCFile)
969
970     if dscfile_id is not None:
971         q = q.filter_by(dscfile_id=dscfile_id)
972
973     if source_id is not None:
974         q = q.filter_by(source_id=source_id)
975
976     if poolfile_id is not None:
977         q = q.filter_by(poolfile_id=poolfile_id)
978
979     return q.all()
980
981 __all__.append('get_dscfiles')
982
983 ################################################################################
984
985 class ExternalOverride(ORMObject):
986     def __init__(self, *args, **kwargs):
987         pass
988
989     def __repr__(self):
990         return '<ExternalOverride %s = %s: %s>' % (self.package, self.key, self.value)
991
992 __all__.append('ExternalOverride')
993
994 ################################################################################
995
996 class PoolFile(ORMObject):
997     def __init__(self, filename = None, filesize = -1, \
998         md5sum = None):
999         self.filename = filename
1000         self.filesize = filesize
1001         self.md5sum = md5sum
1002
1003     @property
1004     def fullpath(self):
1005         session = DBConn().session().object_session(self)
1006         af = session.query(ArchiveFile).join(Archive) \
1007                     .filter(ArchiveFile.file == self) \
1008                     .order_by(Archive.tainted.desc()).first()
1009         return af.path
1010
1011     @property
1012     def component(self):
1013         session = DBConn().session().object_session(self)
1014         component_id = session.query(ArchiveFile.component_id).filter(ArchiveFile.file == self) \
1015                               .group_by(ArchiveFile.component_id).one()
1016         return session.query(Component).get(component_id)
1017
1018     @property
1019     def basename(self):
1020         return os.path.basename(self.filename)
1021
1022     def is_valid(self, filesize = -1, md5sum = None):
1023         return self.filesize == long(filesize) and self.md5sum == md5sum
1024
1025     def properties(self):
1026         return ['filename', 'file_id', 'filesize', 'md5sum', 'sha1sum', \
1027             'sha256sum', 'source', 'binary', 'last_used']
1028
1029     def not_null_constraints(self):
1030         return ['filename', 'md5sum']
1031
1032     def identical_to(self, filename):
1033         """
1034         compare size and hash with the given file
1035
1036         @rtype: bool
1037         @return: true if the given file has the same size and hash as this object; false otherwise
1038         """
1039         st = os.stat(filename)
1040         if self.filesize != st.st_size:
1041             return False
1042
1043         f = open(filename, "r")
1044         sha256sum = apt_pkg.sha256sum(f)
1045         if sha256sum != self.sha256sum:
1046             return False
1047
1048         return True
1049
1050 __all__.append('PoolFile')
1051
1052 @session_wrapper
1053 def get_poolfile_like_name(filename, session=None):
1054     """
1055     Returns an array of PoolFile objects which are like the given name
1056
1057     @type filename: string
1058     @param filename: the filename of the file to check against the DB
1059
1060     @rtype: array
1061     @return: array of PoolFile objects
1062     """
1063
1064     # TODO: There must be a way of properly using bind parameters with %FOO%
1065     q = session.query(PoolFile).filter(PoolFile.filename.like('%%/%s' % filename))
1066
1067     return q.all()
1068
1069 __all__.append('get_poolfile_like_name')
1070
1071 ################################################################################
1072
1073 class Fingerprint(ORMObject):
1074     def __init__(self, fingerprint = None):
1075         self.fingerprint = fingerprint
1076
1077     def properties(self):
1078         return ['fingerprint', 'fingerprint_id', 'keyring', 'uid', \
1079             'binary_reject']
1080
1081     def not_null_constraints(self):
1082         return ['fingerprint']
1083
1084 __all__.append('Fingerprint')
1085
1086 @session_wrapper
1087 def get_fingerprint(fpr, session=None):
1088     """
1089     Returns Fingerprint object for given fpr.
1090
1091     @type fpr: string
1092     @param fpr: The fpr to find / add
1093
1094     @type session: SQLAlchemy
1095     @param session: Optional SQL session object (a temporary one will be
1096     generated if not supplied).
1097
1098     @rtype: Fingerprint
1099     @return: the Fingerprint object for the given fpr or None
1100     """
1101
1102     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1103
1104     try:
1105         ret = q.one()
1106     except NoResultFound:
1107         ret = None
1108
1109     return ret
1110
1111 __all__.append('get_fingerprint')
1112
1113 @session_wrapper
1114 def get_or_set_fingerprint(fpr, session=None):
1115     """
1116     Returns Fingerprint object for given fpr.
1117
1118     If no matching fpr is found, a row is inserted.
1119
1120     @type fpr: string
1121     @param fpr: The fpr to find / add
1122
1123     @type session: SQLAlchemy
1124     @param session: Optional SQL session object (a temporary one will be
1125     generated if not supplied).  If not passed, a commit will be performed at
1126     the end of the function, otherwise the caller is responsible for commiting.
1127     A flush will be performed either way.
1128
1129     @rtype: Fingerprint
1130     @return: the Fingerprint object for the given fpr
1131     """
1132
1133     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1134
1135     try:
1136         ret = q.one()
1137     except NoResultFound:
1138         fingerprint = Fingerprint()
1139         fingerprint.fingerprint = fpr
1140         session.add(fingerprint)
1141         session.commit_or_flush()
1142         ret = fingerprint
1143
1144     return ret
1145
1146 __all__.append('get_or_set_fingerprint')
1147
1148 ################################################################################
1149
1150 # Helper routine for Keyring class
1151 def get_ldap_name(entry):
1152     name = []
1153     for k in ["cn", "mn", "sn"]:
1154         ret = entry.get(k)
1155         if ret and ret[0] != "" and ret[0] != "-":
1156             name.append(ret[0])
1157     return " ".join(name)
1158
1159 ################################################################################
1160
1161 class Keyring(object):
1162     gpg_invocation = "gpg --no-default-keyring --keyring %s" +\
1163                      " --with-colons --fingerprint --fingerprint"
1164
1165     keys = {}
1166     fpr_lookup = {}
1167
1168     def __init__(self, *args, **kwargs):
1169         pass
1170
1171     def __repr__(self):
1172         return '<Keyring %s>' % self.keyring_name
1173
1174     def de_escape_gpg_str(self, txt):
1175         esclist = re.split(r'(\\x..)', txt)
1176         for x in range(1,len(esclist),2):
1177             esclist[x] = "%c" % (int(esclist[x][2:],16))
1178         return "".join(esclist)
1179
1180     def parse_address(self, uid):
1181         """parses uid and returns a tuple of real name and email address"""
1182         import email.Utils
1183         (name, address) = email.Utils.parseaddr(uid)
1184         name = re.sub(r"\s*[(].*[)]", "", name)
1185         name = self.de_escape_gpg_str(name)
1186         if name == "":
1187             name = uid
1188         return (name, address)
1189
1190     def load_keys(self, keyring):
1191         if not self.keyring_id:
1192             raise Exception('Must be initialized with database information')
1193
1194         k = os.popen(self.gpg_invocation % keyring, "r")
1195         key = None
1196         need_fingerprint = False
1197
1198         for line in k:
1199             field = line.split(":")
1200             if field[0] == "pub":
1201                 key = field[4]
1202                 self.keys[key] = {}
1203                 (name, addr) = self.parse_address(field[9])
1204                 if "@" in addr:
1205                     self.keys[key]["email"] = addr
1206                     self.keys[key]["name"] = name
1207                 need_fingerprint = True
1208             elif key and field[0] == "uid":
1209                 (name, addr) = self.parse_address(field[9])
1210                 if "email" not in self.keys[key] and "@" in addr:
1211                     self.keys[key]["email"] = addr
1212                     self.keys[key]["name"] = name
1213             elif need_fingerprint and field[0] == "fpr":
1214                 self.keys[key]["fingerprints"] = [field[9]]
1215                 self.fpr_lookup[field[9]] = key
1216                 need_fingerprint = False
1217
1218     def import_users_from_ldap(self, session):
1219         import ldap
1220         cnf = Config()
1221
1222         LDAPDn = cnf["Import-LDAP-Fingerprints::LDAPDn"]
1223         LDAPServer = cnf["Import-LDAP-Fingerprints::LDAPServer"]
1224         ca_cert_file = cnf.get('Import-LDAP-Fingerprints::CACertFile')
1225
1226         l = ldap.open(LDAPServer)
1227
1228         if ca_cert_file:
1229             # TODO: This should request a new context and use
1230             # connection-specific options (i.e. "l.set_option(...)")
1231
1232             # Request a new TLS context. If there was already one, libldap
1233             # would not change the TLS options (like which CAs to trust).
1234             #l.set_option(ldap.OPT_X_TLS_NEWCTX, True)
1235             ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_HARD)
1236             #ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, None)
1237             ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ca_cert_file)
1238             l.start_tls_s()
1239
1240         l.simple_bind_s("","")
1241         Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
1242                "(&(keyfingerprint=*)(gidnumber=%s))" % (cnf["Import-Users-From-Passwd::ValidGID"]),
1243                ["uid", "keyfingerprint", "cn", "mn", "sn"])
1244
1245         ldap_fin_uid_id = {}
1246
1247         byuid = {}
1248         byname = {}
1249
1250         for i in Attrs:
1251             entry = i[1]
1252             uid = entry["uid"][0]
1253             name = get_ldap_name(entry)
1254             fingerprints = entry["keyFingerPrint"]
1255             keyid = None
1256             for f in fingerprints:
1257                 key = self.fpr_lookup.get(f, None)
1258                 if key not in self.keys:
1259                     continue
1260                 self.keys[key]["uid"] = uid
1261
1262                 if keyid != None:
1263                     continue
1264                 keyid = get_or_set_uid(uid, session).uid_id
1265                 byuid[keyid] = (uid, name)
1266                 byname[uid] = (keyid, name)
1267
1268         return (byname, byuid)
1269
1270     def generate_users_from_keyring(self, format, session):
1271         byuid = {}
1272         byname = {}
1273         any_invalid = False
1274         for x in self.keys.keys():
1275             if "email" not in self.keys[x]:
1276                 any_invalid = True
1277                 self.keys[x]["uid"] = format % "invalid-uid"
1278             else:
1279                 uid = format % self.keys[x]["email"]
1280                 keyid = get_or_set_uid(uid, session).uid_id
1281                 byuid[keyid] = (uid, self.keys[x]["name"])
1282                 byname[uid] = (keyid, self.keys[x]["name"])
1283                 self.keys[x]["uid"] = uid
1284
1285         if any_invalid:
1286             uid = format % "invalid-uid"
1287             keyid = get_or_set_uid(uid, session).uid_id
1288             byuid[keyid] = (uid, "ungeneratable user id")
1289             byname[uid] = (keyid, "ungeneratable user id")
1290
1291         return (byname, byuid)
1292
1293 __all__.append('Keyring')
1294
1295 @session_wrapper
1296 def get_keyring(keyring, session=None):
1297     """
1298     If C{keyring} does not have an entry in the C{keyrings} table yet, return None
1299     If C{keyring} already has an entry, simply return the existing Keyring
1300
1301     @type keyring: string
1302     @param keyring: the keyring name
1303
1304     @rtype: Keyring
1305     @return: the Keyring object for this keyring
1306     """
1307
1308     q = session.query(Keyring).filter_by(keyring_name=keyring)
1309
1310     try:
1311         return q.one()
1312     except NoResultFound:
1313         return None
1314
1315 __all__.append('get_keyring')
1316
1317 @session_wrapper
1318 def get_active_keyring_paths(session=None):
1319     """
1320     @rtype: list
1321     @return: list of active keyring paths
1322     """
1323     return [ x.keyring_name for x in session.query(Keyring).filter(Keyring.active == True).order_by(desc(Keyring.priority)).all() ]
1324
1325 __all__.append('get_active_keyring_paths')
1326
1327 @session_wrapper
1328 def get_primary_keyring_path(session=None):
1329     """
1330     Get the full path to the highest priority active keyring
1331
1332     @rtype: str or None
1333     @return: path to the active keyring with the highest priority or None if no
1334              keyring is configured
1335     """
1336     keyrings = get_active_keyring_paths()
1337
1338     if len(keyrings) > 0:
1339         return keyrings[0]
1340     else:
1341         return None
1342
1343 __all__.append('get_primary_keyring_path')
1344
1345 ################################################################################
1346
1347 class DBChange(object):
1348     def __init__(self, *args, **kwargs):
1349         pass
1350
1351     def __repr__(self):
1352         return '<DBChange %s>' % self.changesname
1353
1354 __all__.append('DBChange')
1355
1356 @session_wrapper
1357 def get_dbchange(filename, session=None):
1358     """
1359     returns DBChange object for given C{filename}.
1360
1361     @type filename: string
1362     @param filename: the name of the file
1363
1364     @type session: Session
1365     @param session: Optional SQLA session object (a temporary one will be
1366     generated if not supplied)
1367
1368     @rtype: DBChange
1369     @return:  DBChange object for the given filename (C{None} if not present)
1370
1371     """
1372     q = session.query(DBChange).filter_by(changesname=filename)
1373
1374     try:
1375         return q.one()
1376     except NoResultFound:
1377         return None
1378
1379 __all__.append('get_dbchange')
1380
1381 ################################################################################
1382
1383 class Maintainer(ORMObject):
1384     def __init__(self, name = None):
1385         self.name = name
1386
1387     def properties(self):
1388         return ['name', 'maintainer_id']
1389
1390     def not_null_constraints(self):
1391         return ['name']
1392
1393     def get_split_maintainer(self):
1394         if not hasattr(self, 'name') or self.name is None:
1395             return ('', '', '', '')
1396
1397         return fix_maintainer(self.name.strip())
1398
1399 __all__.append('Maintainer')
1400
1401 @session_wrapper
1402 def get_or_set_maintainer(name, session=None):
1403     """
1404     Returns Maintainer object for given maintainer name.
1405
1406     If no matching maintainer name is found, a row is inserted.
1407
1408     @type name: string
1409     @param name: The maintainer name to add
1410
1411     @type session: SQLAlchemy
1412     @param session: Optional SQL session object (a temporary one will be
1413     generated if not supplied).  If not passed, a commit will be performed at
1414     the end of the function, otherwise the caller is responsible for commiting.
1415     A flush will be performed either way.
1416
1417     @rtype: Maintainer
1418     @return: the Maintainer object for the given maintainer
1419     """
1420
1421     q = session.query(Maintainer).filter_by(name=name)
1422     try:
1423         ret = q.one()
1424     except NoResultFound:
1425         maintainer = Maintainer()
1426         maintainer.name = name
1427         session.add(maintainer)
1428         session.commit_or_flush()
1429         ret = maintainer
1430
1431     return ret
1432
1433 __all__.append('get_or_set_maintainer')
1434
1435 @session_wrapper
1436 def get_maintainer(maintainer_id, session=None):
1437     """
1438     Return the name of the maintainer behind C{maintainer_id} or None if that
1439     maintainer_id is invalid.
1440
1441     @type maintainer_id: int
1442     @param maintainer_id: the id of the maintainer
1443
1444     @rtype: Maintainer
1445     @return: the Maintainer with this C{maintainer_id}
1446     """
1447
1448     return session.query(Maintainer).get(maintainer_id)
1449
1450 __all__.append('get_maintainer')
1451
1452 ################################################################################
1453
1454 class NewComment(object):
1455     def __init__(self, *args, **kwargs):
1456         pass
1457
1458     def __repr__(self):
1459         return '''<NewComment for '%s %s' (%s)>''' % (self.package, self.version, self.comment_id)
1460
1461 __all__.append('NewComment')
1462
1463 @session_wrapper
1464 def has_new_comment(policy_queue, package, version, session=None):
1465     """
1466     Returns true if the given combination of C{package}, C{version} has a comment.
1467
1468     @type package: string
1469     @param package: name of the package
1470
1471     @type version: string
1472     @param version: package version
1473
1474     @type session: Session
1475     @param session: Optional SQLA session object (a temporary one will be
1476     generated if not supplied)
1477
1478     @rtype: boolean
1479     @return: true/false
1480     """
1481
1482     q = session.query(NewComment).filter_by(policy_queue=policy_queue)
1483     q = q.filter_by(package=package)
1484     q = q.filter_by(version=version)
1485
1486     return bool(q.count() > 0)
1487
1488 __all__.append('has_new_comment')
1489
1490 @session_wrapper
1491 def get_new_comments(policy_queue, package=None, version=None, comment_id=None, session=None):
1492     """
1493     Returns (possibly empty) list of NewComment objects for the given
1494     parameters
1495
1496     @type package: string (optional)
1497     @param package: name of the package
1498
1499     @type version: string (optional)
1500     @param version: package version
1501
1502     @type comment_id: int (optional)
1503     @param comment_id: An id of a comment
1504
1505     @type session: Session
1506     @param session: Optional SQLA session object (a temporary one will be
1507     generated if not supplied)
1508
1509     @rtype: list
1510     @return: A (possibly empty) list of NewComment objects will be returned
1511     """
1512
1513     q = session.query(NewComment).filter_by(policy_queue=policy_queue)
1514     if package is not None: q = q.filter_by(package=package)
1515     if version is not None: q = q.filter_by(version=version)
1516     if comment_id is not None: q = q.filter_by(comment_id=comment_id)
1517
1518     return q.all()
1519
1520 __all__.append('get_new_comments')
1521
1522 ################################################################################
1523
1524 class Override(ORMObject):
1525     def __init__(self, package = None, suite = None, component = None, overridetype = None, \
1526         section = None, priority = None):
1527         self.package = package
1528         self.suite = suite
1529         self.component = component
1530         self.overridetype = overridetype
1531         self.section = section
1532         self.priority = priority
1533
1534     def properties(self):
1535         return ['package', 'suite', 'component', 'overridetype', 'section', \
1536             'priority']
1537
1538     def not_null_constraints(self):
1539         return ['package', 'suite', 'component', 'overridetype', 'section']
1540
1541 __all__.append('Override')
1542
1543 @session_wrapper
1544 def get_override(package, suite=None, component=None, overridetype=None, session=None):
1545     """
1546     Returns Override object for the given parameters
1547
1548     @type package: string
1549     @param package: The name of the package
1550
1551     @type suite: string, list or None
1552     @param suite: The name of the suite (or suites if a list) to limit to.  If
1553                   None, don't limit.  Defaults to None.
1554
1555     @type component: string, list or None
1556     @param component: The name of the component (or components if a list) to
1557                       limit to.  If None, don't limit.  Defaults to None.
1558
1559     @type overridetype: string, list or None
1560     @param overridetype: The name of the overridetype (or overridetypes if a list) to
1561                          limit to.  If None, don't limit.  Defaults to None.
1562
1563     @type session: Session
1564     @param session: Optional SQLA session object (a temporary one will be
1565     generated if not supplied)
1566
1567     @rtype: list
1568     @return: A (possibly empty) list of Override objects will be returned
1569     """
1570
1571     q = session.query(Override)
1572     q = q.filter_by(package=package)
1573
1574     if suite is not None:
1575         if not isinstance(suite, list): suite = [suite]
1576         q = q.join(Suite).filter(Suite.suite_name.in_(suite))
1577
1578     if component is not None:
1579         if not isinstance(component, list): component = [component]
1580         q = q.join(Component).filter(Component.component_name.in_(component))
1581
1582     if overridetype is not None:
1583         if not isinstance(overridetype, list): overridetype = [overridetype]
1584         q = q.join(OverrideType).filter(OverrideType.overridetype.in_(overridetype))
1585
1586     return q.all()
1587
1588 __all__.append('get_override')
1589
1590
1591 ################################################################################
1592
1593 class OverrideType(ORMObject):
1594     def __init__(self, overridetype = None):
1595         self.overridetype = overridetype
1596
1597     def properties(self):
1598         return ['overridetype', 'overridetype_id', 'overrides_count']
1599
1600     def not_null_constraints(self):
1601         return ['overridetype']
1602
1603 __all__.append('OverrideType')
1604
1605 @session_wrapper
1606 def get_override_type(override_type, session=None):
1607     """
1608     Returns OverrideType object for given C{override type}.
1609
1610     @type override_type: string
1611     @param override_type: The name of the override type
1612
1613     @type session: Session
1614     @param session: Optional SQLA session object (a temporary one will be
1615     generated if not supplied)
1616
1617     @rtype: int
1618     @return: the database id for the given override type
1619     """
1620
1621     q = session.query(OverrideType).filter_by(overridetype=override_type)
1622
1623     try:
1624         return q.one()
1625     except NoResultFound:
1626         return None
1627
1628 __all__.append('get_override_type')
1629
1630 ################################################################################
1631
1632 class PolicyQueue(object):
1633     def __init__(self, *args, **kwargs):
1634         pass
1635
1636     def __repr__(self):
1637         return '<PolicyQueue %s>' % self.queue_name
1638
1639 __all__.append('PolicyQueue')
1640
1641 @session_wrapper
1642 def get_policy_queue(queuename, session=None):
1643     """
1644     Returns PolicyQueue object for given C{queue name}
1645
1646     @type queuename: string
1647     @param queuename: The name of the queue
1648
1649     @type session: Session
1650     @param session: Optional SQLA session object (a temporary one will be
1651     generated if not supplied)
1652
1653     @rtype: PolicyQueue
1654     @return: PolicyQueue object for the given queue
1655     """
1656
1657     q = session.query(PolicyQueue).filter_by(queue_name=queuename)
1658
1659     try:
1660         return q.one()
1661     except NoResultFound:
1662         return None
1663
1664 __all__.append('get_policy_queue')
1665
1666 ################################################################################
1667
1668 class PolicyQueueUpload(object):
1669     def __cmp__(self, other):
1670         ret = cmp(self.changes.source, other.changes.source)
1671         if ret == 0:
1672             ret = apt_pkg.version_compare(self.changes.version, other.changes.version)
1673         if ret == 0:
1674             if self.source is not None and other.source is None:
1675                 ret = -1
1676             elif self.source is None and other.source is not None:
1677                 ret = 1
1678         if ret == 0:
1679             ret = cmp(self.changes.changesname, other.changes.changesname)
1680         return ret
1681
1682 __all__.append('PolicyQueueUpload')
1683
1684 ################################################################################
1685
1686 class PolicyQueueByhandFile(object):
1687     pass
1688
1689 __all__.append('PolicyQueueByhandFile')
1690
1691 ################################################################################
1692
1693 class Priority(ORMObject):
1694     def __init__(self, priority = None, level = None):
1695         self.priority = priority
1696         self.level = level
1697
1698     def properties(self):
1699         return ['priority', 'priority_id', 'level', 'overrides_count']
1700
1701     def not_null_constraints(self):
1702         return ['priority', 'level']
1703
1704     def __eq__(self, val):
1705         if isinstance(val, str):
1706             return (self.priority == val)
1707         # This signals to use the normal comparison operator
1708         return NotImplemented
1709
1710     def __ne__(self, val):
1711         if isinstance(val, str):
1712             return (self.priority != val)
1713         # This signals to use the normal comparison operator
1714         return NotImplemented
1715
1716 __all__.append('Priority')
1717
1718 @session_wrapper
1719 def get_priority(priority, session=None):
1720     """
1721     Returns Priority object for given C{priority name}.
1722
1723     @type priority: string
1724     @param priority: The name of the priority
1725
1726     @type session: Session
1727     @param session: Optional SQLA session object (a temporary one will be
1728     generated if not supplied)
1729
1730     @rtype: Priority
1731     @return: Priority object for the given priority
1732     """
1733
1734     q = session.query(Priority).filter_by(priority=priority)
1735
1736     try:
1737         return q.one()
1738     except NoResultFound:
1739         return None
1740
1741 __all__.append('get_priority')
1742
1743 @session_wrapper
1744 def get_priorities(session=None):
1745     """
1746     Returns dictionary of priority names -> id mappings
1747
1748     @type session: Session
1749     @param session: Optional SQL session object (a temporary one will be
1750     generated if not supplied)
1751
1752     @rtype: dictionary
1753     @return: dictionary of priority names -> id mappings
1754     """
1755
1756     ret = {}
1757     q = session.query(Priority)
1758     for x in q.all():
1759         ret[x.priority] = x.priority_id
1760
1761     return ret
1762
1763 __all__.append('get_priorities')
1764
1765 ################################################################################
1766
1767 class Section(ORMObject):
1768     def __init__(self, section = None):
1769         self.section = section
1770
1771     def properties(self):
1772         return ['section', 'section_id', 'overrides_count']
1773
1774     def not_null_constraints(self):
1775         return ['section']
1776
1777     def __eq__(self, val):
1778         if isinstance(val, str):
1779             return (self.section == val)
1780         # This signals to use the normal comparison operator
1781         return NotImplemented
1782
1783     def __ne__(self, val):
1784         if isinstance(val, str):
1785             return (self.section != val)
1786         # This signals to use the normal comparison operator
1787         return NotImplemented
1788
1789 __all__.append('Section')
1790
1791 @session_wrapper
1792 def get_section(section, session=None):
1793     """
1794     Returns Section object for given C{section name}.
1795
1796     @type section: string
1797     @param section: The name of the section
1798
1799     @type session: Session
1800     @param session: Optional SQLA session object (a temporary one will be
1801     generated if not supplied)
1802
1803     @rtype: Section
1804     @return: Section object for the given section name
1805     """
1806
1807     q = session.query(Section).filter_by(section=section)
1808
1809     try:
1810         return q.one()
1811     except NoResultFound:
1812         return None
1813
1814 __all__.append('get_section')
1815
1816 @session_wrapper
1817 def get_sections(session=None):
1818     """
1819     Returns dictionary of section names -> id mappings
1820
1821     @type session: Session
1822     @param session: Optional SQL session object (a temporary one will be
1823     generated if not supplied)
1824
1825     @rtype: dictionary
1826     @return: dictionary of section names -> id mappings
1827     """
1828
1829     ret = {}
1830     q = session.query(Section)
1831     for x in q.all():
1832         ret[x.section] = x.section_id
1833
1834     return ret
1835
1836 __all__.append('get_sections')
1837
1838 ################################################################################
1839
1840 class SignatureHistory(ORMObject):
1841     @classmethod
1842     def from_signed_file(cls, signed_file):
1843         """signature history entry from signed file
1844
1845         @type  signed_file: L{daklib.gpg.SignedFile}
1846         @param signed_file: signed file
1847
1848         @rtype: L{SignatureHistory}
1849         """
1850         self = cls()
1851         self.fingerprint = signed_file.primary_fingerprint
1852         self.signature_timestamp = signed_file.signature_timestamp
1853         self.contents_sha1 = signed_file.contents_sha1()
1854         return self
1855
1856 __all__.append('SignatureHistory')
1857
1858 ################################################################################
1859
1860 class SrcContents(ORMObject):
1861     def __init__(self, file = None, source = None):
1862         self.file = file
1863         self.source = source
1864
1865     def properties(self):
1866         return ['file', 'source']
1867
1868 __all__.append('SrcContents')
1869
1870 ################################################################################
1871
1872 from debian.debfile import Deb822
1873
1874 # Temporary Deb822 subclass to fix bugs with : handling; see #597249
1875 class Dak822(Deb822):
1876     def _internal_parser(self, sequence, fields=None):
1877         # The key is non-whitespace, non-colon characters before any colon.
1878         key_part = r"^(?P<key>[^: \t\n\r\f\v]+)\s*:\s*"
1879         single = re.compile(key_part + r"(?P<data>\S.*?)\s*$")
1880         multi = re.compile(key_part + r"$")
1881         multidata = re.compile(r"^\s(?P<data>.+?)\s*$")
1882
1883         wanted_field = lambda f: fields is None or f in fields
1884
1885         if isinstance(sequence, basestring):
1886             sequence = sequence.splitlines()
1887
1888         curkey = None
1889         content = ""
1890         for line in self.gpg_stripped_paragraph(sequence):
1891             m = single.match(line)
1892             if m:
1893                 if curkey:
1894                     self[curkey] = content
1895
1896                 if not wanted_field(m.group('key')):
1897                     curkey = None
1898                     continue
1899
1900                 curkey = m.group('key')
1901                 content = m.group('data')
1902                 continue
1903
1904             m = multi.match(line)
1905             if m:
1906                 if curkey:
1907                     self[curkey] = content
1908
1909                 if not wanted_field(m.group('key')):
1910                     curkey = None
1911                     continue
1912
1913                 curkey = m.group('key')
1914                 content = ""
1915                 continue
1916
1917             m = multidata.match(line)
1918             if m:
1919                 content += '\n' + line # XXX not m.group('data')?
1920                 continue
1921
1922         if curkey:
1923             self[curkey] = content
1924
1925
1926 class DBSource(ORMObject):
1927     def __init__(self, source = None, version = None, maintainer = None, \
1928         changedby = None, poolfile = None, install_date = None, fingerprint = None):
1929         self.source = source
1930         self.version = version
1931         self.maintainer = maintainer
1932         self.changedby = changedby
1933         self.poolfile = poolfile
1934         self.install_date = install_date
1935         self.fingerprint = fingerprint
1936
1937     @property
1938     def pkid(self):
1939         return self.source_id
1940
1941     def properties(self):
1942         return ['source', 'source_id', 'maintainer', 'changedby', \
1943             'fingerprint', 'poolfile', 'version', 'suites_count', \
1944             'install_date', 'binaries_count', 'uploaders_count']
1945
1946     def not_null_constraints(self):
1947         return ['source', 'version', 'install_date', 'maintainer', \
1948             'changedby', 'poolfile']
1949
1950     def read_control_fields(self):
1951         '''
1952         Reads the control information from a dsc
1953
1954         @rtype: tuple
1955         @return: fields is the dsc information in a dictionary form
1956         '''
1957         fullpath = self.poolfile.fullpath
1958         fields = Dak822(open(self.poolfile.fullpath, 'r'))
1959         return fields
1960
1961     metadata = association_proxy('key', 'value')
1962
1963     def scan_contents(self):
1964         '''
1965         Returns a set of names for non directories. The path names are
1966         normalized after converting them from either utf-8 or iso8859-1
1967         encoding.
1968         '''
1969         fullpath = self.poolfile.fullpath
1970         from daklib.contents import UnpackedSource
1971         unpacked = UnpackedSource(fullpath)
1972         fileset = set()
1973         for name in unpacked.get_all_filenames():
1974             # enforce proper utf-8 encoding
1975             try:
1976                 name.decode('utf-8')
1977             except UnicodeDecodeError:
1978                 name = name.decode('iso8859-1').encode('utf-8')
1979             fileset.add(name)
1980         return fileset
1981
1982 __all__.append('DBSource')
1983
1984 @session_wrapper
1985 def source_exists(source, source_version, suites = ["any"], session=None):
1986     """
1987     Ensure that source exists somewhere in the archive for the binary
1988     upload being processed.
1989       1. exact match     => 1.0-3
1990       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
1991
1992     @type source: string
1993     @param source: source name
1994
1995     @type source_version: string
1996     @param source_version: expected source version
1997
1998     @type suites: list
1999     @param suites: list of suites to check in, default I{any}
2000
2001     @type session: Session
2002     @param session: Optional SQLA session object (a temporary one will be
2003     generated if not supplied)
2004
2005     @rtype: int
2006     @return: returns 1 if a source with expected version is found, otherwise 0
2007
2008     """
2009
2010     cnf = Config()
2011     ret = True
2012
2013     from daklib.regexes import re_bin_only_nmu
2014     orig_source_version = re_bin_only_nmu.sub('', source_version)
2015
2016     for suite in suites:
2017         q = session.query(DBSource).filter_by(source=source). \
2018             filter(DBSource.version.in_([source_version, orig_source_version]))
2019         if suite != "any":
2020             # source must exist in 'suite' or a suite that is enhanced by 'suite'
2021             s = get_suite(suite, session)
2022             if s:
2023                 enhances_vcs = session.query(VersionCheck).filter(VersionCheck.suite==s).filter_by(check='Enhances')
2024                 considered_suites = [ vc.reference for vc in enhances_vcs ]
2025                 considered_suites.append(s)
2026
2027                 q = q.filter(DBSource.suites.any(Suite.suite_id.in_([s.suite_id for s in considered_suites])))
2028
2029         if q.count() > 0:
2030             continue
2031
2032         # No source found so return not ok
2033         ret = False
2034
2035     return ret
2036
2037 __all__.append('source_exists')
2038
2039 @session_wrapper
2040 def get_suites_source_in(source, session=None):
2041     """
2042     Returns list of Suite objects which given C{source} name is in
2043
2044     @type source: str
2045     @param source: DBSource package name to search for
2046
2047     @rtype: list
2048     @return: list of Suite objects for the given source
2049     """
2050
2051     return session.query(Suite).filter(Suite.sources.any(source=source)).all()
2052
2053 __all__.append('get_suites_source_in')
2054
2055 @session_wrapper
2056 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
2057     """
2058     Returns list of DBSource objects for given C{source} name and other parameters
2059
2060     @type source: str
2061     @param source: DBSource package name to search for
2062
2063     @type version: str or None
2064     @param version: DBSource version name to search for or None if not applicable
2065
2066     @type dm_upload_allowed: bool
2067     @param dm_upload_allowed: If None, no effect.  If True or False, only
2068     return packages with that dm_upload_allowed setting
2069
2070     @type session: Session
2071     @param session: Optional SQL session object (a temporary one will be
2072     generated if not supplied)
2073
2074     @rtype: list
2075     @return: list of DBSource objects for the given name (may be empty)
2076     """
2077
2078     q = session.query(DBSource).filter_by(source=source)
2079
2080     if version is not None:
2081         q = q.filter_by(version=version)
2082
2083     if dm_upload_allowed is not None:
2084         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
2085
2086     return q.all()
2087
2088 __all__.append('get_sources_from_name')
2089
2090 # FIXME: This function fails badly if it finds more than 1 source package and
2091 # its implementation is trivial enough to be inlined.
2092 @session_wrapper
2093 def get_source_in_suite(source, suite, session=None):
2094     """
2095     Returns a DBSource object for a combination of C{source} and C{suite}.
2096
2097       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2098       - B{suite} - a suite name, eg. I{unstable}
2099
2100     @type source: string
2101     @param source: source package name
2102
2103     @type suite: string
2104     @param suite: the suite name
2105
2106     @rtype: string
2107     @return: the version for I{source} in I{suite}
2108
2109     """
2110
2111     q = get_suite(suite, session).get_sources(source)
2112     try:
2113         return q.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')