]> git.donarmstrong.com Git - dak.git/blob - daklib/dbconn.py
Add database update to contain release_suite field
[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     def query(self, session):
1863         return session.query(SignatureHistory).filter_by(fingerprint=self.fingerprint, signature_timestamp=self.signature_timestamp, contents_sha1=self.contents_sha1).first()
1864
1865 __all__.append('SignatureHistory')
1866
1867 ################################################################################
1868
1869 class SrcContents(ORMObject):
1870     def __init__(self, file = None, source = None):
1871         self.file = file
1872         self.source = source
1873
1874     def properties(self):
1875         return ['file', 'source']
1876
1877 __all__.append('SrcContents')
1878
1879 ################################################################################
1880
1881 from debian.debfile import Deb822
1882
1883 # Temporary Deb822 subclass to fix bugs with : handling; see #597249
1884 class Dak822(Deb822):
1885     def _internal_parser(self, sequence, fields=None):
1886         # The key is non-whitespace, non-colon characters before any colon.
1887         key_part = r"^(?P<key>[^: \t\n\r\f\v]+)\s*:\s*"
1888         single = re.compile(key_part + r"(?P<data>\S.*?)\s*$")
1889         multi = re.compile(key_part + r"$")
1890         multidata = re.compile(r"^\s(?P<data>.+?)\s*$")
1891
1892         wanted_field = lambda f: fields is None or f in fields
1893
1894         if isinstance(sequence, basestring):
1895             sequence = sequence.splitlines()
1896
1897         curkey = None
1898         content = ""
1899         for line in self.gpg_stripped_paragraph(sequence):
1900             m = single.match(line)
1901             if m:
1902                 if curkey:
1903                     self[curkey] = content
1904
1905                 if not wanted_field(m.group('key')):
1906                     curkey = None
1907                     continue
1908
1909                 curkey = m.group('key')
1910                 content = m.group('data')
1911                 continue
1912
1913             m = multi.match(line)
1914             if m:
1915                 if curkey:
1916                     self[curkey] = content
1917
1918                 if not wanted_field(m.group('key')):
1919                     curkey = None
1920                     continue
1921
1922                 curkey = m.group('key')
1923                 content = ""
1924                 continue
1925
1926             m = multidata.match(line)
1927             if m:
1928                 content += '\n' + line # XXX not m.group('data')?
1929                 continue
1930
1931         if curkey:
1932             self[curkey] = content
1933
1934
1935 class DBSource(ORMObject):
1936     def __init__(self, source = None, version = None, maintainer = None, \
1937         changedby = None, poolfile = None, install_date = None, fingerprint = None):
1938         self.source = source
1939         self.version = version
1940         self.maintainer = maintainer
1941         self.changedby = changedby
1942         self.poolfile = poolfile
1943         self.install_date = install_date
1944         self.fingerprint = fingerprint
1945
1946     @property
1947     def pkid(self):
1948         return self.source_id
1949
1950     def properties(self):
1951         return ['source', 'source_id', 'maintainer', 'changedby', \
1952             'fingerprint', 'poolfile', 'version', 'suites_count', \
1953             'install_date', 'binaries_count', 'uploaders_count']
1954
1955     def not_null_constraints(self):
1956         return ['source', 'version', 'install_date', 'maintainer', \
1957             'changedby', 'poolfile']
1958
1959     def read_control_fields(self):
1960         '''
1961         Reads the control information from a dsc
1962
1963         @rtype: tuple
1964         @return: fields is the dsc information in a dictionary form
1965         '''
1966         fullpath = self.poolfile.fullpath
1967         fields = Dak822(open(self.poolfile.fullpath, 'r'))
1968         return fields
1969
1970     metadata = association_proxy('key', 'value')
1971
1972     def scan_contents(self):
1973         '''
1974         Returns a set of names for non directories. The path names are
1975         normalized after converting them from either utf-8 or iso8859-1
1976         encoding.
1977         '''
1978         fullpath = self.poolfile.fullpath
1979         from daklib.contents import UnpackedSource
1980         unpacked = UnpackedSource(fullpath)
1981         fileset = set()
1982         for name in unpacked.get_all_filenames():
1983             # enforce proper utf-8 encoding
1984             try:
1985                 name.decode('utf-8')
1986             except UnicodeDecodeError:
1987                 name = name.decode('iso8859-1').encode('utf-8')
1988             fileset.add(name)
1989         return fileset
1990
1991     @property
1992     def proxy(self):
1993         session = object_session(self)
1994         query = session.query(SourceMetadata).filter_by(source=self)
1995         return MetadataProxy(session, query)
1996
1997 __all__.append('DBSource')
1998
1999 @session_wrapper
2000 def source_exists(source, source_version, suites = ["any"], session=None):
2001     """
2002     Ensure that source exists somewhere in the archive for the binary
2003     upload being processed.
2004       1. exact match     => 1.0-3
2005       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
2006
2007     @type source: string
2008     @param source: source name
2009
2010     @type source_version: string
2011     @param source_version: expected source version
2012
2013     @type suites: list
2014     @param suites: list of suites to check in, default I{any}
2015
2016     @type session: Session
2017     @param session: Optional SQLA session object (a temporary one will be
2018     generated if not supplied)
2019
2020     @rtype: int
2021     @return: returns 1 if a source with expected version is found, otherwise 0
2022
2023     """
2024
2025     cnf = Config()
2026     ret = True
2027
2028     from daklib.regexes import re_bin_only_nmu
2029     orig_source_version = re_bin_only_nmu.sub('', source_version)
2030
2031     for suite in suites:
2032         q = session.query(DBSource).filter_by(source=source). \
2033             filter(DBSource.version.in_([source_version, orig_source_version]))
2034         if suite != "any":
2035             # source must exist in 'suite' or a suite that is enhanced by 'suite'
2036             s = get_suite(suite, session)
2037             if s:
2038                 enhances_vcs = session.query(VersionCheck).filter(VersionCheck.suite==s).filter_by(check='Enhances')
2039                 considered_suites = [ vc.reference for vc in enhances_vcs ]
2040                 considered_suites.append(s)
2041
2042                 q = q.filter(DBSource.suites.any(Suite.suite_id.in_([s.suite_id for s in considered_suites])))
2043
2044         if q.count() > 0:
2045             continue
2046
2047         # No source found so return not ok
2048         ret = False
2049
2050     return ret
2051
2052 __all__.append('source_exists')
2053
2054 @session_wrapper
2055 def get_suites_source_in(source, session=None):
2056     """
2057     Returns list of Suite objects which given C{source} name is in
2058
2059     @type source: str
2060     @param source: DBSource package name to search for
2061
2062     @rtype: list
2063     @return: list of Suite objects for the given source
2064     """
2065
2066     return session.query(Suite).filter(Suite.sources.any(source=source)).all()
2067
2068 __all__.append('get_suites_source_in')
2069
2070 @session_wrapper
2071 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
2072     """
2073     Returns list of DBSource objects for given C{source} name and other parameters
2074
2075     @type source: str
2076     @param source: DBSource package name to search for
2077
2078     @type version: str or None
2079     @param version: DBSource version name to search for or None if not applicable
2080
2081     @type dm_upload_allowed: bool
2082     @param dm_upload_allowed: If None, no effect.  If True or False, only
2083     return packages with that dm_upload_allowed setting
2084
2085     @type session: Session
2086     @param session: Optional SQL session object (a temporary one will be
2087     generated if not supplied)
2088
2089     @rtype: list
2090     @return: list of DBSource objects for the given name (may be empty)
2091     """
2092
2093     q = session.query(DBSource).filter_by(source=source)
2094
2095     if version is not None:
2096         q = q.filter_by(version=version)
2097
2098     if dm_upload_allowed is not None:
2099         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
2100
2101     return q.all()
2102
2103 __all__.append('get_sources_from_name')
2104
2105 # FIXME: This function fails badly if it finds more than 1 source package and
2106 # its implementation is trivial enough to be inlined.
2107 @session_wrapper
2108 def get_source_in_suite(source, suite_name, session=None):
2109     """
2110     Returns a DBSource object for a combination of C{source} and C{suite_name}.
2111
2112       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2113       - B{suite_name} - a suite name, eg. I{unstable}
2114
2115     @type source: string
2116     @param source: source package name
2117
2118     @type suite_name: string
2119     @param suite: the suite name
2120
2121     @rtype: string
2122     @return: the version for I{source} in I{suite}
2123
2124     """
2125     suite = get_suite(suite_name, session)
2126     if suite is None:
2127         return None
2128     try:
2129         return suite.get_sources(source).one()
2130     except NoResultFound:
2131         return None
2132
2133 __all__.append('get_source_in_suite')
2134
2135 @session_wrapper
2136 def import_metadata_into_db(obj, session=None):
2137     """
2138     This routine works on either DBBinary or DBSource objects and imports
2139     their metadata into the database
2140     """
2141     fields = obj.read_control_fields()
2142     for k in fields.keys():
2143         try:
2144             # Try raw ASCII
2145             val = str(fields[k])
2146         except UnicodeEncodeError:
2147             # Fall back to UTF-8
2148             try:
2149                 val = fields[k].encode('utf-8')
2150             except UnicodeEncodeError:
2151                 # Finally try iso8859-1
2152                 val = fields[k].encode('iso8859-1')
2153                 # Otherwise we allow the exception to percolate up and we cause
2154                 # a reject as someone is playing silly buggers
2155
2156         obj.metadata[get_or_set_metadatakey(k, session)] = val
2157
2158     session.commit_or_flush()
2159
2160 __all__.append('import_metadata_into_db')
2161
2162 ################################################################################
2163
2164 class SrcFormat(object):
2165     def __init__(self, *args, **kwargs):
2166         pass
2167
2168     def __repr__(self):
2169         return '<SrcFormat %s>' % (self.format_name)
2170
2171 __all__.append('SrcFormat')
2172
2173 ################################################################################
2174
2175 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
2176                  ('SuiteID', 'suite_id'),
2177                  ('Version', 'version'),
2178                  ('Origin', 'origin'),
2179                  ('Label', 'label'),
2180                  ('Description', 'description'),
2181                  ('Untouchable', 'untouchable'),
2182                  ('Announce', 'announce'),
2183                  ('Codename', 'codename'),
2184                  ('OverrideCodename', 'overridecodename'),
2185                  ('ValidTime', 'validtime'),
2186                  ('Priority', 'priority'),
2187                  ('NotAutomatic', 'notautomatic'),
2188                  ('CopyChanges', 'copychanges'),
2189                  ('OverrideSuite', 'overridesuite')]
2190
2191 # Why the heck don't we have any UNIQUE constraints in table suite?
2192 # TODO: Add UNIQUE constraints for appropriate columns.
2193 class Suite(ORMObject):
2194     def __init__(self, suite_name = None, version = None):
2195         self.suite_name = suite_name
2196         self.version = version
2197
2198     def properties(self):
2199         return ['suite_name', 'version', 'sources_count', 'binaries_count', \
2200             'overrides_count']
2201
2202     def not_null_constraints(self):
2203         return ['suite_name']
2204
2205     def __eq__(self, val):
2206         if isinstance(val, str):
2207             return (self.suite_name == val)
2208         # This signals to use the normal comparison operator
2209         return NotImplemented
2210
2211     def __ne__(self, val):
2212         if isinstance(val, str):
2213             return (self.suite_name != val)
2214         # This signals to use the normal comparison operator
2215         return NotImplemented
2216
2217     def details(self):
2218         ret = []
2219         for disp, field in SUITE_FIELDS:
2220             val = getattr(self, field, None)
2221             if val is not None:
2222                 ret.append("%s: %s" % (disp, val))
2223
2224         return "\n".join(ret)
2225
2226     def get_architectures(self, skipsrc=False, skipall=False):
2227         """
2228         Returns list of Architecture objects
2229
2230         @type skipsrc: boolean
2231         @param skipsrc: Whether to skip returning the 'source' architecture entry
2232         (Default False)
2233
2234         @type skipall: boolean
2235         @param skipall: Whether to skip returning the 'all' architecture entry
2236         (Default False)
2237
2238         @rtype: list
2239         @return: list of Architecture objects for the given name (may be empty)
2240         """
2241
2242         q = object_session(self).query(Architecture).with_parent(self)
2243         if skipsrc:
2244             q = q.filter(Architecture.arch_string != 'source')
2245         if skipall:
2246             q = q.filter(Architecture.arch_string != 'all')
2247         return q.order_by(Architecture.arch_string).all()
2248
2249     def get_sources(self, source):
2250         """
2251         Returns a query object representing DBSource that is part of C{suite}.
2252
2253           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2254
2255         @type source: string
2256         @param source: source package name
2257
2258         @rtype: sqlalchemy.orm.query.Query
2259         @return: a query of DBSource
2260
2261         """
2262
2263         session = object_session(self)
2264         return session.query(DBSource).filter_by(source = source). \
2265             with_parent(self)
2266
2267     def get_overridesuite(self):
2268         if self.overridesuite is None:
2269             return self
2270         else:
2271             return object_session(self).query(Suite).filter_by(suite_name=self.overridesuite).one()
2272
2273     @property
2274     def path(self):
2275         return os.path.join(self.archive.path, 'dists', self.suite_name)
2276
2277     @property
2278     def release_suite_output(self):
2279         if self.release_suite is not None:
2280             return self.release_suite
2281         return self.suite_name
2282
2283 __all__.append('Suite')
2284
2285 @session_wrapper
2286 def get_suite(suite, session=None):
2287     """
2288     Returns Suite object for given C{suite name}.
2289
2290     @type suite: string
2291     @param suite: The name of the suite
2292
2293     @type session: Session
2294     @param session: Optional SQLA session object (a temporary one will be
2295     generated if not supplied)
2296
2297     @rtype: Suite
2298     @return: Suite object for the requested suite name (None if not present)
2299     """
2300
2301     q = session.query(Suite).filter_by(suite_name=suite)
2302
2303     try:
2304         return q.one()
2305     except NoResultFound:
2306         return None
2307
2308 __all__.append('get_suite')
2309
2310 ################################################################################
2311
2312 @session_wrapper
2313 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
2314     """
2315     Returns list of Architecture objects for given C{suite} name. The list is
2316     empty if suite does not exist.
2317
2318     @type suite: str
2319     @param suite: Suite name to search for
2320
2321     @type skipsrc: boolean
2322     @param skipsrc: Whether to skip returning the 'source' architecture entry
2323     (Default False)
2324
2325     @type skipall: boolean
2326     @param skipall: Whether to skip returning the 'all' architecture entry
2327     (Default False)
2328
2329     @type session: Session
2330     @param session: Optional SQL session object (a temporary one will be
2331     generated if not supplied)
2332
2333     @rtype: list
2334     @return: list of Architecture objects for the given name (may be empty)
2335     """
2336
2337     try:
2338         return get_suite(suite, session).get_architectures(skipsrc, skipall)
2339     except AttributeError:
2340         return []
2341
2342 __all__.append('get_suite_architectures')
2343
2344 ################################################################################
2345
2346 class Uid(ORMObject):
2347     def __init__(self, uid = None, name = None):
2348         self.uid = uid
2349         self.name = name
2350
2351     def __eq__(self, val):
2352         if isinstance(val, str):
2353             return (self.uid == val)
2354         # This signals to use the normal comparison operator
2355         return NotImplemented
2356
2357     def __ne__(self, val):
2358         if isinstance(val, str):
2359             return (self.uid != val)
2360         # This signals to use the normal comparison operator
2361         return NotImplemented
2362
2363     def properties(self):
2364         return ['uid', 'name', 'fingerprint']
2365
2366     def not_null_constraints(self):
2367         return ['uid']
2368
2369 __all__.append('Uid')
2370
2371 @session_wrapper
2372 def get_or_set_uid(uidname, session=None):
2373     """
2374     Returns uid object for given uidname.
2375
2376     If no matching uidname is found, a row is inserted.
2377
2378     @type uidname: string
2379     @param uidname: The uid to add
2380
2381     @type session: SQLAlchemy
2382     @param session: Optional SQL session object (a temporary one will be
2383     generated if not supplied).  If not passed, a commit will be performed at
2384     the end of the function, otherwise the caller is responsible for commiting.
2385
2386     @rtype: Uid
2387     @return: the uid object for the given uidname
2388     """
2389
2390     q = session.query(Uid).filter_by(uid=uidname)
2391
2392     try:
2393         ret = q.one()
2394     except NoResultFound:
2395         uid = Uid()
2396         uid.uid = uidname
2397         session.add(uid)
2398         session.commit_or_flush()
2399         ret = uid
2400
2401     return ret
2402
2403 __all__.append('get_or_set_uid')
2404
2405 @session_wrapper
2406 def get_uid_from_fingerprint(fpr, session=None):
2407     q = session.query(Uid)
2408     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
2409
2410     try:
2411         return q.one()
2412     except NoResultFound:
2413         return None
2414
2415 __all__.append('get_uid_from_fingerprint')
2416
2417 ################################################################################
2418
2419 class MetadataKey(ORMObject):
2420     def __init__(self, key = None):
2421         self.key = key
2422
2423     def properties(self):
2424         return ['key']
2425
2426     def not_null_constraints(self):
2427         return ['key']
2428
2429 __all__.append('MetadataKey')
2430
2431 @session_wrapper
2432 def get_or_set_metadatakey(keyname, session=None):
2433     """
2434     Returns MetadataKey object for given uidname.
2435
2436     If no matching keyname is found, a row is inserted.
2437
2438     @type uidname: string
2439     @param uidname: The keyname to add
2440
2441     @type session: SQLAlchemy
2442     @param session: Optional SQL session object (a temporary one will be
2443     generated if not supplied).  If not passed, a commit will be performed at
2444     the end of the function, otherwise the caller is responsible for commiting.
2445
2446     @rtype: MetadataKey
2447     @return: the metadatakey object for the given keyname
2448     """
2449
2450     q = session.query(MetadataKey).filter_by(key=keyname)
2451
2452     try:
2453         ret = q.one()
2454     except NoResultFound:
2455         ret = MetadataKey(keyname)
2456         session.add(ret)
2457         session.commit_or_flush()
2458
2459     return ret
2460
2461 __all__.append('get_or_set_metadatakey')
2462
2463 ################################################################################
2464
2465 class BinaryMetadata(ORMObject):
2466     def __init__(self, key = None, value = None, binary = None):
2467         self.key = key
2468         self.value = value
2469         self.binary = binary
2470
2471     def properties(self):
2472         return ['binary', 'key', 'value']
2473
2474     def not_null_constraints(self):
2475         return ['value']
2476
2477 __all__.append('BinaryMetadata')
2478
2479 ################################################################################
2480
2481 class SourceMetadata(ORMObject):
2482     def __init__(self, key = None, value = None, source = None):
2483         self.key = key
2484         self.value = value
2485         self.source = source
2486
2487     def properties(self):
2488         return ['source', 'key', 'value']
2489
2490     def not_null_constraints(self):
2491         return ['value']
2492
2493 __all__.append('SourceMetadata')
2494
2495 ################################################################################
2496
2497 class MetadataProxy(object):
2498     def __init__(self, session, query):
2499         self.session = session
2500         self.query = query
2501
2502     def _get(self, key):
2503         metadata_key = self.session.query(MetadataKey).filter_by(key=key).first()
2504         if metadata_key is None:
2505             return None
2506         metadata = self.query.filter_by(key=metadata_key).first()
2507         return metadata
2508
2509     def __contains__(self, key):
2510         if self._get(key) is not None:
2511             return True
2512         return False
2513
2514     def __getitem__(self, key):
2515         metadata = self._get(key)
2516         if metadata is None:
2517             raise KeyError
2518         return metadata.value
2519
2520     def get(self, key, default=None):
2521         try:
2522             return self[key]
2523         except KeyError:
2524             return default
2525
2526 ################################################################################
2527
2528 class VersionCheck(ORMObject):
2529     def __init__(self, *args, **kwargs):
2530         pass
2531
2532     def properties(self):
2533         #return ['suite_id', 'check', 'reference_id']
2534         return ['check']
2535
2536     def not_null_constraints(self):
2537         return ['suite', 'check', 'reference']
2538
2539 __all__.append('VersionCheck')
2540
2541 @session_wrapper
2542 def get_version_checks(suite_name, check = None, session = None):
2543     suite = get_suite(suite_name, session)
2544     if not suite:
2545         # Make sure that what we return is iterable so that list comprehensions
2546         # involving this don't cause a traceback
2547         return []
2548     q = session.query(VersionCheck).filter_by(suite=suite)
2549     if check:
2550         q = q.filter_by(check=check)
2551     return q.all()
2552
2553 __all__.append('get_version_checks')
2554
2555 ################################################################################
2556
2557 class DBConn(object):
2558     """
2559     database module init.
2560     """
2561     __shared_state = {}
2562
2563     def __init__(self, *args, **kwargs):
2564         self.__dict__ = self.__shared_state
2565
2566         if not getattr(self, 'initialised', False):
2567             self.initialised = True
2568             self.debug = kwargs.has_key('debug')
2569             self.__createconn()
2570
2571     def __setuptables(self):
2572         tables = (
2573             'acl',
2574             'acl_architecture_map',
2575             'acl_fingerprint_map',
2576             'acl_per_source',
2577             'architecture',
2578             'archive',
2579             'bin_associations',
2580             'bin_contents',
2581             'binaries',
2582             'binaries_metadata',
2583             'build_queue',
2584             'changelogs_text',
2585             'changes',
2586             'component',
2587             'component_suite',
2588             'config',
2589             'dsc_files',
2590             'external_overrides',
2591             'extra_src_references',
2592             'files',
2593             'files_archive_map',
2594             'fingerprint',
2595             'keyrings',
2596             'maintainer',
2597             'metadata_keys',
2598             'new_comments',
2599             # TODO: the maintainer column in table override should be removed.
2600             'override',
2601             'override_type',
2602             'policy_queue',
2603             'policy_queue_upload',
2604             'policy_queue_upload_binaries_map',
2605             'policy_queue_byhand_file',
2606             'priority',
2607             'section',
2608             'signature_history',
2609             'source',
2610             'source_metadata',
2611             'src_associations',
2612             'src_contents',
2613             'src_format',
2614             'src_uploaders',
2615             'suite',
2616             'suite_acl_map',
2617             'suite_architectures',
2618             'suite_build_queue_copy',
2619             'suite_src_formats',
2620             'uid',
2621             'version_check',
2622         )
2623
2624         views = (
2625             'almost_obsolete_all_associations',
2626             'almost_obsolete_src_associations',
2627             'any_associations_source',
2628             'bin_associations_binaries',
2629             'binaries_suite_arch',
2630             'changelogs',
2631             'file_arch_suite',
2632             'newest_all_associations',
2633             'newest_any_associations',
2634             'newest_source',
2635             'newest_src_association',
2636             'obsolete_all_associations',
2637             'obsolete_any_associations',
2638             'obsolete_any_by_all_associations',
2639             'obsolete_src_associations',
2640             'source_suite',
2641             'src_associations_bin',
2642             'src_associations_src',
2643             'suite_arch_by_name',
2644         )
2645
2646         for table_name in tables:
2647             table = Table(table_name, self.db_meta, \
2648                 autoload=True, useexisting=True)
2649             setattr(self, 'tbl_%s' % table_name, table)
2650
2651         for view_name in views:
2652             view = Table(view_name, self.db_meta, autoload=True)
2653             setattr(self, 'view_%s' % view_name, view)
2654
2655     def __setupmappers(self):
2656         mapper(Architecture, self.tbl_architecture,
2657             properties = dict(arch_id = self.tbl_architecture.c.id,
2658                suites = relation(Suite, secondary=self.tbl_suite_architectures,
2659                    order_by=self.tbl_suite.c.suite_name,
2660                    backref=backref('architectures', order_by=self.tbl_architecture.c.arch_string))),
2661             extension = validator)
2662
2663         mapper(ACL, self.tbl_acl,
2664                properties = dict(
2665                 architectures = relation(Architecture, secondary=self.tbl_acl_architecture_map, collection_class=set),
2666                 fingerprints = relation(Fingerprint, secondary=self.tbl_acl_fingerprint_map, collection_class=set),
2667                 match_keyring = relation(Keyring, primaryjoin=(self.tbl_acl.c.match_keyring_id == self.tbl_keyrings.c.id)),
2668                 per_source = relation(ACLPerSource, collection_class=set),
2669                 ))
2670
2671         mapper(ACLPerSource, self.tbl_acl_per_source,
2672                properties = dict(
2673                 acl = relation(ACL),
2674                 fingerprint = relation(Fingerprint, primaryjoin=(self.tbl_acl_per_source.c.fingerprint_id == self.tbl_fingerprint.c.id)),
2675                 created_by = relation(Fingerprint, primaryjoin=(self.tbl_acl_per_source.c.created_by_id == self.tbl_fingerprint.c.id)),
2676                 ))
2677
2678         mapper(Archive, self.tbl_archive,
2679                properties = dict(archive_id = self.tbl_archive.c.id,
2680                                  archive_name = self.tbl_archive.c.name))
2681
2682         mapper(ArchiveFile, self.tbl_files_archive_map,
2683                properties = dict(archive = relation(Archive, backref='files'),
2684                                  component = relation(Component),
2685                                  file = relation(PoolFile, backref='archives')))
2686
2687         mapper(BuildQueue, self.tbl_build_queue,
2688                properties = dict(queue_id = self.tbl_build_queue.c.id,
2689                                  suite = relation(Suite, primaryjoin=(self.tbl_build_queue.c.suite_id==self.tbl_suite.c.id))))
2690
2691         mapper(DBBinary, self.tbl_binaries,
2692                properties = dict(binary_id = self.tbl_binaries.c.id,
2693                                  package = self.tbl_binaries.c.package,
2694                                  version = self.tbl_binaries.c.version,
2695                                  maintainer_id = self.tbl_binaries.c.maintainer,
2696                                  maintainer = relation(Maintainer),
2697                                  source_id = self.tbl_binaries.c.source,
2698                                  source = relation(DBSource, backref='binaries'),
2699                                  arch_id = self.tbl_binaries.c.architecture,
2700                                  architecture = relation(Architecture),
2701                                  poolfile_id = self.tbl_binaries.c.file,
2702                                  poolfile = relation(PoolFile),
2703                                  binarytype = self.tbl_binaries.c.type,
2704                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
2705                                  fingerprint = relation(Fingerprint),
2706                                  install_date = self.tbl_binaries.c.install_date,
2707                                  suites = relation(Suite, secondary=self.tbl_bin_associations,
2708                                      backref=backref('binaries', lazy='dynamic')),
2709                                  extra_sources = relation(DBSource, secondary=self.tbl_extra_src_references,
2710                                      backref=backref('extra_binary_references', lazy='dynamic')),
2711                                  key = relation(BinaryMetadata, cascade='all',
2712                                      collection_class=attribute_mapped_collection('key'))),
2713                 extension = validator)
2714
2715         mapper(Component, self.tbl_component,
2716                properties = dict(component_id = self.tbl_component.c.id,
2717                                  component_name = self.tbl_component.c.name),
2718                extension = validator)
2719
2720         mapper(DBConfig, self.tbl_config,
2721                properties = dict(config_id = self.tbl_config.c.id))
2722
2723         mapper(DSCFile, self.tbl_dsc_files,
2724                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
2725                                  source_id = self.tbl_dsc_files.c.source,
2726                                  source = relation(DBSource),
2727                                  poolfile_id = self.tbl_dsc_files.c.file,
2728                                  poolfile = relation(PoolFile)))
2729
2730         mapper(ExternalOverride, self.tbl_external_overrides,
2731                 properties = dict(
2732                     suite_id = self.tbl_external_overrides.c.suite,
2733                     suite = relation(Suite),
2734                     component_id = self.tbl_external_overrides.c.component,
2735                     component = relation(Component)))
2736
2737         mapper(PoolFile, self.tbl_files,
2738                properties = dict(file_id = self.tbl_files.c.id,
2739                                  filesize = self.tbl_files.c.size),
2740                 extension = validator)
2741
2742         mapper(Fingerprint, self.tbl_fingerprint,
2743                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
2744                                  uid_id = self.tbl_fingerprint.c.uid,
2745                                  uid = relation(Uid),
2746                                  keyring_id = self.tbl_fingerprint.c.keyring,
2747                                  keyring = relation(Keyring),
2748                                  acl = relation(ACL)),
2749                extension = validator)
2750
2751         mapper(Keyring, self.tbl_keyrings,
2752                properties = dict(keyring_name = self.tbl_keyrings.c.name,
2753                                  keyring_id = self.tbl_keyrings.c.id,
2754                                  acl = relation(ACL, primaryjoin=(self.tbl_keyrings.c.acl_id == self.tbl_acl.c.id)))),
2755
2756         mapper(DBChange, self.tbl_changes,
2757                properties = dict(change_id = self.tbl_changes.c.id,
2758                                  seen = self.tbl_changes.c.seen,
2759                                  source = self.tbl_changes.c.source,
2760                                  binaries = self.tbl_changes.c.binaries,
2761                                  architecture = self.tbl_changes.c.architecture,
2762                                  distribution = self.tbl_changes.c.distribution,
2763                                  urgency = self.tbl_changes.c.urgency,
2764                                  maintainer = self.tbl_changes.c.maintainer,
2765                                  changedby = self.tbl_changes.c.changedby,
2766                                  date = self.tbl_changes.c.date,
2767                                  version = self.tbl_changes.c.version))
2768
2769         mapper(Maintainer, self.tbl_maintainer,
2770                properties = dict(maintainer_id = self.tbl_maintainer.c.id,
2771                    maintains_sources = relation(DBSource, backref='maintainer',
2772                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.maintainer)),
2773                    changed_sources = relation(DBSource, backref='changedby',
2774                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.changedby))),
2775                 extension = validator)
2776
2777         mapper(NewComment, self.tbl_new_comments,
2778                properties = dict(comment_id = self.tbl_new_comments.c.id,
2779                                  policy_queue = relation(PolicyQueue)))
2780
2781         mapper(Override, self.tbl_override,
2782                properties = dict(suite_id = self.tbl_override.c.suite,
2783                                  suite = relation(Suite, \
2784                                     backref=backref('overrides', lazy='dynamic')),
2785                                  package = self.tbl_override.c.package,
2786                                  component_id = self.tbl_override.c.component,
2787                                  component = relation(Component, \
2788                                     backref=backref('overrides', lazy='dynamic')),
2789                                  priority_id = self.tbl_override.c.priority,
2790                                  priority = relation(Priority, \
2791                                     backref=backref('overrides', lazy='dynamic')),
2792                                  section_id = self.tbl_override.c.section,
2793                                  section = relation(Section, \
2794                                     backref=backref('overrides', lazy='dynamic')),
2795                                  overridetype_id = self.tbl_override.c.type,
2796                                  overridetype = relation(OverrideType, \
2797                                     backref=backref('overrides', lazy='dynamic'))))
2798
2799         mapper(OverrideType, self.tbl_override_type,
2800                properties = dict(overridetype = self.tbl_override_type.c.type,
2801                                  overridetype_id = self.tbl_override_type.c.id))
2802
2803         mapper(PolicyQueue, self.tbl_policy_queue,
2804                properties = dict(policy_queue_id = self.tbl_policy_queue.c.id,
2805                                  suite = relation(Suite, primaryjoin=(self.tbl_policy_queue.c.suite_id == self.tbl_suite.c.id))))
2806
2807         mapper(PolicyQueueUpload, self.tbl_policy_queue_upload,
2808                properties = dict(
2809                    changes = relation(DBChange),
2810                    policy_queue = relation(PolicyQueue, backref='uploads'),
2811                    target_suite = relation(Suite),
2812                    source = relation(DBSource),
2813                    binaries = relation(DBBinary, secondary=self.tbl_policy_queue_upload_binaries_map),
2814                 ))
2815
2816         mapper(PolicyQueueByhandFile, self.tbl_policy_queue_byhand_file,
2817                properties = dict(
2818                    upload = relation(PolicyQueueUpload, backref='byhand'),
2819                    )
2820                )
2821
2822         mapper(Priority, self.tbl_priority,
2823                properties = dict(priority_id = self.tbl_priority.c.id))
2824
2825         mapper(Section, self.tbl_section,
2826                properties = dict(section_id = self.tbl_section.c.id,
2827                                  section=self.tbl_section.c.section))
2828
2829         mapper(SignatureHistory, self.tbl_signature_history)
2830
2831         mapper(DBSource, self.tbl_source,
2832                properties = dict(source_id = self.tbl_source.c.id,
2833                                  version = self.tbl_source.c.version,
2834                                  maintainer_id = self.tbl_source.c.maintainer,
2835                                  poolfile_id = self.tbl_source.c.file,
2836                                  poolfile = relation(PoolFile),
2837                                  fingerprint_id = self.tbl_source.c.sig_fpr,
2838                                  fingerprint = relation(Fingerprint),
2839                                  changedby_id = self.tbl_source.c.changedby,
2840                                  srcfiles = relation(DSCFile,
2841                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
2842                                  suites = relation(Suite, secondary=self.tbl_src_associations,
2843                                      backref=backref('sources', lazy='dynamic')),
2844                                  uploaders = relation(Maintainer,
2845                                      secondary=self.tbl_src_uploaders),
2846                                  key = relation(SourceMetadata, cascade='all',
2847                                      collection_class=attribute_mapped_collection('key'))),
2848                extension = validator)
2849
2850         mapper(SrcFormat, self.tbl_src_format,
2851                properties = dict(src_format_id = self.tbl_src_format.c.id,
2852                                  format_name = self.tbl_src_format.c.format_name))
2853
2854         mapper(Suite, self.tbl_suite,
2855                properties = dict(suite_id = self.tbl_suite.c.id,
2856                                  policy_queue = relation(PolicyQueue, primaryjoin=(self.tbl_suite.c.policy_queue_id == self.tbl_policy_queue.c.id)),
2857                                  new_queue = relation(PolicyQueue, primaryjoin=(self.tbl_suite.c.new_queue_id == self.tbl_policy_queue.c.id)),
2858                                  copy_queues = relation(BuildQueue,
2859                                      secondary=self.tbl_suite_build_queue_copy),
2860                                  srcformats = relation(SrcFormat, secondary=self.tbl_suite_src_formats,
2861                                      backref=backref('suites', lazy='dynamic')),
2862                                  archive = relation(Archive, backref='suites'),
2863                                  acls = relation(ACL, secondary=self.tbl_suite_acl_map, collection_class=set),
2864                                  components = relation(Component, secondary=self.tbl_component_suite,
2865                                                    order_by=self.tbl_component.c.ordering,
2866                                                    backref=backref('suites'))),
2867                 extension = validator)
2868
2869         mapper(Uid, self.tbl_uid,
2870                properties = dict(uid_id = self.tbl_uid.c.id,
2871                                  fingerprint = relation(Fingerprint)),
2872                extension = validator)
2873
2874         mapper(BinContents, self.tbl_bin_contents,
2875             properties = dict(
2876                 binary = relation(DBBinary,
2877                     backref=backref('contents', lazy='dynamic', cascade='all')),
2878                 file = self.tbl_bin_contents.c.file))
2879
2880         mapper(SrcContents, self.tbl_src_contents,
2881             properties = dict(
2882                 source = relation(DBSource,
2883                     backref=backref('contents', lazy='dynamic', cascade='all')),
2884                 file = self.tbl_src_contents.c.file))
2885
2886         mapper(MetadataKey, self.tbl_metadata_keys,
2887             properties = dict(
2888                 key_id = self.tbl_metadata_keys.c.key_id,
2889                 key = self.tbl_metadata_keys.c.key))
2890
2891         mapper(BinaryMetadata, self.tbl_binaries_metadata,
2892             properties = dict(
2893                 binary_id = self.tbl_binaries_metadata.c.bin_id,
2894                 binary = relation(DBBinary),
2895                 key_id = self.tbl_binaries_metadata.c.key_id,
2896                 key = relation(MetadataKey),
2897                 value = self.tbl_binaries_metadata.c.value))
2898
2899         mapper(SourceMetadata, self.tbl_source_metadata,
2900             properties = dict(
2901                 source_id = self.tbl_source_metadata.c.src_id,
2902                 source = relation(DBSource),
2903                 key_id = self.tbl_source_metadata.c.key_id,
2904                 key = relation(MetadataKey),
2905                 value = self.tbl_source_metadata.c.value))
2906
2907         mapper(VersionCheck, self.tbl_version_check,
2908             properties = dict(
2909                 suite_id = self.tbl_version_check.c.suite,
2910                 suite = relation(Suite, primaryjoin=self.tbl_version_check.c.suite==self.tbl_suite.c.id),
2911                 reference_id = self.tbl_version_check.c.reference,
2912                 reference = relation(Suite, primaryjoin=self.tbl_version_check.c.reference==self.tbl_suite.c.id, lazy='joined')))
2913
2914     ## Connection functions
2915     def __createconn(self):
2916         from config import Config
2917         cnf = Config()
2918         if cnf.has_key("DB::Service"):
2919             connstr = "postgresql://service=%s" % cnf["DB::Service"]
2920         elif cnf.has_key("DB::Host"):
2921             # TCP/IP
2922             connstr = "postgresql://%s" % cnf["DB::Host"]
2923             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
2924                 connstr += ":%s" % cnf["DB::Port"]
2925             connstr += "/%s" % cnf["DB::Name"]
2926         else:
2927             # Unix Socket
2928             connstr = "postgresql:///%s" % cnf["DB::Name"]
2929             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
2930                 connstr += "?port=%s" % cnf["DB::Port"]
2931
2932         engine_args = { 'echo': self.debug }
2933         if cnf.has_key('DB::PoolSize'):
2934             engine_args['pool_size'] = int(cnf['DB::PoolSize'])
2935         if cnf.has_key('DB::MaxOverflow'):
2936             engine_args['max_overflow'] = int(cnf['DB::MaxOverflow'])
2937         if sa_major_version != '0.5' and cnf.has_key('DB::Unicode') and \
2938             cnf['DB::Unicode'] == 'false':
2939             engine_args['use_native_unicode'] = False
2940
2941         # Monkey patch a new dialect in in order to support service= syntax
2942         import sqlalchemy.dialects.postgresql
2943         from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
2944         class PGDialect_psycopg2_dak(PGDialect_psycopg2):
2945             def create_connect_args(self, url):
2946                 if str(url).startswith('postgresql://service='):
2947                     # Eww
2948                     servicename = str(url)[21:]
2949                     return (['service=%s' % servicename], {})
2950                 else:
2951                     return PGDialect_psycopg2.create_connect_args(self, url)
2952
2953         sqlalchemy.dialects.postgresql.base.dialect = PGDialect_psycopg2_dak
2954
2955         try:
2956             self.db_pg   = create_engine(connstr, **engine_args)
2957             self.db_meta = MetaData()
2958             self.db_meta.bind = self.db_pg
2959             self.db_smaker = sessionmaker(bind=self.db_pg,
2960                                           autoflush=True,
2961                                           autocommit=False)
2962
2963             self.__setuptables()
2964             self.__setupmappers()
2965
2966         except OperationalError as e:
2967             import utils
2968             utils.fubar("Cannot connect to database (%s)" % str(e))
2969
2970         self.pid = os.getpid()
2971
2972     def session(self, work_mem = 0):
2973         '''
2974         Returns a new session object. If a work_mem parameter is provided a new
2975         transaction is started and the work_mem parameter is set for this
2976         transaction. The work_mem parameter is measured in MB. A default value
2977         will be used if the parameter is not set.
2978         '''
2979         # reinitialize DBConn in new processes
2980         if self.pid != os.getpid():
2981             clear_mappers()
2982             self.__createconn()
2983         session = self.db_smaker()
2984         if work_mem > 0:
2985             session.execute("SET LOCAL work_mem TO '%d MB'" % work_mem)
2986         return session
2987
2988 __all__.append('DBConn')