]> git.donarmstrong.com Git - dak.git/blob - daklib/dbconn.py
e55c283daa58444193839cb50ffc7810337d2224
[dak.git] / daklib / dbconn.py
1 #!/usr/bin/python
2
3 """ DB access class
4
5 @contact: Debian FTPMaster <ftpmaster@debian.org>
6 @copyright: 2000, 2001, 2002, 2003, 2004, 2006  James Troup <james@nocrew.org>
7 @copyright: 2008-2009  Mark Hymers <mhy@debian.org>
8 @copyright: 2009, 2010  Joerg Jaspert <joerg@debian.org>
9 @copyright: 2009  Mike O'Connor <stew@debian.org>
10 @license: GNU General Public License version 2 or later
11 """
12
13 # This program is free software; you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation; either version 2 of the License, or
16 # (at your option) any later version.
17
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 # GNU General Public License for more details.
22
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
26
27 ################################################################################
28
29 # < mhy> I need a funny comment
30 # < sgran> two peanuts were walking down a dark street
31 # < sgran> one was a-salted
32 #  * mhy looks up the definition of "funny"
33
34 ################################################################################
35
36 import apt_pkg
37 import daklib.daksubprocess
38 import os
39 from os.path import normpath
40 import re
41 import psycopg2
42 import subprocess
43 import traceback
44
45 try:
46     # python >= 2.6
47     import json
48 except:
49     # python <= 2.5
50     import simplejson as json
51
52 from datetime import datetime, timedelta
53 from errno import ENOENT
54 from tempfile import mkstemp, mkdtemp
55 from tarfile import TarFile
56
57 from inspect import getargspec
58
59 import sqlalchemy
60 from sqlalchemy import create_engine, Table, MetaData, Column, Integer, desc, \
61     Text, ForeignKey
62 from sqlalchemy.orm import sessionmaker, mapper, relation, object_session, \
63     backref, MapperExtension, EXT_CONTINUE, object_mapper, clear_mappers
64 from sqlalchemy import types as sqltypes
65 from sqlalchemy.orm.collections import attribute_mapped_collection
66 from sqlalchemy.ext.associationproxy import association_proxy
67
68 # Don't remove this, we re-export the exceptions to scripts which import us
69 from sqlalchemy.exc import *
70 from sqlalchemy.orm.exc import NoResultFound
71
72 # Only import Config until Queue stuff is changed to store its config
73 # in the database
74 from config import Config
75 from textutils import fix_maintainer
76 from dak_exceptions import DBUpdateError, NoSourceFieldError, FileExistsError
77
78 # suppress some deprecation warnings in squeeze related to sqlalchemy
79 import warnings
80 warnings.filterwarnings('ignore', \
81     "The SQLAlchemy PostgreSQL dialect has been renamed from 'postgres' to 'postgresql'.*", \
82     SADeprecationWarning)
83 warnings.filterwarnings('ignore', \
84     "Predicate of partial index .* ignored during reflection", \
85     SAWarning)
86
87
88 ################################################################################
89
90 # Patch in support for the debversion field type so that it works during
91 # reflection
92
93 try:
94     # that is for sqlalchemy 0.6
95     UserDefinedType = sqltypes.UserDefinedType
96 except:
97     # this one for sqlalchemy 0.5
98     UserDefinedType = sqltypes.TypeEngine
99
100 class DebVersion(UserDefinedType):
101     def get_col_spec(self):
102         return "DEBVERSION"
103
104     def bind_processor(self, dialect):
105         return None
106
107     # ' = None' is needed for sqlalchemy 0.5:
108     def result_processor(self, dialect, coltype = None):
109         return None
110
111 sa_major_version = sqlalchemy.__version__[0:3]
112 if sa_major_version in ["0.5", "0.6", "0.7", "0.8", "0.9"]:
113     from sqlalchemy.databases import postgres
114     postgres.ischema_names['debversion'] = DebVersion
115 else:
116     raise Exception("dak only ported to SQLA versions 0.5 to 0.9.  See daklib/dbconn.py")
117
118 ################################################################################
119
120 __all__ = ['IntegrityError', 'SQLAlchemyError', 'DebVersion']
121
122 ################################################################################
123
124 def session_wrapper(fn):
125     """
126     Wrapper around common ".., session=None):" handling. If the wrapped
127     function is called without passing 'session', we create a local one
128     and destroy it when the function ends.
129
130     Also attaches a commit_or_flush method to the session; if we created a
131     local session, this is a synonym for session.commit(), otherwise it is a
132     synonym for session.flush().
133     """
134
135     def wrapped(*args, **kwargs):
136         private_transaction = False
137
138         # Find the session object
139         session = kwargs.get('session')
140
141         if session is None:
142             if len(args) <= len(getargspec(fn)[0]) - 1:
143                 # No session specified as last argument or in kwargs
144                 private_transaction = True
145                 session = kwargs['session'] = DBConn().session()
146             else:
147                 # Session is last argument in args
148                 session = args[-1]
149                 if session is None:
150                     args = list(args)
151                     session = args[-1] = DBConn().session()
152                     private_transaction = True
153
154         if private_transaction:
155             session.commit_or_flush = session.commit
156         else:
157             session.commit_or_flush = session.flush
158
159         try:
160             return fn(*args, **kwargs)
161         finally:
162             if private_transaction:
163                 # We created a session; close it.
164                 session.close()
165
166     wrapped.__doc__ = fn.__doc__
167     wrapped.func_name = fn.func_name
168
169     return wrapped
170
171 __all__.append('session_wrapper')
172
173 ################################################################################
174
175 class ORMObject(object):
176     """
177     ORMObject is a base class for all ORM classes mapped by SQLalchemy. All
178     derived classes must implement the properties() method.
179     """
180
181     def properties(self):
182         '''
183         This method should be implemented by all derived classes and returns a
184         list of the important properties. The properties 'created' and
185         'modified' will be added automatically. A suffix '_count' should be
186         added to properties that are lists or query objects. The most important
187         property name should be returned as the first element in the list
188         because it is used by repr().
189         '''
190         return []
191
192     def json(self):
193         '''
194         Returns a JSON representation of the object based on the properties
195         returned from the properties() method.
196         '''
197         data = {}
198         # add created and modified
199         all_properties = self.properties() + ['created', 'modified']
200         for property in all_properties:
201             # check for list or query
202             if property[-6:] == '_count':
203                 real_property = property[:-6]
204                 if not hasattr(self, real_property):
205                     continue
206                 value = getattr(self, real_property)
207                 if hasattr(value, '__len__'):
208                     # list
209                     value = len(value)
210                 elif hasattr(value, 'count'):
211                     # query (but not during validation)
212                     if self.in_validation:
213                         continue
214                     value = value.count()
215                 else:
216                     raise KeyError('Do not understand property %s.' % property)
217             else:
218                 if not hasattr(self, property):
219                     continue
220                 # plain object
221                 value = getattr(self, property)
222                 if value is None:
223                     # skip None
224                     continue
225                 elif isinstance(value, ORMObject):
226                     # use repr() for ORMObject types
227                     value = repr(value)
228                 else:
229                     # we want a string for all other types because json cannot
230                     # encode everything
231                     value = str(value)
232             data[property] = value
233         return json.dumps(data)
234
235     def classname(self):
236         '''
237         Returns the name of the class.
238         '''
239         return type(self).__name__
240
241     def __repr__(self):
242         '''
243         Returns a short string representation of the object using the first
244         element from the properties() method.
245         '''
246         primary_property = self.properties()[0]
247         value = getattr(self, primary_property)
248         return '<%s %s>' % (self.classname(), str(value))
249
250     def __str__(self):
251         '''
252         Returns a human readable form of the object using the properties()
253         method.
254         '''
255         return '<%s %s>' % (self.classname(), self.json())
256
257     def not_null_constraints(self):
258         '''
259         Returns a list of properties that must be not NULL. Derived classes
260         should override this method if needed.
261         '''
262         return []
263
264     validation_message = \
265         "Validation failed because property '%s' must not be empty in object\n%s"
266
267     in_validation = False
268
269     def validate(self):
270         '''
271         This function validates the not NULL constraints as returned by
272         not_null_constraints(). It raises the DBUpdateError exception if
273         validation fails.
274         '''
275         for property in self.not_null_constraints():
276             # TODO: It is a bit awkward that the mapper configuration allow
277             # directly setting the numeric _id columns. We should get rid of it
278             # in the long run.
279             if hasattr(self, property + '_id') and \
280                 getattr(self, property + '_id') is not None:
281                 continue
282             if not hasattr(self, property) or getattr(self, property) is None:
283                 # str() might lead to races due to a 2nd flush
284                 self.in_validation = True
285                 message = self.validation_message % (property, str(self))
286                 self.in_validation = False
287                 raise DBUpdateError(message)
288
289     @classmethod
290     @session_wrapper
291     def get(cls, primary_key,  session = None):
292         '''
293         This is a support function that allows getting an object by its primary
294         key.
295
296         Architecture.get(3[, session])
297
298         instead of the more verbose
299
300         session.query(Architecture).get(3)
301         '''
302         return session.query(cls).get(primary_key)
303
304     def session(self, replace = False):
305         '''
306         Returns the current session that is associated with the object. May
307         return None is object is in detached state.
308         '''
309
310         return object_session(self)
311
312     def clone(self, session = None):
313         """
314         Clones the current object in a new session and returns the new clone. A
315         fresh session is created if the optional session parameter is not
316         provided. The function will fail if a session is provided and has
317         unflushed changes.
318
319         RATIONALE: SQLAlchemy's session is not thread safe. This method clones
320         an existing object to allow several threads to work with their own
321         instances of an ORMObject.
322
323         WARNING: Only persistent (committed) objects can be cloned. Changes
324         made to the original object that are not committed yet will get lost.
325         The session of the new object will always be rolled back to avoid
326         resource leaks.
327         """
328
329         if self.session() is None:
330             raise RuntimeError( \
331                 'Method clone() failed for detached object:\n%s' % self)
332         self.session().flush()
333         mapper = object_mapper(self)
334         primary_key = mapper.primary_key_from_instance(self)
335         object_class = self.__class__
336         if session is None:
337             session = DBConn().session()
338         elif len(session.new) + len(session.dirty) + len(session.deleted) > 0:
339             raise RuntimeError( \
340                 'Method clone() failed due to unflushed changes in session.')
341         new_object = session.query(object_class).get(primary_key)
342         session.rollback()
343         if new_object is None:
344             raise RuntimeError( \
345                 'Method clone() failed for non-persistent object:\n%s' % self)
346         return new_object
347
348 __all__.append('ORMObject')
349
350 ################################################################################
351
352 class Validator(MapperExtension):
353     '''
354     This class calls the validate() method for each instance for the
355     'before_update' and 'before_insert' events. A global object validator is
356     used for configuring the individual mappers.
357     '''
358
359     def before_update(self, mapper, connection, instance):
360         instance.validate()
361         return EXT_CONTINUE
362
363     def before_insert(self, mapper, connection, instance):
364         instance.validate()
365         return EXT_CONTINUE
366
367 validator = Validator()
368
369 ################################################################################
370
371 class ACL(ORMObject):
372     def __repr__(self):
373         return "<ACL {0}>".format(self.name)
374
375 __all__.append('ACL')
376
377 class ACLPerSource(ORMObject):
378     def __repr__(self):
379         return "<ACLPerSource acl={0} fingerprint={1} source={2} reason={3}>".format(self.acl.name, self.fingerprint.fingerprint, self.source, self.reason)
380
381 __all__.append('ACLPerSource')
382
383 ################################################################################
384
385 class Architecture(ORMObject):
386     def __init__(self, arch_string = None, description = None):
387         self.arch_string = arch_string
388         self.description = description
389
390     def __eq__(self, val):
391         if isinstance(val, str):
392             return (self.arch_string== val)
393         # This signals to use the normal comparison operator
394         return NotImplemented
395
396     def __ne__(self, val):
397         if isinstance(val, str):
398             return (self.arch_string != val)
399         # This signals to use the normal comparison operator
400         return NotImplemented
401
402     def properties(self):
403         return ['arch_string', 'arch_id', 'suites_count']
404
405     def not_null_constraints(self):
406         return ['arch_string']
407
408 __all__.append('Architecture')
409
410 @session_wrapper
411 def get_architecture(architecture, session=None):
412     """
413     Returns database id for given C{architecture}.
414
415     @type architecture: string
416     @param architecture: The name of the architecture
417
418     @type session: Session
419     @param session: Optional SQLA session object (a temporary one will be
420     generated if not supplied)
421
422     @rtype: Architecture
423     @return: Architecture object for the given arch (None if not present)
424     """
425
426     q = session.query(Architecture).filter_by(arch_string=architecture)
427
428     try:
429         return q.one()
430     except NoResultFound:
431         return None
432
433 __all__.append('get_architecture')
434
435 ################################################################################
436
437 class Archive(object):
438     def __init__(self, *args, **kwargs):
439         pass
440
441     def __repr__(self):
442         return '<Archive %s>' % self.archive_name
443
444 __all__.append('Archive')
445
446 @session_wrapper
447 def get_archive(archive, session=None):
448     """
449     returns database id for given C{archive}.
450
451     @type archive: string
452     @param archive: the name of the arhive
453
454     @type session: Session
455     @param session: Optional SQLA session object (a temporary one will be
456     generated if not supplied)
457
458     @rtype: Archive
459     @return: Archive object for the given name (None if not present)
460
461     """
462     archive = archive.lower()
463
464     q = session.query(Archive).filter_by(archive_name=archive)
465
466     try:
467         return q.one()
468     except NoResultFound:
469         return None
470
471 __all__.append('get_archive')
472
473 ################################################################################
474
475 class ArchiveFile(object):
476     def __init__(self, archive=None, component=None, file=None):
477         self.archive = archive
478         self.component = component
479         self.file = file
480     @property
481     def path(self):
482         return os.path.join(self.archive.path, 'pool', self.component.component_name, self.file.filename)
483
484 __all__.append('ArchiveFile')
485
486 ################################################################################
487
488 class BinContents(ORMObject):
489     def __init__(self, file = None, binary = None):
490         self.file = file
491         self.binary = binary
492
493     def properties(self):
494         return ['file', 'binary']
495
496 __all__.append('BinContents')
497
498 ################################################################################
499
500 class DBBinary(ORMObject):
501     def __init__(self, package = None, source = None, version = None, \
502         maintainer = None, architecture = None, poolfile = None, \
503         binarytype = 'deb', fingerprint=None):
504         self.package = package
505         self.source = source
506         self.version = version
507         self.maintainer = maintainer
508         self.architecture = architecture
509         self.poolfile = poolfile
510         self.binarytype = binarytype
511         self.fingerprint = fingerprint
512
513     @property
514     def pkid(self):
515         return self.binary_id
516
517     def properties(self):
518         return ['package', 'version', 'maintainer', 'source', 'architecture', \
519             'poolfile', 'binarytype', 'fingerprint', 'install_date', \
520             'suites_count', 'binary_id', 'contents_count', 'extra_sources']
521
522     def not_null_constraints(self):
523         return ['package', 'version', 'maintainer', 'source',  'poolfile', \
524             'binarytype']
525
526     metadata = association_proxy('key', 'value')
527
528     def scan_contents(self):
529         '''
530         Yields the contents of the package. Only regular files are yielded and
531         the path names are normalized after converting them from either utf-8
532         or iso8859-1 encoding. It yields the string ' <EMPTY PACKAGE>' if the
533         package does not contain any regular file.
534         '''
535         fullpath = self.poolfile.fullpath
536         dpkg_cmd = ('dpkg-deb', '--fsys-tarfile', fullpath)
537         dpkg = daklib.daksubprocess.Popen(dpkg_cmd, stdout=subprocess.PIPE)
538         tar = TarFile.open(fileobj = dpkg.stdout, mode = 'r|')
539         for member in tar.getmembers():
540             if not member.isdir():
541                 name = normpath(member.name)
542                 # enforce proper utf-8 encoding
543                 try:
544                     name.decode('utf-8')
545                 except UnicodeDecodeError:
546                     name = name.decode('iso8859-1').encode('utf-8')
547                 yield name
548         tar.close()
549         dpkg.stdout.close()
550         dpkg.wait()
551
552     def read_control(self):
553         '''
554         Reads the control information from a binary.
555
556         @rtype: text
557         @return: stanza text of the control section.
558         '''
559         import utils
560         fullpath = self.poolfile.fullpath
561         with open(fullpath, 'r') as deb_file:
562             return utils.deb_extract_control(deb_file)
563
564     def read_control_fields(self):
565         '''
566         Reads the control information from a binary and return
567         as a dictionary.
568
569         @rtype: dict
570         @return: fields of the control section as a dictionary.
571         '''
572         stanza = self.read_control()
573         return apt_pkg.TagSection(stanza)
574
575     @property
576     def proxy(self):
577         session = object_session(self)
578         query = session.query(BinaryMetadata).filter_by(binary=self)
579         return MetadataProxy(session, query)
580
581 __all__.append('DBBinary')
582
583 @session_wrapper
584 def get_suites_binary_in(package, session=None):
585     """
586     Returns list of Suite objects which given C{package} name is in
587
588     @type package: str
589     @param package: DBBinary package name to search for
590
591     @rtype: list
592     @return: list of Suite objects for the given package
593     """
594
595     return session.query(Suite).filter(Suite.binaries.any(DBBinary.package == package)).all()
596
597 __all__.append('get_suites_binary_in')
598
599 @session_wrapper
600 def get_component_by_package_suite(package, suite_list, arch_list=[], session=None):
601     '''
602     Returns the component name of the newest binary package in suite_list or
603     None if no package is found. The result can be optionally filtered by a list
604     of architecture names.
605
606     @type package: str
607     @param package: DBBinary package name to search for
608
609     @type suite_list: list of str
610     @param suite_list: list of suite_name items
611
612     @type arch_list: list of str
613     @param arch_list: optional list of arch_string items that defaults to []
614
615     @rtype: str or NoneType
616     @return: name of component or None
617     '''
618
619     q = session.query(DBBinary).filter_by(package = package). \
620         join(DBBinary.suites).filter(Suite.suite_name.in_(suite_list))
621     if len(arch_list) > 0:
622         q = q.join(DBBinary.architecture). \
623             filter(Architecture.arch_string.in_(arch_list))
624     binary = q.order_by(desc(DBBinary.version)).first()
625     if binary is None:
626         return None
627     else:
628         return binary.poolfile.component.component_name
629
630 __all__.append('get_component_by_package_suite')
631
632 ################################################################################
633
634 class BuildQueue(object):
635     def __init__(self, *args, **kwargs):
636         pass
637
638     def __repr__(self):
639         return '<BuildQueue %s>' % self.queue_name
640
641 __all__.append('BuildQueue')
642
643 ################################################################################
644
645 class Component(ORMObject):
646     def __init__(self, component_name = None):
647         self.component_name = component_name
648
649     def __eq__(self, val):
650         if isinstance(val, str):
651             return (self.component_name == val)
652         # This signals to use the normal comparison operator
653         return NotImplemented
654
655     def __ne__(self, val):
656         if isinstance(val, str):
657             return (self.component_name != val)
658         # This signals to use the normal comparison operator
659         return NotImplemented
660
661     def properties(self):
662         return ['component_name', 'component_id', 'description', \
663             'meets_dfsg', 'overrides_count']
664
665     def not_null_constraints(self):
666         return ['component_name']
667
668
669 __all__.append('Component')
670
671 @session_wrapper
672 def get_component(component, session=None):
673     """
674     Returns database id for given C{component}.
675
676     @type component: string
677     @param component: The name of the override type
678
679     @rtype: int
680     @return: the database id for the given component
681
682     """
683     component = component.lower()
684
685     q = session.query(Component).filter_by(component_name=component)
686
687     try:
688         return q.one()
689     except NoResultFound:
690         return None
691
692 __all__.append('get_component')
693
694 @session_wrapper
695 def get_mapped_component(component_name, session=None):
696     """get component after mappings
697
698     Evaluate component mappings from ComponentMappings in dak.conf for the
699     given component name.
700
701     @todo: ansgar wants to get rid of this. It's currently only used for
702            the security archive
703
704     @type  component_name: str
705     @param component_name: component name
706
707     @param session: database session
708
709     @rtype:  L{daklib.dbconn.Component} or C{None}
710     @return: component after applying maps or C{None}
711     """
712     cnf = Config()
713     for m in cnf.value_list("ComponentMappings"):
714         (src, dst) = m.split()
715         if component_name == src:
716             component_name = dst
717     component = session.query(Component).filter_by(component_name=component_name).first()
718     return component
719
720 __all__.append('get_mapped_component')
721
722 @session_wrapper
723 def get_component_names(session=None):
724     """
725     Returns list of strings of component names.
726
727     @rtype: list
728     @return: list of strings of component names
729     """
730
731     return [ x.component_name for x in session.query(Component).all() ]
732
733 __all__.append('get_component_names')
734
735 ################################################################################
736
737 class DBConfig(object):
738     def __init__(self, *args, **kwargs):
739         pass
740
741     def __repr__(self):
742         return '<DBConfig %s>' % self.name
743
744 __all__.append('DBConfig')
745
746 ################################################################################
747
748 @session_wrapper
749 def get_or_set_contents_file_id(filename, session=None):
750     """
751     Returns database id for given filename.
752
753     If no matching file is found, a row is inserted.
754
755     @type filename: string
756     @param filename: The filename
757     @type session: SQLAlchemy
758     @param session: Optional SQL session object (a temporary one will be
759     generated if not supplied).  If not passed, a commit will be performed at
760     the end of the function, otherwise the caller is responsible for commiting.
761
762     @rtype: int
763     @return: the database id for the given component
764     """
765
766     q = session.query(ContentFilename).filter_by(filename=filename)
767
768     try:
769         ret = q.one().cafilename_id
770     except NoResultFound:
771         cf = ContentFilename()
772         cf.filename = filename
773         session.add(cf)
774         session.commit_or_flush()
775         ret = cf.cafilename_id
776
777     return ret
778
779 __all__.append('get_or_set_contents_file_id')
780
781 @session_wrapper
782 def get_contents(suite, overridetype, section=None, session=None):
783     """
784     Returns contents for a suite / overridetype combination, limiting
785     to a section if not None.
786
787     @type suite: Suite
788     @param suite: Suite object
789
790     @type overridetype: OverrideType
791     @param overridetype: OverrideType object
792
793     @type section: Section
794     @param section: Optional section object to limit results to
795
796     @type session: SQLAlchemy
797     @param session: Optional SQL session object (a temporary one will be
798     generated if not supplied)
799
800     @rtype: ResultsProxy
801     @return: ResultsProxy object set up to return tuples of (filename, section,
802     package, arch_id)
803     """
804
805     # find me all of the contents for a given suite
806     contents_q = """SELECT (p.path||'/'||n.file) AS fn,
807                             s.section,
808                             b.package,
809                             b.architecture
810                    FROM content_associations c join content_file_paths p ON (c.filepath=p.id)
811                    JOIN content_file_names n ON (c.filename=n.id)
812                    JOIN binaries b ON (b.id=c.binary_pkg)
813                    JOIN override o ON (o.package=b.package)
814                    JOIN section s ON (s.id=o.section)
815                    WHERE o.suite = :suiteid AND o.type = :overridetypeid
816                    AND b.type=:overridetypename"""
817
818     vals = {'suiteid': suite.suite_id,
819             'overridetypeid': overridetype.overridetype_id,
820             'overridetypename': overridetype.overridetype}
821
822     if section is not None:
823         contents_q += " AND s.id = :sectionid"
824         vals['sectionid'] = section.section_id
825
826     contents_q += " ORDER BY fn"
827
828     return session.execute(contents_q, vals)
829
830 __all__.append('get_contents')
831
832 ################################################################################
833
834 class ContentFilepath(object):
835     def __init__(self, *args, **kwargs):
836         pass
837
838     def __repr__(self):
839         return '<ContentFilepath %s>' % self.filepath
840
841 __all__.append('ContentFilepath')
842
843 @session_wrapper
844 def get_or_set_contents_path_id(filepath, session=None):
845     """
846     Returns database id for given path.
847
848     If no matching file is found, a row is inserted.
849
850     @type filepath: string
851     @param filepath: The filepath
852
853     @type session: SQLAlchemy
854     @param session: Optional SQL session object (a temporary one will be
855     generated if not supplied).  If not passed, a commit will be performed at
856     the end of the function, otherwise the caller is responsible for commiting.
857
858     @rtype: int
859     @return: the database id for the given path
860     """
861
862     q = session.query(ContentFilepath).filter_by(filepath=filepath)
863
864     try:
865         ret = q.one().cafilepath_id
866     except NoResultFound:
867         cf = ContentFilepath()
868         cf.filepath = filepath
869         session.add(cf)
870         session.commit_or_flush()
871         ret = cf.cafilepath_id
872
873     return ret
874
875 __all__.append('get_or_set_contents_path_id')
876
877 ################################################################################
878
879 class ContentAssociation(object):
880     def __init__(self, *args, **kwargs):
881         pass
882
883     def __repr__(self):
884         return '<ContentAssociation %s>' % self.ca_id
885
886 __all__.append('ContentAssociation')
887
888 def insert_content_paths(binary_id, fullpaths, session=None):
889     """
890     Make sure given path is associated with given binary id
891
892     @type binary_id: int
893     @param binary_id: the id of the binary
894     @type fullpaths: list
895     @param fullpaths: the list of paths of the file being associated with the binary
896     @type session: SQLAlchemy session
897     @param session: Optional SQLAlchemy session.  If this is passed, the caller
898     is responsible for ensuring a transaction has begun and committing the
899     results or rolling back based on the result code.  If not passed, a commit
900     will be performed at the end of the function, otherwise the caller is
901     responsible for commiting.
902
903     @return: True upon success
904     """
905
906     privatetrans = False
907     if session is None:
908         session = DBConn().session()
909         privatetrans = True
910
911     try:
912         # Insert paths
913         def generate_path_dicts():
914             for fullpath in fullpaths:
915                 if fullpath.startswith( './' ):
916                     fullpath = fullpath[2:]
917
918                 yield {'filename':fullpath, 'id': binary_id }
919
920         for d in generate_path_dicts():
921             session.execute( "INSERT INTO bin_contents ( file, binary_id ) VALUES ( :filename, :id )",
922                          d )
923
924         session.commit()
925         if privatetrans:
926             session.close()
927         return True
928
929     except:
930         traceback.print_exc()
931
932         # Only rollback if we set up the session ourself
933         if privatetrans:
934             session.rollback()
935             session.close()
936
937         return False
938
939 __all__.append('insert_content_paths')
940
941 ################################################################################
942
943 class DSCFile(object):
944     def __init__(self, *args, **kwargs):
945         pass
946
947     def __repr__(self):
948         return '<DSCFile %s>' % self.dscfile_id
949
950 __all__.append('DSCFile')
951
952 @session_wrapper
953 def get_dscfiles(dscfile_id=None, source_id=None, poolfile_id=None, session=None):
954     """
955     Returns a list of DSCFiles which may be empty
956
957     @type dscfile_id: int (optional)
958     @param dscfile_id: the dscfile_id of the DSCFiles to find
959
960     @type source_id: int (optional)
961     @param source_id: the source id related to the DSCFiles to find
962
963     @type poolfile_id: int (optional)
964     @param poolfile_id: the poolfile id related to the DSCFiles to find
965
966     @rtype: list
967     @return: Possibly empty list of DSCFiles
968     """
969
970     q = session.query(DSCFile)
971
972     if dscfile_id is not None:
973         q = q.filter_by(dscfile_id=dscfile_id)
974
975     if source_id is not None:
976         q = q.filter_by(source_id=source_id)
977
978     if poolfile_id is not None:
979         q = q.filter_by(poolfile_id=poolfile_id)
980
981     return q.all()
982
983 __all__.append('get_dscfiles')
984
985 ################################################################################
986
987 class ExternalOverride(ORMObject):
988     def __init__(self, *args, **kwargs):
989         pass
990
991     def __repr__(self):
992         return '<ExternalOverride %s = %s: %s>' % (self.package, self.key, self.value)
993
994 __all__.append('ExternalOverride')
995
996 ################################################################################
997
998 class PoolFile(ORMObject):
999     def __init__(self, filename = None, filesize = -1, \
1000         md5sum = None):
1001         self.filename = filename
1002         self.filesize = filesize
1003         self.md5sum = md5sum
1004
1005     @property
1006     def fullpath(self):
1007         session = DBConn().session().object_session(self)
1008         af = session.query(ArchiveFile).join(Archive) \
1009                     .filter(ArchiveFile.file == self) \
1010                     .order_by(Archive.tainted.desc()).first()
1011         return af.path
1012
1013     @property
1014     def component(self):
1015         session = DBConn().session().object_session(self)
1016         component_id = session.query(ArchiveFile.component_id).filter(ArchiveFile.file == self) \
1017                               .group_by(ArchiveFile.component_id).one()
1018         return session.query(Component).get(component_id)
1019
1020     @property
1021     def basename(self):
1022         return os.path.basename(self.filename)
1023
1024     def is_valid(self, filesize = -1, md5sum = None):
1025         return self.filesize == long(filesize) and self.md5sum == md5sum
1026
1027     def properties(self):
1028         return ['filename', 'file_id', 'filesize', 'md5sum', 'sha1sum', \
1029             'sha256sum', 'source', 'binary', 'last_used']
1030
1031     def not_null_constraints(self):
1032         return ['filename', 'md5sum']
1033
1034     def identical_to(self, filename):
1035         """
1036         compare size and hash with the given file
1037
1038         @rtype: bool
1039         @return: true if the given file has the same size and hash as this object; false otherwise
1040         """
1041         st = os.stat(filename)
1042         if self.filesize != st.st_size:
1043             return False
1044
1045         f = open(filename, "r")
1046         sha256sum = apt_pkg.sha256sum(f)
1047         if sha256sum != self.sha256sum:
1048             return False
1049
1050         return True
1051
1052 __all__.append('PoolFile')
1053
1054 @session_wrapper
1055 def get_poolfile_like_name(filename, session=None):
1056     """
1057     Returns an array of PoolFile objects which are like the given name
1058
1059     @type filename: string
1060     @param filename: the filename of the file to check against the DB
1061
1062     @rtype: array
1063     @return: array of PoolFile objects
1064     """
1065
1066     # TODO: There must be a way of properly using bind parameters with %FOO%
1067     q = session.query(PoolFile).filter(PoolFile.filename.like('%%/%s' % filename))
1068
1069     return q.all()
1070
1071 __all__.append('get_poolfile_like_name')
1072
1073 ################################################################################
1074
1075 class Fingerprint(ORMObject):
1076     def __init__(self, fingerprint = None):
1077         self.fingerprint = fingerprint
1078
1079     def properties(self):
1080         return ['fingerprint', 'fingerprint_id', 'keyring', 'uid', \
1081             'binary_reject']
1082
1083     def not_null_constraints(self):
1084         return ['fingerprint']
1085
1086 __all__.append('Fingerprint')
1087
1088 @session_wrapper
1089 def get_fingerprint(fpr, session=None):
1090     """
1091     Returns Fingerprint object for given fpr.
1092
1093     @type fpr: string
1094     @param fpr: The fpr to find / add
1095
1096     @type session: SQLAlchemy
1097     @param session: Optional SQL session object (a temporary one will be
1098     generated if not supplied).
1099
1100     @rtype: Fingerprint
1101     @return: the Fingerprint object for the given fpr or None
1102     """
1103
1104     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1105
1106     try:
1107         ret = q.one()
1108     except NoResultFound:
1109         ret = None
1110
1111     return ret
1112
1113 __all__.append('get_fingerprint')
1114
1115 @session_wrapper
1116 def get_or_set_fingerprint(fpr, session=None):
1117     """
1118     Returns Fingerprint object for given fpr.
1119
1120     If no matching fpr is found, a row is inserted.
1121
1122     @type fpr: string
1123     @param fpr: The fpr to find / add
1124
1125     @type session: SQLAlchemy
1126     @param session: Optional SQL session object (a temporary one will be
1127     generated if not supplied).  If not passed, a commit will be performed at
1128     the end of the function, otherwise the caller is responsible for commiting.
1129     A flush will be performed either way.
1130
1131     @rtype: Fingerprint
1132     @return: the Fingerprint object for the given fpr
1133     """
1134
1135     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1136
1137     try:
1138         ret = q.one()
1139     except NoResultFound:
1140         fingerprint = Fingerprint()
1141         fingerprint.fingerprint = fpr
1142         session.add(fingerprint)
1143         session.commit_or_flush()
1144         ret = fingerprint
1145
1146     return ret
1147
1148 __all__.append('get_or_set_fingerprint')
1149
1150 ################################################################################
1151
1152 # Helper routine for Keyring class
1153 def get_ldap_name(entry):
1154     name = []
1155     for k in ["cn", "mn", "sn"]:
1156         ret = entry.get(k)
1157         if ret and ret[0] != "" and ret[0] != "-":
1158             name.append(ret[0])
1159     return " ".join(name)
1160
1161 ################################################################################
1162
1163 class Keyring(object):
1164     keys = {}
1165     fpr_lookup = {}
1166
1167     def __init__(self, *args, **kwargs):
1168         pass
1169
1170     def __repr__(self):
1171         return '<Keyring %s>' % self.keyring_name
1172
1173     def de_escape_gpg_str(self, txt):
1174         esclist = re.split(r'(\\x..)', txt)
1175         for x in range(1,len(esclist),2):
1176             esclist[x] = "%c" % (int(esclist[x][2:],16))
1177         return "".join(esclist)
1178
1179     def parse_address(self, uid):
1180         """parses uid and returns a tuple of real name and email address"""
1181         import email.Utils
1182         (name, address) = email.Utils.parseaddr(uid)
1183         name = re.sub(r"\s*[(].*[)]", "", name)
1184         name = self.de_escape_gpg_str(name)
1185         if name == "":
1186             name = uid
1187         return (name, address)
1188
1189     def load_keys(self, keyring):
1190         if not self.keyring_id:
1191             raise Exception('Must be initialized with database information')
1192
1193         cmd = ["gpg", "--no-default-keyring", "--keyring", keyring,
1194                "--with-colons", "--fingerprint", "--fingerprint"]
1195         p = daklib.daksubprocess.Popen(cmd, stdout=subprocess.PIPE)
1196
1197         key = None
1198         need_fingerprint = False
1199
1200         for line in p.stdout:
1201             field = line.split(":")
1202             if field[0] == "pub":
1203                 key = field[4]
1204                 self.keys[key] = {}
1205                 (name, addr) = self.parse_address(field[9])
1206                 if "@" in addr:
1207                     self.keys[key]["email"] = addr
1208                     self.keys[key]["name"] = name
1209                 need_fingerprint = True
1210             elif key and field[0] == "uid":
1211                 (name, addr) = self.parse_address(field[9])
1212                 if "email" not in self.keys[key] and "@" in addr:
1213                     self.keys[key]["email"] = addr
1214                     self.keys[key]["name"] = name
1215             elif need_fingerprint and field[0] == "fpr":
1216                 self.keys[key]["fingerprints"] = [field[9]]
1217                 self.fpr_lookup[field[9]] = key
1218                 need_fingerprint = False
1219
1220         r = p.wait()
1221         if r != 0:
1222             raise subprocess.CalledProcessError(r, cmd)
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     @property
1989     def proxy(self):
1990         session = object_session(self)
1991         query = session.query(SourceMetadata).filter_by(source=self)
1992         return MetadataProxy(session, query)
1993
1994 __all__.append('DBSource')
1995
1996 @session_wrapper
1997 def source_exists(source, source_version, suites = ["any"], session=None):
1998     """
1999     Ensure that source exists somewhere in the archive for the binary
2000     upload being processed.
2001       1. exact match     => 1.0-3
2002       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
2003
2004     @type source: string
2005     @param source: source name
2006
2007     @type source_version: string
2008     @param source_version: expected source version
2009
2010     @type suites: list
2011     @param suites: list of suites to check in, default I{any}
2012
2013     @type session: Session
2014     @param session: Optional SQLA session object (a temporary one will be
2015     generated if not supplied)
2016
2017     @rtype: int
2018     @return: returns 1 if a source with expected version is found, otherwise 0
2019
2020     """
2021
2022     cnf = Config()
2023     ret = True
2024
2025     from daklib.regexes import re_bin_only_nmu
2026     orig_source_version = re_bin_only_nmu.sub('', source_version)
2027
2028     for suite in suites:
2029         q = session.query(DBSource).filter_by(source=source). \
2030             filter(DBSource.version.in_([source_version, orig_source_version]))
2031         if suite != "any":
2032             # source must exist in 'suite' or a suite that is enhanced by 'suite'
2033             s = get_suite(suite, session)
2034             if s:
2035                 enhances_vcs = session.query(VersionCheck).filter(VersionCheck.suite==s).filter_by(check='Enhances')
2036                 considered_suites = [ vc.reference for vc in enhances_vcs ]
2037                 considered_suites.append(s)
2038
2039                 q = q.filter(DBSource.suites.any(Suite.suite_id.in_([s.suite_id for s in considered_suites])))
2040
2041         if q.count() > 0:
2042             continue
2043
2044         # No source found so return not ok
2045         ret = False
2046
2047     return ret
2048
2049 __all__.append('source_exists')
2050
2051 @session_wrapper
2052 def get_suites_source_in(source, session=None):
2053     """
2054     Returns list of Suite objects which given C{source} name is in
2055
2056     @type source: str
2057     @param source: DBSource package name to search for
2058
2059     @rtype: list
2060     @return: list of Suite objects for the given source
2061     """
2062
2063     return session.query(Suite).filter(Suite.sources.any(source=source)).all()
2064
2065 __all__.append('get_suites_source_in')
2066
2067 @session_wrapper
2068 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
2069     """
2070     Returns list of DBSource objects for given C{source} name and other parameters
2071
2072     @type source: str
2073     @param source: DBSource package name to search for
2074
2075     @type version: str or None
2076     @param version: DBSource version name to search for or None if not applicable
2077
2078     @type dm_upload_allowed: bool
2079     @param dm_upload_allowed: If None, no effect.  If True or False, only
2080     return packages with that dm_upload_allowed setting
2081
2082     @type session: Session
2083     @param session: Optional SQL session object (a temporary one will be
2084     generated if not supplied)
2085
2086     @rtype: list
2087     @return: list of DBSource objects for the given name (may be empty)
2088     """
2089
2090     q = session.query(DBSource).filter_by(source=source)
2091
2092     if version is not None:
2093         q = q.filter_by(version=version)
2094
2095     if dm_upload_allowed is not None:
2096         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
2097
2098     return q.all()
2099
2100 __all__.append('get_sources_from_name')
2101
2102 # FIXME: This function fails badly if it finds more than 1 source package and
2103 # its implementation is trivial enough to be inlined.
2104 @session_wrapper
2105 def get_source_in_suite(source, suite_name, session=None):
2106     """
2107     Returns a DBSource object for a combination of C{source} and C{suite_name}.
2108
2109       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2110       - B{suite_name} - a suite name, eg. I{unstable}
2111
2112     @type source: string
2113     @param source: source package name
2114
2115     @type suite_name: string
2116     @param suite: the suite name
2117
2118     @rtype: string
2119     @return: the version for I{source} in I{suite}
2120
2121     """
2122     suite = get_suite(suite_name, session)
2123     if suite is None:
2124         return None
2125     try:
2126         return suite.get_sources(source).one()
2127     except NoResultFound:
2128         return None
2129
2130 __all__.append('get_source_in_suite')
2131
2132 @session_wrapper
2133 def import_metadata_into_db(obj, session=None):
2134     """
2135     This routine works on either DBBinary or DBSource objects and imports
2136     their metadata into the database
2137     """
2138     fields = obj.read_control_fields()
2139     for k in fields.keys():
2140         try:
2141             # Try raw ASCII
2142             val = str(fields[k])
2143         except UnicodeEncodeError:
2144             # Fall back to UTF-8
2145             try:
2146                 val = fields[k].encode('utf-8')
2147             except UnicodeEncodeError:
2148                 # Finally try iso8859-1
2149                 val = fields[k].encode('iso8859-1')
2150                 # Otherwise we allow the exception to percolate up and we cause
2151                 # a reject as someone is playing silly buggers
2152
2153         obj.metadata[get_or_set_metadatakey(k, session)] = val
2154
2155     session.commit_or_flush()
2156
2157 __all__.append('import_metadata_into_db')
2158
2159 ################################################################################
2160
2161 class SrcFormat(object):
2162     def __init__(self, *args, **kwargs):
2163         pass
2164
2165     def __repr__(self):
2166         return '<SrcFormat %s>' % (self.format_name)
2167
2168 __all__.append('SrcFormat')
2169
2170 ################################################################################
2171
2172 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
2173                  ('SuiteID', 'suite_id'),
2174                  ('Version', 'version'),
2175                  ('Origin', 'origin'),
2176                  ('Label', 'label'),
2177                  ('Description', 'description'),
2178                  ('Untouchable', 'untouchable'),
2179                  ('Announce', 'announce'),
2180                  ('Codename', 'codename'),
2181                  ('OverrideCodename', 'overridecodename'),
2182                  ('ValidTime', 'validtime'),
2183                  ('Priority', 'priority'),
2184                  ('NotAutomatic', 'notautomatic'),
2185                  ('CopyChanges', 'copychanges'),
2186                  ('OverrideSuite', 'overridesuite')]
2187
2188 # Why the heck don't we have any UNIQUE constraints in table suite?
2189 # TODO: Add UNIQUE constraints for appropriate columns.
2190 class Suite(ORMObject):
2191     def __init__(self, suite_name = None, version = None):
2192         self.suite_name = suite_name
2193         self.version = version
2194
2195     def properties(self):
2196         return ['suite_name', 'version', 'sources_count', 'binaries_count', \
2197             'overrides_count']
2198
2199     def not_null_constraints(self):
2200         return ['suite_name']
2201
2202     def __eq__(self, val):
2203         if isinstance(val, str):
2204             return (self.suite_name == val)
2205         # This signals to use the normal comparison operator
2206         return NotImplemented
2207
2208     def __ne__(self, val):
2209         if isinstance(val, str):
2210             return (self.suite_name != val)
2211         # This signals to use the normal comparison operator
2212         return NotImplemented
2213
2214     def details(self):
2215         ret = []
2216         for disp, field in SUITE_FIELDS:
2217             val = getattr(self, field, None)
2218             if val is not None:
2219                 ret.append("%s: %s" % (disp, val))
2220
2221         return "\n".join(ret)
2222
2223     def get_architectures(self, skipsrc=False, skipall=False):
2224         """
2225         Returns list of Architecture objects
2226
2227         @type skipsrc: boolean
2228         @param skipsrc: Whether to skip returning the 'source' architecture entry
2229         (Default False)
2230
2231         @type skipall: boolean
2232         @param skipall: Whether to skip returning the 'all' architecture entry
2233         (Default False)
2234
2235         @rtype: list
2236         @return: list of Architecture objects for the given name (may be empty)
2237         """
2238
2239         q = object_session(self).query(Architecture).with_parent(self)
2240         if skipsrc:
2241             q = q.filter(Architecture.arch_string != 'source')
2242         if skipall:
2243             q = q.filter(Architecture.arch_string != 'all')
2244         return q.order_by(Architecture.arch_string).all()
2245
2246     def get_sources(self, source):
2247         """
2248         Returns a query object representing DBSource that is part of C{suite}.
2249
2250           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2251
2252         @type source: string
2253         @param source: source package name
2254
2255         @rtype: sqlalchemy.orm.query.Query
2256         @return: a query of DBSource
2257
2258         """
2259
2260         session = object_session(self)
2261         return session.query(DBSource).filter_by(source = source). \
2262             with_parent(self)
2263
2264     def get_overridesuite(self):
2265         if self.overridesuite is None:
2266             return self
2267         else:
2268             return object_session(self).query(Suite).filter_by(suite_name=self.overridesuite).one()
2269
2270     @property
2271     def path(self):
2272         return os.path.join(self.archive.path, 'dists', self.suite_name)
2273
2274 __all__.append('Suite')
2275
2276 @session_wrapper
2277 def get_suite(suite, session=None):
2278     """
2279     Returns Suite object for given C{suite name}.
2280
2281     @type suite: string
2282     @param suite: The name of the suite
2283
2284     @type session: Session
2285     @param session: Optional SQLA session object (a temporary one will be
2286     generated if not supplied)
2287
2288     @rtype: Suite
2289     @return: Suite object for the requested suite name (None if not present)
2290     """
2291
2292     q = session.query(Suite).filter_by(suite_name=suite)
2293
2294     try:
2295         return q.one()
2296     except NoResultFound:
2297         return None
2298
2299 __all__.append('get_suite')
2300
2301 ################################################################################
2302
2303 @session_wrapper
2304 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
2305     """
2306     Returns list of Architecture objects for given C{suite} name. The list is
2307     empty if suite does not exist.
2308
2309     @type suite: str
2310     @param suite: Suite name to search for
2311
2312     @type skipsrc: boolean
2313     @param skipsrc: Whether to skip returning the 'source' architecture entry
2314     (Default False)
2315
2316     @type skipall: boolean
2317     @param skipall: Whether to skip returning the 'all' architecture entry
2318     (Default False)
2319
2320     @type session: Session
2321     @param session: Optional SQL session object (a temporary one will be
2322     generated if not supplied)
2323
2324     @rtype: list
2325     @return: list of Architecture objects for the given name (may be empty)
2326     """
2327
2328     try:
2329         return get_suite(suite, session).get_architectures(skipsrc, skipall)
2330     except AttributeError:
2331         return []
2332
2333 __all__.append('get_suite_architectures')
2334
2335 ################################################################################
2336
2337 class Uid(ORMObject):
2338     def __init__(self, uid = None, name = None):
2339         self.uid = uid
2340         self.name = name
2341
2342     def __eq__(self, val):
2343         if isinstance(val, str):
2344             return (self.uid == val)
2345         # This signals to use the normal comparison operator
2346         return NotImplemented
2347
2348     def __ne__(self, val):
2349         if isinstance(val, str):
2350             return (self.uid != val)
2351         # This signals to use the normal comparison operator
2352         return NotImplemented
2353
2354     def properties(self):
2355         return ['uid', 'name', 'fingerprint']
2356
2357     def not_null_constraints(self):
2358         return ['uid']
2359
2360 __all__.append('Uid')
2361
2362 @session_wrapper
2363 def get_or_set_uid(uidname, session=None):
2364     """
2365     Returns uid object for given uidname.
2366
2367     If no matching uidname is found, a row is inserted.
2368
2369     @type uidname: string
2370     @param uidname: The uid to add
2371
2372     @type session: SQLAlchemy
2373     @param session: Optional SQL session object (a temporary one will be
2374     generated if not supplied).  If not passed, a commit will be performed at
2375     the end of the function, otherwise the caller is responsible for commiting.
2376
2377     @rtype: Uid
2378     @return: the uid object for the given uidname
2379     """
2380
2381     q = session.query(Uid).filter_by(uid=uidname)
2382
2383     try:
2384         ret = q.one()
2385     except NoResultFound:
2386         uid = Uid()
2387         uid.uid = uidname
2388         session.add(uid)
2389         session.commit_or_flush()
2390         ret = uid
2391
2392     return ret
2393
2394 __all__.append('get_or_set_uid')
2395
2396 @session_wrapper
2397 def get_uid_from_fingerprint(fpr, session=None):
2398     q = session.query(Uid)
2399     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
2400
2401     try:
2402         return q.one()
2403     except NoResultFound:
2404         return None
2405
2406 __all__.append('get_uid_from_fingerprint')
2407
2408 ################################################################################
2409
2410 class MetadataKey(ORMObject):
2411     def __init__(self, key = None):
2412         self.key = key
2413
2414     def properties(self):
2415         return ['key']
2416
2417     def not_null_constraints(self):
2418         return ['key']
2419
2420 __all__.append('MetadataKey')
2421
2422 @session_wrapper
2423 def get_or_set_metadatakey(keyname, session=None):
2424     """
2425     Returns MetadataKey object for given uidname.
2426
2427     If no matching keyname is found, a row is inserted.
2428
2429     @type uidname: string
2430     @param uidname: The keyname to add
2431
2432     @type session: SQLAlchemy
2433     @param session: Optional SQL session object (a temporary one will be
2434     generated if not supplied).  If not passed, a commit will be performed at
2435     the end of the function, otherwise the caller is responsible for commiting.
2436
2437     @rtype: MetadataKey
2438     @return: the metadatakey object for the given keyname
2439     """
2440
2441     q = session.query(MetadataKey).filter_by(key=keyname)
2442
2443     try:
2444         ret = q.one()
2445     except NoResultFound:
2446         ret = MetadataKey(keyname)
2447         session.add(ret)
2448         session.commit_or_flush()
2449
2450     return ret
2451
2452 __all__.append('get_or_set_metadatakey')
2453
2454 ################################################################################
2455
2456 class BinaryMetadata(ORMObject):
2457     def __init__(self, key = None, value = None, binary = None):
2458         self.key = key
2459         self.value = value
2460         self.binary = binary
2461
2462     def properties(self):
2463         return ['binary', 'key', 'value']
2464
2465     def not_null_constraints(self):
2466         return ['value']
2467
2468 __all__.append('BinaryMetadata')
2469
2470 ################################################################################
2471
2472 class SourceMetadata(ORMObject):
2473     def __init__(self, key = None, value = None, source = None):
2474         self.key = key
2475         self.value = value
2476         self.source = source
2477
2478     def properties(self):
2479         return ['source', 'key', 'value']
2480
2481     def not_null_constraints(self):
2482         return ['value']
2483
2484 __all__.append('SourceMetadata')
2485
2486 ################################################################################
2487
2488 class MetadataProxy(object):
2489     def __init__(self, session, query):
2490         self.session = session
2491         self.query = query
2492
2493     def _get(self, key):
2494         metadata_key = self.session.query(MetadataKey).filter_by(key=key).first()
2495         if metadata_key is None:
2496             return None
2497         metadata = self.query.filter_by(key=metadata_key).first()
2498         return metadata
2499
2500     def __contains__(self, key):
2501         if self._get(key) is not None:
2502             return True
2503         return False
2504
2505     def __getitem__(self, key):
2506         metadata = self._get(key)
2507         if metadata is None:
2508             raise KeyError
2509         return metadata.value
2510
2511     def get(self, key, default=None):
2512         try:
2513             return self[key]
2514         except KeyError:
2515             return default
2516
2517 ################################################################################
2518
2519 class VersionCheck(ORMObject):
2520     def __init__(self, *args, **kwargs):
2521         pass
2522
2523     def properties(self):
2524         #return ['suite_id', 'check', 'reference_id']
2525         return ['check']
2526
2527     def not_null_constraints(self):
2528         return ['suite', 'check', 'reference']
2529
2530 __all__.append('VersionCheck')
2531
2532 @session_wrapper
2533 def get_version_checks(suite_name, check = None, session = None):
2534     suite = get_suite(suite_name, session)
2535     if not suite:
2536         # Make sure that what we return is iterable so that list comprehensions
2537         # involving this don't cause a traceback
2538         return []
2539     q = session.query(VersionCheck).filter_by(suite=suite)
2540     if check:
2541         q = q.filter_by(check=check)
2542     return q.all()
2543
2544 __all__.append('get_version_checks')
2545
2546 ################################################################################
2547
2548 class DBConn(object):
2549     """
2550     database module init.
2551     """
2552     __shared_state = {}
2553
2554     def __init__(self, *args, **kwargs):
2555         self.__dict__ = self.__shared_state
2556
2557         if not getattr(self, 'initialised', False):
2558             self.initialised = True
2559             self.debug = kwargs.has_key('debug')
2560             self.__createconn()
2561
2562     def __setuptables(self):
2563         tables = (
2564             'acl',
2565             'acl_architecture_map',
2566             'acl_fingerprint_map',
2567             'acl_per_source',
2568             'architecture',
2569             'archive',
2570             'bin_associations',
2571             'bin_contents',
2572             'binaries',
2573             'binaries_metadata',
2574             'build_queue',
2575             'changelogs_text',
2576             'changes',
2577             'component',
2578             'component_suite',
2579             'config',
2580             'dsc_files',
2581             'external_overrides',
2582             'extra_src_references',
2583             'files',
2584             'files_archive_map',
2585             'fingerprint',
2586             'keyrings',
2587             'maintainer',
2588             'metadata_keys',
2589             'new_comments',
2590             # TODO: the maintainer column in table override should be removed.
2591             'override',
2592             'override_type',
2593             'policy_queue',
2594             'policy_queue_upload',
2595             'policy_queue_upload_binaries_map',
2596             'policy_queue_byhand_file',
2597             'priority',
2598             'section',
2599             'signature_history',
2600             'source',
2601             'source_metadata',
2602             'src_associations',
2603             'src_contents',
2604             'src_format',
2605             'src_uploaders',
2606             'suite',
2607             'suite_acl_map',
2608             'suite_architectures',
2609             'suite_build_queue_copy',
2610             'suite_src_formats',
2611             'uid',
2612             'version_check',
2613         )
2614
2615         views = (
2616             'almost_obsolete_all_associations',
2617             'almost_obsolete_src_associations',
2618             'any_associations_source',
2619             'bin_associations_binaries',
2620             'binaries_suite_arch',
2621             'changelogs',
2622             'file_arch_suite',
2623             'newest_all_associations',
2624             'newest_any_associations',
2625             'newest_source',
2626             'newest_src_association',
2627             'obsolete_all_associations',
2628             'obsolete_any_associations',
2629             'obsolete_any_by_all_associations',
2630             'obsolete_src_associations',
2631             'source_suite',
2632             'src_associations_bin',
2633             'src_associations_src',
2634             'suite_arch_by_name',
2635         )
2636
2637         for table_name in tables:
2638             table = Table(table_name, self.db_meta, \
2639                 autoload=True, useexisting=True)
2640             setattr(self, 'tbl_%s' % table_name, table)
2641
2642         for view_name in views:
2643             view = Table(view_name, self.db_meta, autoload=True)
2644             setattr(self, 'view_%s' % view_name, view)
2645
2646     def __setupmappers(self):
2647         mapper(Architecture, self.tbl_architecture,
2648             properties = dict(arch_id = self.tbl_architecture.c.id,
2649                suites = relation(Suite, secondary=self.tbl_suite_architectures,
2650                    order_by=self.tbl_suite.c.suite_name,
2651                    backref=backref('architectures', order_by=self.tbl_architecture.c.arch_string))),
2652             extension = validator)
2653
2654         mapper(ACL, self.tbl_acl,
2655                properties = dict(
2656                 architectures = relation(Architecture, secondary=self.tbl_acl_architecture_map, collection_class=set),
2657                 fingerprints = relation(Fingerprint, secondary=self.tbl_acl_fingerprint_map, collection_class=set),
2658                 match_keyring = relation(Keyring, primaryjoin=(self.tbl_acl.c.match_keyring_id == self.tbl_keyrings.c.id)),
2659                 per_source = relation(ACLPerSource, collection_class=set),
2660                 ))
2661
2662         mapper(ACLPerSource, self.tbl_acl_per_source,
2663                properties = dict(
2664                 acl = relation(ACL),
2665                 fingerprint = relation(Fingerprint, primaryjoin=(self.tbl_acl_per_source.c.fingerprint_id == self.tbl_fingerprint.c.id)),
2666                 created_by = relation(Fingerprint, primaryjoin=(self.tbl_acl_per_source.c.created_by_id == self.tbl_fingerprint.c.id)),
2667                 ))
2668
2669         mapper(Archive, self.tbl_archive,
2670                properties = dict(archive_id = self.tbl_archive.c.id,
2671                                  archive_name = self.tbl_archive.c.name))
2672
2673         mapper(ArchiveFile, self.tbl_files_archive_map,
2674                properties = dict(archive = relation(Archive, backref='files'),
2675                                  component = relation(Component),
2676                                  file = relation(PoolFile, backref='archives')))
2677
2678         mapper(BuildQueue, self.tbl_build_queue,
2679                properties = dict(queue_id = self.tbl_build_queue.c.id,
2680                                  suite = relation(Suite, primaryjoin=(self.tbl_build_queue.c.suite_id==self.tbl_suite.c.id))))
2681
2682         mapper(DBBinary, self.tbl_binaries,
2683                properties = dict(binary_id = self.tbl_binaries.c.id,
2684                                  package = self.tbl_binaries.c.package,
2685                                  version = self.tbl_binaries.c.version,
2686                                  maintainer_id = self.tbl_binaries.c.maintainer,
2687                                  maintainer = relation(Maintainer),
2688                                  source_id = self.tbl_binaries.c.source,
2689                                  source = relation(DBSource, backref='binaries'),
2690                                  arch_id = self.tbl_binaries.c.architecture,
2691                                  architecture = relation(Architecture),
2692                                  poolfile_id = self.tbl_binaries.c.file,
2693                                  poolfile = relation(PoolFile),
2694                                  binarytype = self.tbl_binaries.c.type,
2695                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
2696                                  fingerprint = relation(Fingerprint),
2697                                  install_date = self.tbl_binaries.c.install_date,
2698                                  suites = relation(Suite, secondary=self.tbl_bin_associations,
2699                                      backref=backref('binaries', lazy='dynamic')),
2700                                  extra_sources = relation(DBSource, secondary=self.tbl_extra_src_references,
2701                                      backref=backref('extra_binary_references', lazy='dynamic')),
2702                                  key = relation(BinaryMetadata, cascade='all',
2703                                      collection_class=attribute_mapped_collection('key'))),
2704                 extension = validator)
2705
2706         mapper(Component, self.tbl_component,
2707                properties = dict(component_id = self.tbl_component.c.id,
2708                                  component_name = self.tbl_component.c.name),
2709                extension = validator)
2710
2711         mapper(DBConfig, self.tbl_config,
2712                properties = dict(config_id = self.tbl_config.c.id))
2713
2714         mapper(DSCFile, self.tbl_dsc_files,
2715                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
2716                                  source_id = self.tbl_dsc_files.c.source,
2717                                  source = relation(DBSource),
2718                                  poolfile_id = self.tbl_dsc_files.c.file,
2719                                  poolfile = relation(PoolFile)))
2720
2721         mapper(ExternalOverride, self.tbl_external_overrides,
2722                 properties = dict(
2723                     suite_id = self.tbl_external_overrides.c.suite,
2724                     suite = relation(Suite),
2725                     component_id = self.tbl_external_overrides.c.component,
2726                     component = relation(Component)))
2727
2728         mapper(PoolFile, self.tbl_files,
2729                properties = dict(file_id = self.tbl_files.c.id,
2730                                  filesize = self.tbl_files.c.size),
2731                 extension = validator)
2732
2733         mapper(Fingerprint, self.tbl_fingerprint,
2734                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
2735                                  uid_id = self.tbl_fingerprint.c.uid,
2736                                  uid = relation(Uid),
2737                                  keyring_id = self.tbl_fingerprint.c.keyring,
2738                                  keyring = relation(Keyring),
2739                                  acl = relation(ACL)),
2740                extension = validator)
2741
2742         mapper(Keyring, self.tbl_keyrings,
2743                properties = dict(keyring_name = self.tbl_keyrings.c.name,
2744                                  keyring_id = self.tbl_keyrings.c.id,
2745                                  acl = relation(ACL, primaryjoin=(self.tbl_keyrings.c.acl_id == self.tbl_acl.c.id)))),
2746
2747         mapper(DBChange, self.tbl_changes,
2748                properties = dict(change_id = self.tbl_changes.c.id,
2749                                  seen = self.tbl_changes.c.seen,
2750                                  source = self.tbl_changes.c.source,
2751                                  binaries = self.tbl_changes.c.binaries,
2752                                  architecture = self.tbl_changes.c.architecture,
2753                                  distribution = self.tbl_changes.c.distribution,
2754                                  urgency = self.tbl_changes.c.urgency,
2755                                  maintainer = self.tbl_changes.c.maintainer,
2756                                  changedby = self.tbl_changes.c.changedby,
2757                                  date = self.tbl_changes.c.date,
2758                                  version = self.tbl_changes.c.version))
2759
2760         mapper(Maintainer, self.tbl_maintainer,
2761                properties = dict(maintainer_id = self.tbl_maintainer.c.id,
2762                    maintains_sources = relation(DBSource, backref='maintainer',
2763                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.maintainer)),
2764                    changed_sources = relation(DBSource, backref='changedby',
2765                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.changedby))),
2766                 extension = validator)
2767
2768         mapper(NewComment, self.tbl_new_comments,
2769                properties = dict(comment_id = self.tbl_new_comments.c.id,
2770                                  policy_queue = relation(PolicyQueue)))
2771
2772         mapper(Override, self.tbl_override,
2773                properties = dict(suite_id = self.tbl_override.c.suite,
2774                                  suite = relation(Suite, \
2775                                     backref=backref('overrides', lazy='dynamic')),
2776                                  package = self.tbl_override.c.package,
2777                                  component_id = self.tbl_override.c.component,
2778                                  component = relation(Component, \
2779                                     backref=backref('overrides', lazy='dynamic')),
2780                                  priority_id = self.tbl_override.c.priority,
2781                                  priority = relation(Priority, \
2782                                     backref=backref('overrides', lazy='dynamic')),
2783                                  section_id = self.tbl_override.c.section,
2784                                  section = relation(Section, \
2785                                     backref=backref('overrides', lazy='dynamic')),
2786                                  overridetype_id = self.tbl_override.c.type,
2787                                  overridetype = relation(OverrideType, \
2788                                     backref=backref('overrides', lazy='dynamic'))))
2789
2790         mapper(OverrideType, self.tbl_override_type,
2791                properties = dict(overridetype = self.tbl_override_type.c.type,
2792                                  overridetype_id = self.tbl_override_type.c.id))
2793
2794         mapper(PolicyQueue, self.tbl_policy_queue,
2795                properties = dict(policy_queue_id = self.tbl_policy_queue.c.id,
2796                                  suite = relation(Suite, primaryjoin=(self.tbl_policy_queue.c.suite_id == self.tbl_suite.c.id))))
2797
2798         mapper(PolicyQueueUpload, self.tbl_policy_queue_upload,
2799                properties = dict(
2800                    changes = relation(DBChange),
2801                    policy_queue = relation(PolicyQueue, backref='uploads'),
2802                    target_suite = relation(Suite),
2803                    source = relation(DBSource),
2804                    binaries = relation(DBBinary, secondary=self.tbl_policy_queue_upload_binaries_map),
2805                 ))
2806
2807         mapper(PolicyQueueByhandFile, self.tbl_policy_queue_byhand_file,
2808                properties = dict(
2809                    upload = relation(PolicyQueueUpload, backref='byhand'),
2810                    )
2811                )
2812
2813         mapper(Priority, self.tbl_priority,
2814                properties = dict(priority_id = self.tbl_priority.c.id))
2815
2816         mapper(Section, self.tbl_section,
2817                properties = dict(section_id = self.tbl_section.c.id,
2818                                  section=self.tbl_section.c.section))
2819
2820         mapper(SignatureHistory, self.tbl_signature_history)
2821
2822         mapper(DBSource, self.tbl_source,
2823                properties = dict(source_id = self.tbl_source.c.id,
2824                                  version = self.tbl_source.c.version,
2825                                  maintainer_id = self.tbl_source.c.maintainer,
2826                                  poolfile_id = self.tbl_source.c.file,
2827                                  poolfile = relation(PoolFile),
2828                                  fingerprint_id = self.tbl_source.c.sig_fpr,
2829                                  fingerprint = relation(Fingerprint),
2830                                  changedby_id = self.tbl_source.c.changedby,
2831                                  srcfiles = relation(DSCFile,
2832                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
2833                                  suites = relation(Suite, secondary=self.tbl_src_associations,
2834                                      backref=backref('sources', lazy='dynamic')),
2835                                  uploaders = relation(Maintainer,
2836                                      secondary=self.tbl_src_uploaders),
2837                                  key = relation(SourceMetadata, cascade='all',
2838                                      collection_class=attribute_mapped_collection('key'))),
2839                extension = validator)
2840
2841         mapper(SrcFormat, self.tbl_src_format,
2842                properties = dict(src_format_id = self.tbl_src_format.c.id,
2843                                  format_name = self.tbl_src_format.c.format_name))
2844
2845         mapper(Suite, self.tbl_suite,
2846                properties = dict(suite_id = self.tbl_suite.c.id,
2847                                  policy_queue = relation(PolicyQueue, primaryjoin=(self.tbl_suite.c.policy_queue_id == self.tbl_policy_queue.c.id)),
2848                                  new_queue = relation(PolicyQueue, primaryjoin=(self.tbl_suite.c.new_queue_id == self.tbl_policy_queue.c.id)),
2849                                  copy_queues = relation(BuildQueue,
2850                                      secondary=self.tbl_suite_build_queue_copy),
2851                                  srcformats = relation(SrcFormat, secondary=self.tbl_suite_src_formats,
2852                                      backref=backref('suites', lazy='dynamic')),
2853                                  archive = relation(Archive, backref='suites'),
2854                                  acls = relation(ACL, secondary=self.tbl_suite_acl_map, collection_class=set),
2855                                  components = relation(Component, secondary=self.tbl_component_suite,
2856                                                    order_by=self.tbl_component.c.ordering,
2857                                                    backref=backref('suites'))),
2858                 extension = validator)
2859
2860         mapper(Uid, self.tbl_uid,
2861                properties = dict(uid_id = self.tbl_uid.c.id,
2862                                  fingerprint = relation(Fingerprint)),
2863                extension = validator)
2864
2865         mapper(BinContents, self.tbl_bin_contents,
2866             properties = dict(
2867                 binary = relation(DBBinary,
2868                     backref=backref('contents', lazy='dynamic', cascade='all')),
2869                 file = self.tbl_bin_contents.c.file))
2870
2871         mapper(SrcContents, self.tbl_src_contents,
2872             properties = dict(
2873                 source = relation(DBSource,
2874                     backref=backref('contents', lazy='dynamic', cascade='all')),
2875                 file = self.tbl_src_contents.c.file))
2876
2877         mapper(MetadataKey, self.tbl_metadata_keys,
2878             properties = dict(
2879                 key_id = self.tbl_metadata_keys.c.key_id,
2880                 key = self.tbl_metadata_keys.c.key))
2881
2882         mapper(BinaryMetadata, self.tbl_binaries_metadata,
2883             properties = dict(
2884                 binary_id = self.tbl_binaries_metadata.c.bin_id,
2885                 binary = relation(DBBinary),
2886                 key_id = self.tbl_binaries_metadata.c.key_id,
2887                 key = relation(MetadataKey),
2888                 value = self.tbl_binaries_metadata.c.value))
2889
2890         mapper(SourceMetadata, self.tbl_source_metadata,
2891             properties = dict(
2892                 source_id = self.tbl_source_metadata.c.src_id,
2893                 source = relation(DBSource),
2894                 key_id = self.tbl_source_metadata.c.key_id,
2895                 key = relation(MetadataKey),
2896                 value = self.tbl_source_metadata.c.value))
2897
2898         mapper(VersionCheck, self.tbl_version_check,
2899             properties = dict(
2900                 suite_id = self.tbl_version_check.c.suite,
2901                 suite = relation(Suite, primaryjoin=self.tbl_version_check.c.suite==self.tbl_suite.c.id),
2902                 reference_id = self.tbl_version_check.c.reference,
2903                 reference = relation(Suite, primaryjoin=self.tbl_version_check.c.reference==self.tbl_suite.c.id, lazy='joined')))
2904
2905     ## Connection functions
2906     def __createconn(self):
2907         from config import Config
2908         cnf = Config()
2909         if cnf.has_key("DB::Service"):
2910             connstr = "postgresql://service=%s" % cnf["DB::Service"]
2911         elif cnf.has_key("DB::Host"):
2912             # TCP/IP
2913             connstr = "postgresql://%s" % cnf["DB::Host"]
2914             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
2915                 connstr += ":%s" % cnf["DB::Port"]
2916             connstr += "/%s" % cnf["DB::Name"]
2917         else:
2918             # Unix Socket
2919             connstr = "postgresql:///%s" % cnf["DB::Name"]
2920             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
2921                 connstr += "?port=%s" % cnf["DB::Port"]
2922
2923         engine_args = { 'echo': self.debug }
2924         if cnf.has_key('DB::PoolSize'):
2925             engine_args['pool_size'] = int(cnf['DB::PoolSize'])
2926         if cnf.has_key('DB::MaxOverflow'):
2927             engine_args['max_overflow'] = int(cnf['DB::MaxOverflow'])
2928         if sa_major_version != '0.5' and cnf.has_key('DB::Unicode') and \
2929             cnf['DB::Unicode'] == 'false':
2930             engine_args['use_native_unicode'] = False
2931
2932         # Monkey patch a new dialect in in order to support service= syntax
2933         import sqlalchemy.dialects.postgresql
2934         from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
2935         class PGDialect_psycopg2_dak(PGDialect_psycopg2):
2936             def create_connect_args(self, url):
2937                 if str(url).startswith('postgresql://service='):
2938                     # Eww
2939                     servicename = str(url)[21:]
2940                     return (['service=%s' % servicename], {})
2941                 else:
2942                     return PGDialect_psycopg2.create_connect_args(self, url)
2943
2944         sqlalchemy.dialects.postgresql.base.dialect = PGDialect_psycopg2_dak
2945
2946         try:
2947             self.db_pg   = create_engine(connstr, **engine_args)
2948             self.db_meta = MetaData()
2949             self.db_meta.bind = self.db_pg
2950             self.db_smaker = sessionmaker(bind=self.db_pg,
2951                                           autoflush=True,
2952                                           autocommit=False)
2953
2954             self.__setuptables()
2955             self.__setupmappers()
2956
2957         except OperationalError as e:
2958             import utils
2959             utils.fubar("Cannot connect to database (%s)" % str(e))
2960
2961         self.pid = os.getpid()
2962
2963     def session(self, work_mem = 0):
2964         '''
2965         Returns a new session object. If a work_mem parameter is provided a new
2966         transaction is started and the work_mem parameter is set for this
2967         transaction. The work_mem parameter is measured in MB. A default value
2968         will be used if the parameter is not set.
2969         '''
2970         # reinitialize DBConn in new processes
2971         if self.pid != os.getpid():
2972             clear_mappers()
2973             self.__createconn()
2974         session = self.db_smaker()
2975         if work_mem > 0:
2976             session.execute("SET LOCAL work_mem TO '%d MB'" % work_mem)
2977         return session
2978
2979 __all__.append('DBConn')