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