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