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