]> git.donarmstrong.com Git - dak.git/blob - daklib/dbconn.py
fix last commit: return [] instead of None
[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
85
86 ################################################################################
87
88 # Patch in support for the debversion field type so that it works during
89 # reflection
90
91 try:
92     # that is for sqlalchemy 0.6
93     UserDefinedType = sqltypes.UserDefinedType
94 except:
95     # this one for sqlalchemy 0.5
96     UserDefinedType = sqltypes.TypeEngine
97
98 class DebVersion(UserDefinedType):
99     def get_col_spec(self):
100         return "DEBVERSION"
101
102     def bind_processor(self, dialect):
103         return None
104
105     # ' = None' is needed for sqlalchemy 0.5:
106     def result_processor(self, dialect, coltype = None):
107         return None
108
109 sa_major_version = sqlalchemy.__version__[0:3]
110 if sa_major_version in ["0.5", "0.6"]:
111     from sqlalchemy.databases import postgres
112     postgres.ischema_names['debversion'] = DebVersion
113 else:
114     raise Exception("dak only ported to SQLA versions 0.5 and 0.6.  See daklib/dbconn.py")
115
116 ################################################################################
117
118 __all__ = ['IntegrityError', 'SQLAlchemyError', 'DebVersion']
119
120 ################################################################################
121
122 def session_wrapper(fn):
123     """
124     Wrapper around common ".., session=None):" handling. If the wrapped
125     function is called without passing 'session', we create a local one
126     and destroy it when the function ends.
127
128     Also attaches a commit_or_flush method to the session; if we created a
129     local session, this is a synonym for session.commit(), otherwise it is a
130     synonym for session.flush().
131     """
132
133     def wrapped(*args, **kwargs):
134         private_transaction = False
135
136         # Find the session object
137         session = kwargs.get('session')
138
139         if session is None:
140             if len(args) <= len(getargspec(fn)[0]) - 1:
141                 # No session specified as last argument or in kwargs
142                 private_transaction = True
143                 session = kwargs['session'] = DBConn().session()
144             else:
145                 # Session is last argument in args
146                 session = args[-1]
147                 if session is None:
148                     args = list(args)
149                     session = args[-1] = DBConn().session()
150                     private_transaction = True
151
152         if private_transaction:
153             session.commit_or_flush = session.commit
154         else:
155             session.commit_or_flush = session.flush
156
157         try:
158             return fn(*args, **kwargs)
159         finally:
160             if private_transaction:
161                 # We created a session; close it.
162                 session.close()
163
164     wrapped.__doc__ = fn.__doc__
165     wrapped.func_name = fn.func_name
166
167     return wrapped
168
169 __all__.append('session_wrapper')
170
171 ################################################################################
172
173 class ORMObject(object):
174     """
175     ORMObject is a base class for all ORM classes mapped by SQLalchemy. All
176     derived classes must implement the properties() method.
177     """
178
179     def properties(self):
180         '''
181         This method should be implemented by all derived classes and returns a
182         list of the important properties. The properties 'created' and
183         'modified' will be added automatically. A suffix '_count' should be
184         added to properties that are lists or query objects. The most important
185         property name should be returned as the first element in the list
186         because it is used by repr().
187         '''
188         return []
189
190     def json(self):
191         '''
192         Returns a JSON representation of the object based on the properties
193         returned from the properties() method.
194         '''
195         data = {}
196         # add created and modified
197         all_properties = self.properties() + ['created', 'modified']
198         for property in all_properties:
199             # check for list or query
200             if property[-6:] == '_count':
201                 real_property = property[:-6]
202                 if not hasattr(self, real_property):
203                     continue
204                 value = getattr(self, real_property)
205                 if hasattr(value, '__len__'):
206                     # list
207                     value = len(value)
208                 elif hasattr(value, 'count'):
209                     # query (but not during validation)
210                     if self.in_validation:
211                         continue
212                     value = value.count()
213                 else:
214                     raise KeyError('Do not understand property %s.' % property)
215             else:
216                 if not hasattr(self, property):
217                     continue
218                 # plain object
219                 value = getattr(self, property)
220                 if value is None:
221                     # skip None
222                     continue
223                 elif isinstance(value, ORMObject):
224                     # use repr() for ORMObject types
225                     value = repr(value)
226                 else:
227                     # we want a string for all other types because json cannot
228                     # encode everything
229                     value = str(value)
230             data[property] = value
231         return json.dumps(data)
232
233     def classname(self):
234         '''
235         Returns the name of the class.
236         '''
237         return type(self).__name__
238
239     def __repr__(self):
240         '''
241         Returns a short string representation of the object using the first
242         element from the properties() method.
243         '''
244         primary_property = self.properties()[0]
245         value = getattr(self, primary_property)
246         return '<%s %s>' % (self.classname(), str(value))
247
248     def __str__(self):
249         '''
250         Returns a human readable form of the object using the properties()
251         method.
252         '''
253         return '<%s %s>' % (self.classname(), self.json())
254
255     def not_null_constraints(self):
256         '''
257         Returns a list of properties that must be not NULL. Derived classes
258         should override this method if needed.
259         '''
260         return []
261
262     validation_message = \
263         "Validation failed because property '%s' must not be empty in object\n%s"
264
265     in_validation = False
266
267     def validate(self):
268         '''
269         This function validates the not NULL constraints as returned by
270         not_null_constraints(). It raises the DBUpdateError exception if
271         validation fails.
272         '''
273         for property in self.not_null_constraints():
274             # TODO: It is a bit awkward that the mapper configuration allow
275             # directly setting the numeric _id columns. We should get rid of it
276             # in the long run.
277             if hasattr(self, property + '_id') and \
278                 getattr(self, property + '_id') is not None:
279                 continue
280             if not hasattr(self, property) or getattr(self, property) is None:
281                 # str() might lead to races due to a 2nd flush
282                 self.in_validation = True
283                 message = self.validation_message % (property, str(self))
284                 self.in_validation = False
285                 raise DBUpdateError(message)
286
287     @classmethod
288     @session_wrapper
289     def get(cls, primary_key,  session = None):
290         '''
291         This is a support function that allows getting an object by its primary
292         key.
293
294         Architecture.get(3[, session])
295
296         instead of the more verbose
297
298         session.query(Architecture).get(3)
299         '''
300         return session.query(cls).get(primary_key)
301
302     def session(self, replace = False):
303         '''
304         Returns the current session that is associated with the object. May
305         return None is object is in detached state.
306         '''
307
308         return object_session(self)
309
310     def clone(self, session = None):
311         '''
312         Clones the current object in a new session and returns the new clone. A
313         fresh session is created if the optional session parameter is not
314         provided. The function will fail if a session is provided and has
315         unflushed changes.
316
317         RATIONALE: SQLAlchemy's session is not thread safe. This method clones
318         an existing object to allow several threads to work with their own
319         instances of an ORMObject.
320
321         WARNING: Only persistent (committed) objects can be cloned. Changes
322         made to the original object that are not committed yet will get lost.
323         The session of the new object will always be rolled back to avoid
324         ressource leaks.
325         '''
326
327         if self.session() is None:
328             raise RuntimeError( \
329                 'Method clone() failed for detached object:\n%s' % self)
330         self.session().flush()
331         mapper = object_mapper(self)
332         primary_key = mapper.primary_key_from_instance(self)
333         object_class = self.__class__
334         if session is None:
335             session = DBConn().session()
336         elif len(session.new) + len(session.dirty) + len(session.deleted) > 0:
337             raise RuntimeError( \
338                 'Method clone() failed due to unflushed changes in session.')
339         new_object = session.query(object_class).get(primary_key)
340         session.rollback()
341         if new_object is None:
342             raise RuntimeError( \
343                 'Method clone() failed for non-persistent object:\n%s' % self)
344         return new_object
345
346 __all__.append('ORMObject')
347
348 ################################################################################
349
350 class Validator(MapperExtension):
351     '''
352     This class calls the validate() method for each instance for the
353     'before_update' and 'before_insert' events. A global object validator is
354     used for configuring the individual mappers.
355     '''
356
357     def before_update(self, mapper, connection, instance):
358         instance.validate()
359         return EXT_CONTINUE
360
361     def before_insert(self, mapper, connection, instance):
362         instance.validate()
363         return EXT_CONTINUE
364
365 validator = Validator()
366
367 ################################################################################
368
369 class Architecture(ORMObject):
370     def __init__(self, arch_string = None, description = None):
371         self.arch_string = arch_string
372         self.description = description
373
374     def __eq__(self, val):
375         if isinstance(val, str):
376             return (self.arch_string== val)
377         # This signals to use the normal comparison operator
378         return NotImplemented
379
380     def __ne__(self, val):
381         if isinstance(val, str):
382             return (self.arch_string != val)
383         # This signals to use the normal comparison operator
384         return NotImplemented
385
386     def properties(self):
387         return ['arch_string', 'arch_id', 'suites_count']
388
389     def not_null_constraints(self):
390         return ['arch_string']
391
392 __all__.append('Architecture')
393
394 @session_wrapper
395 def get_architecture(architecture, session=None):
396     """
397     Returns database id for given C{architecture}.
398
399     @type architecture: string
400     @param architecture: The name of the architecture
401
402     @type session: Session
403     @param session: Optional SQLA session object (a temporary one will be
404     generated if not supplied)
405
406     @rtype: Architecture
407     @return: Architecture object for the given arch (None if not present)
408     """
409
410     q = session.query(Architecture).filter_by(arch_string=architecture)
411
412     try:
413         return q.one()
414     except NoResultFound:
415         return None
416
417 __all__.append('get_architecture')
418
419 # TODO: should be removed because the implementation is too trivial
420 @session_wrapper
421 def get_architecture_suites(architecture, session=None):
422     """
423     Returns list of Suite objects for given C{architecture} name
424
425     @type architecture: str
426     @param architecture: Architecture name to search for
427
428     @type session: Session
429     @param session: Optional SQL session object (a temporary one will be
430     generated if not supplied)
431
432     @rtype: list
433     @return: list of Suite objects for the given name (may be empty)
434     """
435
436     return get_architecture(architecture, session).suites
437
438 __all__.append('get_architecture_suites')
439
440 ################################################################################
441
442 class Archive(object):
443     def __init__(self, *args, **kwargs):
444         pass
445
446     def __repr__(self):
447         return '<Archive %s>' % self.archive_name
448
449 __all__.append('Archive')
450
451 @session_wrapper
452 def get_archive(archive, session=None):
453     """
454     returns database id for given C{archive}.
455
456     @type archive: string
457     @param archive: the name of the arhive
458
459     @type session: Session
460     @param session: Optional SQLA session object (a temporary one will be
461     generated if not supplied)
462
463     @rtype: Archive
464     @return: Archive object for the given name (None if not present)
465
466     """
467     archive = archive.lower()
468
469     q = session.query(Archive).filter_by(archive_name=archive)
470
471     try:
472         return q.one()
473     except NoResultFound:
474         return None
475
476 __all__.append('get_archive')
477
478 ################################################################################
479
480 class BinContents(ORMObject):
481     def __init__(self, file = None, binary = None):
482         self.file = file
483         self.binary = binary
484
485     def properties(self):
486         return ['file', 'binary']
487
488 __all__.append('BinContents')
489
490 ################################################################################
491
492 def subprocess_setup():
493     # Python installs a SIGPIPE handler by default. This is usually not what
494     # non-Python subprocesses expect.
495     signal.signal(signal.SIGPIPE, signal.SIG_DFL)
496
497 class DBBinary(ORMObject):
498     def __init__(self, package = None, source = None, version = None, \
499         maintainer = None, architecture = None, poolfile = None, \
500         binarytype = 'deb'):
501         self.package = package
502         self.source = source
503         self.version = version
504         self.maintainer = maintainer
505         self.architecture = architecture
506         self.poolfile = poolfile
507         self.binarytype = binarytype
508
509     @property
510     def pkid(self):
511         return self.binary_id
512
513     def properties(self):
514         return ['package', 'version', 'maintainer', 'source', 'architecture', \
515             'poolfile', 'binarytype', 'fingerprint', 'install_date', \
516             'suites_count', 'binary_id', 'contents_count', 'extra_sources']
517
518     def not_null_constraints(self):
519         return ['package', 'version', 'maintainer', 'source',  'poolfile', \
520             'binarytype']
521
522     metadata = association_proxy('key', 'value')
523
524     def get_component_name(self):
525         return self.poolfile.location.component.component_name
526
527     def scan_contents(self):
528         '''
529         Yields the contents of the package. Only regular files are yielded and
530         the path names are normalized after converting them from either utf-8
531         or iso8859-1 encoding. It yields the string ' <EMPTY PACKAGE>' if the
532         package does not contain any regular file.
533         '''
534         fullpath = self.poolfile.fullpath
535         dpkg = Popen(['dpkg-deb', '--fsys-tarfile', fullpath], stdout = PIPE,
536             preexec_fn = subprocess_setup)
537         tar = TarFile.open(fileobj = dpkg.stdout, mode = 'r|')
538         for member in tar.getmembers():
539             if not member.isdir():
540                 name = normpath(member.name)
541                 # enforce proper utf-8 encoding
542                 try:
543                     name.decode('utf-8')
544                 except UnicodeDecodeError:
545                     name = name.decode('iso8859-1').encode('utf-8')
546                 yield name
547         tar.close()
548         dpkg.stdout.close()
549         dpkg.wait()
550
551     def read_control(self):
552         '''
553         Reads the control information from a binary.
554
555         @rtype: text
556         @return: stanza text of the control section.
557         '''
558         import apt_inst
559         fullpath = self.poolfile.fullpath
560         deb_file = open(fullpath, 'r')
561         stanza = apt_inst.debExtractControl(deb_file)
562         deb_file.close()
563
564         return stanza
565
566     def read_control_fields(self):
567         '''
568         Reads the control information from a binary and return
569         as a dictionary.
570
571         @rtype: dict
572         @return: fields of the control section as a dictionary.
573         '''
574         import apt_pkg
575         stanza = self.read_control()
576         return apt_pkg.TagSection(stanza)
577
578 __all__.append('DBBinary')
579
580 @session_wrapper
581 def get_suites_binary_in(package, session=None):
582     """
583     Returns list of Suite objects which given C{package} name is in
584
585     @type package: str
586     @param package: DBBinary package name to search for
587
588     @rtype: list
589     @return: list of Suite objects for the given package
590     """
591
592     return session.query(Suite).filter(Suite.binaries.any(DBBinary.package == package)).all()
593
594 __all__.append('get_suites_binary_in')
595
596 @session_wrapper
597 def get_component_by_package_suite(package, suite_list, arch_list=[], session=None):
598     '''
599     Returns the component name of the newest binary package in suite_list or
600     None if no package is found. The result can be optionally filtered by a list
601     of architecture names.
602
603     @type package: str
604     @param package: DBBinary package name to search for
605
606     @type suite_list: list of str
607     @param suite_list: list of suite_name items
608
609     @type arch_list: list of str
610     @param arch_list: optional list of arch_string items that defaults to []
611
612     @rtype: str or NoneType
613     @return: name of component or None
614     '''
615
616     q = session.query(DBBinary).filter_by(package = package). \
617         join(DBBinary.suites).filter(Suite.suite_name.in_(suite_list))
618     if len(arch_list) > 0:
619         q = q.join(DBBinary.architecture). \
620             filter(Architecture.arch_string.in_(arch_list))
621     binary = q.order_by(desc(DBBinary.version)).first()
622     if binary is None:
623         return None
624     else:
625         return binary.get_component_name()
626
627 __all__.append('get_component_by_package_suite')
628
629 ################################################################################
630
631 class BinaryACL(object):
632     def __init__(self, *args, **kwargs):
633         pass
634
635     def __repr__(self):
636         return '<BinaryACL %s>' % self.binary_acl_id
637
638 __all__.append('BinaryACL')
639
640 ################################################################################
641
642 class BinaryACLMap(object):
643     def __init__(self, *args, **kwargs):
644         pass
645
646     def __repr__(self):
647         return '<BinaryACLMap %s>' % self.binary_acl_map_id
648
649 __all__.append('BinaryACLMap')
650
651 ################################################################################
652
653 MINIMAL_APT_CONF="""
654 Dir
655 {
656    ArchiveDir "%(archivepath)s";
657    OverrideDir "%(overridedir)s";
658    CacheDir "%(cachedir)s";
659 };
660
661 Default
662 {
663    Packages::Compress ". bzip2 gzip";
664    Sources::Compress ". bzip2 gzip";
665    DeLinkLimit 0;
666    FileMode 0664;
667 }
668
669 bindirectory "incoming"
670 {
671    Packages "Packages";
672    Contents " ";
673
674    BinOverride "override.sid.all3";
675    BinCacheDB "packages-accepted.db";
676
677    FileList "%(filelist)s";
678
679    PathPrefix "";
680    Packages::Extensions ".deb .udeb";
681 };
682
683 bindirectory "incoming/"
684 {
685    Sources "Sources";
686    BinOverride "override.sid.all3";
687    SrcOverride "override.sid.all3.src";
688    FileList "%(filelist)s";
689 };
690 """
691
692 class BuildQueue(object):
693     def __init__(self, *args, **kwargs):
694         pass
695
696     def __repr__(self):
697         return '<BuildQueue %s>' % self.queue_name
698
699     def write_metadata(self, starttime, force=False):
700         # Do we write out metafiles?
701         if not (force or self.generate_metadata):
702             return
703
704         session = DBConn().session().object_session(self)
705
706         fl_fd = fl_name = ac_fd = ac_name = None
707         tempdir = None
708         arches = " ".join([ a.arch_string for a in session.query(Architecture).all() if a.arch_string != 'source' ])
709         startdir = os.getcwd()
710
711         try:
712             # Grab files we want to include
713             newer = session.query(BuildQueueFile).filter_by(build_queue_id = self.queue_id).filter(BuildQueueFile.lastused + timedelta(seconds=self.stay_of_execution) > starttime).all()
714             newer += session.query(BuildQueuePolicyFile).filter_by(build_queue_id = self.queue_id).filter(BuildQueuePolicyFile.lastused + timedelta(seconds=self.stay_of_execution) > starttime).all()
715             # Write file list with newer files
716             (fl_fd, fl_name) = mkstemp()
717             for n in newer:
718                 os.write(fl_fd, '%s\n' % n.fullpath)
719             os.close(fl_fd)
720
721             cnf = Config()
722
723             # Write minimal apt.conf
724             # TODO: Remove hardcoding from template
725             (ac_fd, ac_name) = mkstemp()
726             os.write(ac_fd, MINIMAL_APT_CONF % {'archivepath': self.path,
727                                                 'filelist': fl_name,
728                                                 'cachedir': cnf["Dir::Cache"],
729                                                 'overridedir': cnf["Dir::Override"],
730                                                 })
731             os.close(ac_fd)
732
733             # Run apt-ftparchive generate
734             os.chdir(os.path.dirname(ac_name))
735             os.system('apt-ftparchive -qq -o APT::FTPArchive::Contents=off generate %s' % os.path.basename(ac_name))
736
737             # Run apt-ftparchive release
738             # TODO: Eww - fix this
739             bname = os.path.basename(self.path)
740             os.chdir(self.path)
741             os.chdir('..')
742
743             # We have to remove the Release file otherwise it'll be included in the
744             # new one
745             try:
746                 os.unlink(os.path.join(bname, 'Release'))
747             except OSError:
748                 pass
749
750             os.system("""apt-ftparchive -qq -o APT::FTPArchive::Release::Origin="%s" -o APT::FTPArchive::Release::Label="%s" -o APT::FTPArchive::Release::Description="%s" -o APT::FTPArchive::Release::Architectures="%s" release %s > Release""" % (self.origin, self.label, self.releasedescription, arches, bname))
751
752             # Crude hack with open and append, but this whole section is and should be redone.
753             if self.notautomatic:
754                 release=open("Release", "a")
755                 release.write("NotAutomatic: yes\n")
756                 release.close()
757
758             # Sign if necessary
759             if self.signingkey:
760                 keyring = "--secret-keyring \"%s\"" % cnf["Dinstall::SigningKeyring"]
761                 if cnf.has_key("Dinstall::SigningPubKeyring"):
762                     keyring += " --keyring \"%s\"" % cnf["Dinstall::SigningPubKeyring"]
763
764                 os.system("gpg %s --no-options --batch --no-tty --armour --default-key %s --detach-sign -o Release.gpg Release""" % (keyring, self.signingkey))
765
766             # Move the files if we got this far
767             os.rename('Release', os.path.join(bname, 'Release'))
768             if self.signingkey:
769                 os.rename('Release.gpg', os.path.join(bname, 'Release.gpg'))
770
771         # Clean up any left behind files
772         finally:
773             os.chdir(startdir)
774             if fl_fd:
775                 try:
776                     os.close(fl_fd)
777                 except OSError:
778                     pass
779
780             if fl_name:
781                 try:
782                     os.unlink(fl_name)
783                 except OSError:
784                     pass
785
786             if ac_fd:
787                 try:
788                     os.close(ac_fd)
789                 except OSError:
790                     pass
791
792             if ac_name:
793                 try:
794                     os.unlink(ac_name)
795                 except OSError:
796                     pass
797
798     def clean_and_update(self, starttime, Logger, dryrun=False):
799         """WARNING: This routine commits for you"""
800         session = DBConn().session().object_session(self)
801
802         if self.generate_metadata and not dryrun:
803             self.write_metadata(starttime)
804
805         # Grab files older than our execution time
806         older = session.query(BuildQueueFile).filter_by(build_queue_id = self.queue_id).filter(BuildQueueFile.lastused + timedelta(seconds=self.stay_of_execution) <= starttime).all()
807         older += session.query(BuildQueuePolicyFile).filter_by(build_queue_id = self.queue_id).filter(BuildQueuePolicyFile.lastused + timedelta(seconds=self.stay_of_execution) <= starttime).all()
808
809         for o in older:
810             killdb = False
811             try:
812                 if dryrun:
813                     Logger.log(["I: Would have removed %s from the queue" % o.fullpath])
814                 else:
815                     Logger.log(["I: Removing %s from the queue" % o.fullpath])
816                     os.unlink(o.fullpath)
817                     killdb = True
818             except OSError, e:
819                 # If it wasn't there, don't worry
820                 if e.errno == ENOENT:
821                     killdb = True
822                 else:
823                     # TODO: Replace with proper logging call
824                     Logger.log(["E: Could not remove %s" % o.fullpath])
825
826             if killdb:
827                 session.delete(o)
828
829         session.commit()
830
831         for f in os.listdir(self.path):
832             if f.startswith('Packages') or f.startswith('Source') or f.startswith('Release') or f.startswith('advisory'):
833                 continue
834
835             if not self.contains_filename(f):
836                 fp = os.path.join(self.path, f)
837                 if dryrun:
838                     Logger.log(["I: Would remove unused link %s" % fp])
839                 else:
840                     Logger.log(["I: Removing unused link %s" % fp])
841                     try:
842                         os.unlink(fp)
843                     except OSError:
844                         Logger.log(["E: Failed to unlink unreferenced file %s" % r.fullpath])
845
846     def contains_filename(self, filename):
847         """
848         @rtype Boolean
849         @returns True if filename is supposed to be in the queue; False otherwise
850         """
851         session = DBConn().session().object_session(self)
852         if session.query(BuildQueueFile).filter_by(build_queue_id = self.queue_id, filename = filename).count() > 0:
853             return True
854         elif session.query(BuildQueuePolicyFile).filter_by(build_queue = self, filename = filename).count() > 0:
855             return True
856         return False
857
858     def add_file_from_pool(self, poolfile):
859         """Copies a file into the pool.  Assumes that the PoolFile object is
860         attached to the same SQLAlchemy session as the Queue object is.
861
862         The caller is responsible for committing after calling this function."""
863         poolfile_basename = poolfile.filename[poolfile.filename.rindex(os.sep)+1:]
864
865         # Check if we have a file of this name or this ID already
866         for f in self.queuefiles:
867             if (f.fileid is not None and f.fileid == poolfile.file_id) or \
868                (f.poolfile is not None and f.poolfile.filename == poolfile_basename):
869                    # In this case, update the BuildQueueFile entry so we
870                    # don't remove it too early
871                    f.lastused = datetime.now()
872                    DBConn().session().object_session(poolfile).add(f)
873                    return f
874
875         # Prepare BuildQueueFile object
876         qf = BuildQueueFile()
877         qf.build_queue_id = self.queue_id
878         qf.lastused = datetime.now()
879         qf.filename = poolfile_basename
880
881         targetpath = poolfile.fullpath
882         queuepath = os.path.join(self.path, poolfile_basename)
883
884         try:
885             if self.copy_files:
886                 # We need to copy instead of symlink
887                 import utils
888                 utils.copy(targetpath, queuepath)
889                 # NULL in the fileid field implies a copy
890                 qf.fileid = None
891             else:
892                 os.symlink(targetpath, queuepath)
893                 qf.fileid = poolfile.file_id
894         except FileExistsError:
895             if not poolfile.identical_to(queuepath):
896                 raise
897         except OSError:
898             return None
899
900         # Get the same session as the PoolFile is using and add the qf to it
901         DBConn().session().object_session(poolfile).add(qf)
902
903         return qf
904
905     def add_changes_from_policy_queue(self, policyqueue, changes):
906         """
907         Copies a changes from a policy queue together with its poolfiles.
908
909         @type policyqueue: PolicyQueue
910         @param policyqueue: policy queue to copy the changes from
911
912         @type changes: DBChange
913         @param changes: changes to copy to this build queue
914         """
915         for policyqueuefile in changes.files:
916             self.add_file_from_policy_queue(policyqueue, policyqueuefile)
917         for poolfile in changes.poolfiles:
918             self.add_file_from_pool(poolfile)
919
920     def add_file_from_policy_queue(self, policyqueue, policyqueuefile):
921         """
922         Copies a file from a policy queue.
923         Assumes that the policyqueuefile is attached to the same SQLAlchemy
924         session as the Queue object is.  The caller is responsible for
925         committing after calling this function.
926
927         @type policyqueue: PolicyQueue
928         @param policyqueue: policy queue to copy the file from
929
930         @type policyqueuefile: ChangePendingFile
931         @param policyqueuefile: file to be added to the build queue
932         """
933         session = DBConn().session().object_session(policyqueuefile)
934
935         # Is the file already there?
936         try:
937             f = session.query(BuildQueuePolicyFile).filter_by(build_queue=self, file=policyqueuefile).one()
938             f.lastused = datetime.now()
939             return f
940         except NoResultFound:
941             pass # continue below
942
943         # We have to add the file.
944         f = BuildQueuePolicyFile()
945         f.build_queue = self
946         f.file = policyqueuefile
947         f.filename = policyqueuefile.filename
948
949         source = os.path.join(policyqueue.path, policyqueuefile.filename)
950         target = f.fullpath
951         try:
952             # Always copy files from policy queues as they might move around.
953             import utils
954             utils.copy(source, target)
955         except FileExistsError:
956             if not policyqueuefile.identical_to(target):
957                 raise
958         except OSError:
959             return None
960
961         session.add(f)
962         return f
963
964 __all__.append('BuildQueue')
965
966 @session_wrapper
967 def get_build_queue(queuename, session=None):
968     """
969     Returns BuildQueue object for given C{queue name}, creating it if it does not
970     exist.
971
972     @type queuename: string
973     @param queuename: The name of the queue
974
975     @type session: Session
976     @param session: Optional SQLA session object (a temporary one will be
977     generated if not supplied)
978
979     @rtype: BuildQueue
980     @return: BuildQueue object for the given queue
981     """
982
983     q = session.query(BuildQueue).filter_by(queue_name=queuename)
984
985     try:
986         return q.one()
987     except NoResultFound:
988         return None
989
990 __all__.append('get_build_queue')
991
992 ################################################################################
993
994 class BuildQueueFile(object):
995     """
996     BuildQueueFile represents a file in a build queue coming from a pool.
997     """
998
999     def __init__(self, *args, **kwargs):
1000         pass
1001
1002     def __repr__(self):
1003         return '<BuildQueueFile %s (%s)>' % (self.filename, self.build_queue_id)
1004
1005     @property
1006     def fullpath(self):
1007         return os.path.join(self.buildqueue.path, self.filename)
1008
1009
1010 __all__.append('BuildQueueFile')
1011
1012 ################################################################################
1013
1014 class BuildQueuePolicyFile(object):
1015     """
1016     BuildQueuePolicyFile represents a file in a build queue that comes from a
1017     policy queue (and not a pool).
1018     """
1019
1020     def __init__(self, *args, **kwargs):
1021         pass
1022
1023     #@property
1024     #def filename(self):
1025     #    return self.file.filename
1026
1027     @property
1028     def fullpath(self):
1029         return os.path.join(self.build_queue.path, self.filename)
1030
1031 __all__.append('BuildQueuePolicyFile')
1032
1033 ################################################################################
1034
1035 class ChangePendingBinary(object):
1036     def __init__(self, *args, **kwargs):
1037         pass
1038
1039     def __repr__(self):
1040         return '<ChangePendingBinary %s>' % self.change_pending_binary_id
1041
1042 __all__.append('ChangePendingBinary')
1043
1044 ################################################################################
1045
1046 class ChangePendingFile(object):
1047     def __init__(self, *args, **kwargs):
1048         pass
1049
1050     def __repr__(self):
1051         return '<ChangePendingFile %s>' % self.change_pending_file_id
1052
1053     def identical_to(self, filename):
1054         """
1055         compare size and hash with the given file
1056
1057         @rtype: bool
1058         @return: true if the given file has the same size and hash as this object; false otherwise
1059         """
1060         st = os.stat(filename)
1061         if self.size != st.st_size:
1062             return False
1063
1064         f = open(filename, "r")
1065         sha256sum = apt_pkg.sha256sum(f)
1066         if sha256sum != self.sha256sum:
1067             return False
1068
1069         return True
1070
1071 __all__.append('ChangePendingFile')
1072
1073 ################################################################################
1074
1075 class ChangePendingSource(object):
1076     def __init__(self, *args, **kwargs):
1077         pass
1078
1079     def __repr__(self):
1080         return '<ChangePendingSource %s>' % self.change_pending_source_id
1081
1082 __all__.append('ChangePendingSource')
1083
1084 ################################################################################
1085
1086 class Component(ORMObject):
1087     def __init__(self, component_name = None):
1088         self.component_name = component_name
1089
1090     def __eq__(self, val):
1091         if isinstance(val, str):
1092             return (self.component_name == val)
1093         # This signals to use the normal comparison operator
1094         return NotImplemented
1095
1096     def __ne__(self, val):
1097         if isinstance(val, str):
1098             return (self.component_name != val)
1099         # This signals to use the normal comparison operator
1100         return NotImplemented
1101
1102     def properties(self):
1103         return ['component_name', 'component_id', 'description', \
1104             'location_count', 'meets_dfsg', 'overrides_count']
1105
1106     def not_null_constraints(self):
1107         return ['component_name']
1108
1109
1110 __all__.append('Component')
1111
1112 @session_wrapper
1113 def get_component(component, session=None):
1114     """
1115     Returns database id for given C{component}.
1116
1117     @type component: string
1118     @param component: The name of the override type
1119
1120     @rtype: int
1121     @return: the database id for the given component
1122
1123     """
1124     component = component.lower()
1125
1126     q = session.query(Component).filter_by(component_name=component)
1127
1128     try:
1129         return q.one()
1130     except NoResultFound:
1131         return None
1132
1133 __all__.append('get_component')
1134
1135 @session_wrapper
1136 def get_component_names(session=None):
1137     """
1138     Returns list of strings of component names.
1139
1140     @rtype: list
1141     @return: list of strings of component names
1142     """
1143
1144     return [ x.component_name for x in session.query(Component).all() ]
1145
1146 __all__.append('get_component_names')
1147
1148 ################################################################################
1149
1150 class DBConfig(object):
1151     def __init__(self, *args, **kwargs):
1152         pass
1153
1154     def __repr__(self):
1155         return '<DBConfig %s>' % self.name
1156
1157 __all__.append('DBConfig')
1158
1159 ################################################################################
1160
1161 @session_wrapper
1162 def get_or_set_contents_file_id(filename, session=None):
1163     """
1164     Returns database id for given filename.
1165
1166     If no matching file is found, a row is inserted.
1167
1168     @type filename: string
1169     @param filename: The filename
1170     @type session: SQLAlchemy
1171     @param session: Optional SQL session object (a temporary one will be
1172     generated if not supplied).  If not passed, a commit will be performed at
1173     the end of the function, otherwise the caller is responsible for commiting.
1174
1175     @rtype: int
1176     @return: the database id for the given component
1177     """
1178
1179     q = session.query(ContentFilename).filter_by(filename=filename)
1180
1181     try:
1182         ret = q.one().cafilename_id
1183     except NoResultFound:
1184         cf = ContentFilename()
1185         cf.filename = filename
1186         session.add(cf)
1187         session.commit_or_flush()
1188         ret = cf.cafilename_id
1189
1190     return ret
1191
1192 __all__.append('get_or_set_contents_file_id')
1193
1194 @session_wrapper
1195 def get_contents(suite, overridetype, section=None, session=None):
1196     """
1197     Returns contents for a suite / overridetype combination, limiting
1198     to a section if not None.
1199
1200     @type suite: Suite
1201     @param suite: Suite object
1202
1203     @type overridetype: OverrideType
1204     @param overridetype: OverrideType object
1205
1206     @type section: Section
1207     @param section: Optional section object to limit results to
1208
1209     @type session: SQLAlchemy
1210     @param session: Optional SQL session object (a temporary one will be
1211     generated if not supplied)
1212
1213     @rtype: ResultsProxy
1214     @return: ResultsProxy object set up to return tuples of (filename, section,
1215     package, arch_id)
1216     """
1217
1218     # find me all of the contents for a given suite
1219     contents_q = """SELECT (p.path||'/'||n.file) AS fn,
1220                             s.section,
1221                             b.package,
1222                             b.architecture
1223                    FROM content_associations c join content_file_paths p ON (c.filepath=p.id)
1224                    JOIN content_file_names n ON (c.filename=n.id)
1225                    JOIN binaries b ON (b.id=c.binary_pkg)
1226                    JOIN override o ON (o.package=b.package)
1227                    JOIN section s ON (s.id=o.section)
1228                    WHERE o.suite = :suiteid AND o.type = :overridetypeid
1229                    AND b.type=:overridetypename"""
1230
1231     vals = {'suiteid': suite.suite_id,
1232             'overridetypeid': overridetype.overridetype_id,
1233             'overridetypename': overridetype.overridetype}
1234
1235     if section is not None:
1236         contents_q += " AND s.id = :sectionid"
1237         vals['sectionid'] = section.section_id
1238
1239     contents_q += " ORDER BY fn"
1240
1241     return session.execute(contents_q, vals)
1242
1243 __all__.append('get_contents')
1244
1245 ################################################################################
1246
1247 class ContentFilepath(object):
1248     def __init__(self, *args, **kwargs):
1249         pass
1250
1251     def __repr__(self):
1252         return '<ContentFilepath %s>' % self.filepath
1253
1254 __all__.append('ContentFilepath')
1255
1256 @session_wrapper
1257 def get_or_set_contents_path_id(filepath, session=None):
1258     """
1259     Returns database id for given path.
1260
1261     If no matching file is found, a row is inserted.
1262
1263     @type filepath: string
1264     @param filepath: The filepath
1265
1266     @type session: SQLAlchemy
1267     @param session: Optional SQL session object (a temporary one will be
1268     generated if not supplied).  If not passed, a commit will be performed at
1269     the end of the function, otherwise the caller is responsible for commiting.
1270
1271     @rtype: int
1272     @return: the database id for the given path
1273     """
1274
1275     q = session.query(ContentFilepath).filter_by(filepath=filepath)
1276
1277     try:
1278         ret = q.one().cafilepath_id
1279     except NoResultFound:
1280         cf = ContentFilepath()
1281         cf.filepath = filepath
1282         session.add(cf)
1283         session.commit_or_flush()
1284         ret = cf.cafilepath_id
1285
1286     return ret
1287
1288 __all__.append('get_or_set_contents_path_id')
1289
1290 ################################################################################
1291
1292 class ContentAssociation(object):
1293     def __init__(self, *args, **kwargs):
1294         pass
1295
1296     def __repr__(self):
1297         return '<ContentAssociation %s>' % self.ca_id
1298
1299 __all__.append('ContentAssociation')
1300
1301 def insert_content_paths(binary_id, fullpaths, session=None):
1302     """
1303     Make sure given path is associated with given binary id
1304
1305     @type binary_id: int
1306     @param binary_id: the id of the binary
1307     @type fullpaths: list
1308     @param fullpaths: the list of paths of the file being associated with the binary
1309     @type session: SQLAlchemy session
1310     @param session: Optional SQLAlchemy session.  If this is passed, the caller
1311     is responsible for ensuring a transaction has begun and committing the
1312     results or rolling back based on the result code.  If not passed, a commit
1313     will be performed at the end of the function, otherwise the caller is
1314     responsible for commiting.
1315
1316     @return: True upon success
1317     """
1318
1319     privatetrans = False
1320     if session is None:
1321         session = DBConn().session()
1322         privatetrans = True
1323
1324     try:
1325         # Insert paths
1326         def generate_path_dicts():
1327             for fullpath in fullpaths:
1328                 if fullpath.startswith( './' ):
1329                     fullpath = fullpath[2:]
1330
1331                 yield {'filename':fullpath, 'id': binary_id }
1332
1333         for d in generate_path_dicts():
1334             session.execute( "INSERT INTO bin_contents ( file, binary_id ) VALUES ( :filename, :id )",
1335                          d )
1336
1337         session.commit()
1338         if privatetrans:
1339             session.close()
1340         return True
1341
1342     except:
1343         traceback.print_exc()
1344
1345         # Only rollback if we set up the session ourself
1346         if privatetrans:
1347             session.rollback()
1348             session.close()
1349
1350         return False
1351
1352 __all__.append('insert_content_paths')
1353
1354 ################################################################################
1355
1356 class DSCFile(object):
1357     def __init__(self, *args, **kwargs):
1358         pass
1359
1360     def __repr__(self):
1361         return '<DSCFile %s>' % self.dscfile_id
1362
1363 __all__.append('DSCFile')
1364
1365 @session_wrapper
1366 def get_dscfiles(dscfile_id=None, source_id=None, poolfile_id=None, session=None):
1367     """
1368     Returns a list of DSCFiles which may be empty
1369
1370     @type dscfile_id: int (optional)
1371     @param dscfile_id: the dscfile_id of the DSCFiles to find
1372
1373     @type source_id: int (optional)
1374     @param source_id: the source id related to the DSCFiles to find
1375
1376     @type poolfile_id: int (optional)
1377     @param poolfile_id: the poolfile id related to the DSCFiles to find
1378
1379     @rtype: list
1380     @return: Possibly empty list of DSCFiles
1381     """
1382
1383     q = session.query(DSCFile)
1384
1385     if dscfile_id is not None:
1386         q = q.filter_by(dscfile_id=dscfile_id)
1387
1388     if source_id is not None:
1389         q = q.filter_by(source_id=source_id)
1390
1391     if poolfile_id is not None:
1392         q = q.filter_by(poolfile_id=poolfile_id)
1393
1394     return q.all()
1395
1396 __all__.append('get_dscfiles')
1397
1398 ################################################################################
1399
1400 class ExternalOverride(ORMObject):
1401     def __init__(self, *args, **kwargs):
1402         pass
1403
1404     def __repr__(self):
1405         return '<ExternalOverride %s = %s: %s>' % (self.package, self.key, self.value)
1406
1407 __all__.append('ExternalOverride')
1408
1409 ################################################################################
1410
1411 class PoolFile(ORMObject):
1412     def __init__(self, filename = None, location = None, filesize = -1, \
1413         md5sum = None):
1414         self.filename = filename
1415         self.location = location
1416         self.filesize = filesize
1417         self.md5sum = md5sum
1418
1419     @property
1420     def fullpath(self):
1421         return os.path.join(self.location.path, self.filename)
1422
1423     def is_valid(self, filesize = -1, md5sum = None):
1424         return self.filesize == long(filesize) and self.md5sum == md5sum
1425
1426     def properties(self):
1427         return ['filename', 'file_id', 'filesize', 'md5sum', 'sha1sum', \
1428             'sha256sum', 'location', 'source', 'binary', 'last_used']
1429
1430     def not_null_constraints(self):
1431         return ['filename', 'md5sum', 'location']
1432
1433     def identical_to(self, filename):
1434         """
1435         compare size and hash with the given file
1436
1437         @rtype: bool
1438         @return: true if the given file has the same size and hash as this object; false otherwise
1439         """
1440         st = os.stat(filename)
1441         if self.filesize != st.st_size:
1442             return False
1443
1444         f = open(filename, "r")
1445         sha256sum = apt_pkg.sha256sum(f)
1446         if sha256sum != self.sha256sum:
1447             return False
1448
1449         return True
1450
1451 __all__.append('PoolFile')
1452
1453 @session_wrapper
1454 def check_poolfile(filename, filesize, md5sum, location_id, session=None):
1455     """
1456     Returns a tuple:
1457     (ValidFileFound [boolean], PoolFile object or None)
1458
1459     @type filename: string
1460     @param filename: the filename of the file to check against the DB
1461
1462     @type filesize: int
1463     @param filesize: the size of the file to check against the DB
1464
1465     @type md5sum: string
1466     @param md5sum: the md5sum of the file to check against the DB
1467
1468     @type location_id: int
1469     @param location_id: the id of the location to look in
1470
1471     @rtype: tuple
1472     @return: Tuple of length 2.
1473                  - If valid pool file found: (C{True}, C{PoolFile object})
1474                  - If valid pool file not found:
1475                      - (C{False}, C{None}) if no file found
1476                      - (C{False}, C{PoolFile object}) if file found with size/md5sum mismatch
1477     """
1478
1479     poolfile = session.query(Location).get(location_id). \
1480         files.filter_by(filename=filename).first()
1481     valid = False
1482     if poolfile and poolfile.is_valid(filesize = filesize, md5sum = md5sum):
1483         valid = True
1484
1485     return (valid, poolfile)
1486
1487 __all__.append('check_poolfile')
1488
1489 # TODO: the implementation can trivially be inlined at the place where the
1490 # function is called
1491 @session_wrapper
1492 def get_poolfile_by_id(file_id, session=None):
1493     """
1494     Returns a PoolFile objects or None for the given id
1495
1496     @type file_id: int
1497     @param file_id: the id of the file to look for
1498
1499     @rtype: PoolFile or None
1500     @return: either the PoolFile object or None
1501     """
1502
1503     return session.query(PoolFile).get(file_id)
1504
1505 __all__.append('get_poolfile_by_id')
1506
1507 @session_wrapper
1508 def get_poolfile_like_name(filename, session=None):
1509     """
1510     Returns an array of PoolFile objects which are like the given name
1511
1512     @type filename: string
1513     @param filename: the filename of the file to check against the DB
1514
1515     @rtype: array
1516     @return: array of PoolFile objects
1517     """
1518
1519     # TODO: There must be a way of properly using bind parameters with %FOO%
1520     q = session.query(PoolFile).filter(PoolFile.filename.like('%%/%s' % filename))
1521
1522     return q.all()
1523
1524 __all__.append('get_poolfile_like_name')
1525
1526 @session_wrapper
1527 def add_poolfile(filename, datadict, location_id, session=None):
1528     """
1529     Add a new file to the pool
1530
1531     @type filename: string
1532     @param filename: filename
1533
1534     @type datadict: dict
1535     @param datadict: dict with needed data
1536
1537     @type location_id: int
1538     @param location_id: database id of the location
1539
1540     @rtype: PoolFile
1541     @return: the PoolFile object created
1542     """
1543     poolfile = PoolFile()
1544     poolfile.filename = filename
1545     poolfile.filesize = datadict["size"]
1546     poolfile.md5sum = datadict["md5sum"]
1547     poolfile.sha1sum = datadict["sha1sum"]
1548     poolfile.sha256sum = datadict["sha256sum"]
1549     poolfile.location_id = location_id
1550
1551     session.add(poolfile)
1552     # Flush to get a file id (NB: This is not a commit)
1553     session.flush()
1554
1555     return poolfile
1556
1557 __all__.append('add_poolfile')
1558
1559 ################################################################################
1560
1561 class Fingerprint(ORMObject):
1562     def __init__(self, fingerprint = None):
1563         self.fingerprint = fingerprint
1564
1565     def properties(self):
1566         return ['fingerprint', 'fingerprint_id', 'keyring', 'uid', \
1567             'binary_reject']
1568
1569     def not_null_constraints(self):
1570         return ['fingerprint']
1571
1572 __all__.append('Fingerprint')
1573
1574 @session_wrapper
1575 def get_fingerprint(fpr, session=None):
1576     """
1577     Returns Fingerprint object for given fpr.
1578
1579     @type fpr: string
1580     @param fpr: The fpr to find / add
1581
1582     @type session: SQLAlchemy
1583     @param session: Optional SQL session object (a temporary one will be
1584     generated if not supplied).
1585
1586     @rtype: Fingerprint
1587     @return: the Fingerprint object for the given fpr or None
1588     """
1589
1590     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1591
1592     try:
1593         ret = q.one()
1594     except NoResultFound:
1595         ret = None
1596
1597     return ret
1598
1599 __all__.append('get_fingerprint')
1600
1601 @session_wrapper
1602 def get_or_set_fingerprint(fpr, session=None):
1603     """
1604     Returns Fingerprint object for given fpr.
1605
1606     If no matching fpr is found, a row is inserted.
1607
1608     @type fpr: string
1609     @param fpr: The fpr to find / add
1610
1611     @type session: SQLAlchemy
1612     @param session: Optional SQL session object (a temporary one will be
1613     generated if not supplied).  If not passed, a commit will be performed at
1614     the end of the function, otherwise the caller is responsible for commiting.
1615     A flush will be performed either way.
1616
1617     @rtype: Fingerprint
1618     @return: the Fingerprint object for the given fpr
1619     """
1620
1621     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1622
1623     try:
1624         ret = q.one()
1625     except NoResultFound:
1626         fingerprint = Fingerprint()
1627         fingerprint.fingerprint = fpr
1628         session.add(fingerprint)
1629         session.commit_or_flush()
1630         ret = fingerprint
1631
1632     return ret
1633
1634 __all__.append('get_or_set_fingerprint')
1635
1636 ################################################################################
1637
1638 # Helper routine for Keyring class
1639 def get_ldap_name(entry):
1640     name = []
1641     for k in ["cn", "mn", "sn"]:
1642         ret = entry.get(k)
1643         if ret and ret[0] != "" and ret[0] != "-":
1644             name.append(ret[0])
1645     return " ".join(name)
1646
1647 ################################################################################
1648
1649 class Keyring(object):
1650     gpg_invocation = "gpg --no-default-keyring --keyring %s" +\
1651                      " --with-colons --fingerprint --fingerprint"
1652
1653     keys = {}
1654     fpr_lookup = {}
1655
1656     def __init__(self, *args, **kwargs):
1657         pass
1658
1659     def __repr__(self):
1660         return '<Keyring %s>' % self.keyring_name
1661
1662     def de_escape_gpg_str(self, txt):
1663         esclist = re.split(r'(\\x..)', txt)
1664         for x in range(1,len(esclist),2):
1665             esclist[x] = "%c" % (int(esclist[x][2:],16))
1666         return "".join(esclist)
1667
1668     def parse_address(self, uid):
1669         """parses uid and returns a tuple of real name and email address"""
1670         import email.Utils
1671         (name, address) = email.Utils.parseaddr(uid)
1672         name = re.sub(r"\s*[(].*[)]", "", name)
1673         name = self.de_escape_gpg_str(name)
1674         if name == "":
1675             name = uid
1676         return (name, address)
1677
1678     def load_keys(self, keyring):
1679         if not self.keyring_id:
1680             raise Exception('Must be initialized with database information')
1681
1682         k = os.popen(self.gpg_invocation % keyring, "r")
1683         key = None
1684         signingkey = False
1685
1686         for line in k.xreadlines():
1687             field = line.split(":")
1688             if field[0] == "pub":
1689                 key = field[4]
1690                 self.keys[key] = {}
1691                 (name, addr) = self.parse_address(field[9])
1692                 if "@" in addr:
1693                     self.keys[key]["email"] = addr
1694                     self.keys[key]["name"] = name
1695                 self.keys[key]["fingerprints"] = []
1696                 signingkey = True
1697             elif key and field[0] == "sub" and len(field) >= 12:
1698                 signingkey = ("s" in field[11])
1699             elif key and field[0] == "uid":
1700                 (name, addr) = self.parse_address(field[9])
1701                 if "email" not in self.keys[key] and "@" in addr:
1702                     self.keys[key]["email"] = addr
1703                     self.keys[key]["name"] = name
1704             elif signingkey and field[0] == "fpr":
1705                 self.keys[key]["fingerprints"].append(field[9])
1706                 self.fpr_lookup[field[9]] = key
1707
1708     def import_users_from_ldap(self, session):
1709         import ldap
1710         cnf = Config()
1711
1712         LDAPDn = cnf["Import-LDAP-Fingerprints::LDAPDn"]
1713         LDAPServer = cnf["Import-LDAP-Fingerprints::LDAPServer"]
1714
1715         l = ldap.open(LDAPServer)
1716         l.simple_bind_s("","")
1717         Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
1718                "(&(keyfingerprint=*)(gidnumber=%s))" % (cnf["Import-Users-From-Passwd::ValidGID"]),
1719                ["uid", "keyfingerprint", "cn", "mn", "sn"])
1720
1721         ldap_fin_uid_id = {}
1722
1723         byuid = {}
1724         byname = {}
1725
1726         for i in Attrs:
1727             entry = i[1]
1728             uid = entry["uid"][0]
1729             name = get_ldap_name(entry)
1730             fingerprints = entry["keyFingerPrint"]
1731             keyid = None
1732             for f in fingerprints:
1733                 key = self.fpr_lookup.get(f, None)
1734                 if key not in self.keys:
1735                     continue
1736                 self.keys[key]["uid"] = uid
1737
1738                 if keyid != None:
1739                     continue
1740                 keyid = get_or_set_uid(uid, session).uid_id
1741                 byuid[keyid] = (uid, name)
1742                 byname[uid] = (keyid, name)
1743
1744         return (byname, byuid)
1745
1746     def generate_users_from_keyring(self, format, session):
1747         byuid = {}
1748         byname = {}
1749         any_invalid = False
1750         for x in self.keys.keys():
1751             if "email" not in self.keys[x]:
1752                 any_invalid = True
1753                 self.keys[x]["uid"] = format % "invalid-uid"
1754             else:
1755                 uid = format % self.keys[x]["email"]
1756                 keyid = get_or_set_uid(uid, session).uid_id
1757                 byuid[keyid] = (uid, self.keys[x]["name"])
1758                 byname[uid] = (keyid, self.keys[x]["name"])
1759                 self.keys[x]["uid"] = uid
1760
1761         if any_invalid:
1762             uid = format % "invalid-uid"
1763             keyid = get_or_set_uid(uid, session).uid_id
1764             byuid[keyid] = (uid, "ungeneratable user id")
1765             byname[uid] = (keyid, "ungeneratable user id")
1766
1767         return (byname, byuid)
1768
1769 __all__.append('Keyring')
1770
1771 @session_wrapper
1772 def get_keyring(keyring, session=None):
1773     """
1774     If C{keyring} does not have an entry in the C{keyrings} table yet, return None
1775     If C{keyring} already has an entry, simply return the existing Keyring
1776
1777     @type keyring: string
1778     @param keyring: the keyring name
1779
1780     @rtype: Keyring
1781     @return: the Keyring object for this keyring
1782     """
1783
1784     q = session.query(Keyring).filter_by(keyring_name=keyring)
1785
1786     try:
1787         return q.one()
1788     except NoResultFound:
1789         return None
1790
1791 __all__.append('get_keyring')
1792
1793 @session_wrapper
1794 def get_active_keyring_paths(session=None):
1795     """
1796     @rtype: list
1797     @return: list of active keyring paths
1798     """
1799     return [ x.keyring_name for x in session.query(Keyring).filter(Keyring.active == True).order_by(desc(Keyring.priority)).all() ]
1800
1801 __all__.append('get_active_keyring_paths')
1802
1803 @session_wrapper
1804 def get_primary_keyring_path(session=None):
1805     """
1806     Get the full path to the highest priority active keyring
1807
1808     @rtype: str or None
1809     @return: path to the active keyring with the highest priority or None if no
1810              keyring is configured
1811     """
1812     keyrings = get_active_keyring_paths()
1813
1814     if len(keyrings) > 0:
1815         return keyrings[0]
1816     else:
1817         return None
1818
1819 __all__.append('get_primary_keyring_path')
1820
1821 ################################################################################
1822
1823 class KeyringACLMap(object):
1824     def __init__(self, *args, **kwargs):
1825         pass
1826
1827     def __repr__(self):
1828         return '<KeyringACLMap %s>' % self.keyring_acl_map_id
1829
1830 __all__.append('KeyringACLMap')
1831
1832 ################################################################################
1833
1834 class DBChange(object):
1835     def __init__(self, *args, **kwargs):
1836         pass
1837
1838     def __repr__(self):
1839         return '<DBChange %s>' % self.changesname
1840
1841     def clean_from_queue(self):
1842         session = DBConn().session().object_session(self)
1843
1844         # Remove changes_pool_files entries
1845         self.poolfiles = []
1846
1847         # Remove changes_pending_files references
1848         self.files = []
1849
1850         # Clear out of queue
1851         self.in_queue = None
1852         self.approved_for_id = None
1853
1854 __all__.append('DBChange')
1855
1856 @session_wrapper
1857 def get_dbchange(filename, session=None):
1858     """
1859     returns DBChange object for given C{filename}.
1860
1861     @type filename: string
1862     @param filename: the name of the file
1863
1864     @type session: Session
1865     @param session: Optional SQLA session object (a temporary one will be
1866     generated if not supplied)
1867
1868     @rtype: DBChange
1869     @return:  DBChange object for the given filename (C{None} if not present)
1870
1871     """
1872     q = session.query(DBChange).filter_by(changesname=filename)
1873
1874     try:
1875         return q.one()
1876     except NoResultFound:
1877         return None
1878
1879 __all__.append('get_dbchange')
1880
1881 ################################################################################
1882
1883 class Location(ORMObject):
1884     def __init__(self, path = None, component = None):
1885         self.path = path
1886         self.component = component
1887         # the column 'type' should go away, see comment at mapper
1888         self.archive_type = 'pool'
1889
1890     def properties(self):
1891         return ['path', 'location_id', 'archive_type', 'component', \
1892             'files_count']
1893
1894     def not_null_constraints(self):
1895         return ['path', 'archive_type']
1896
1897 __all__.append('Location')
1898
1899 @session_wrapper
1900 def get_location(location, component=None, archive=None, session=None):
1901     """
1902     Returns Location object for the given combination of location, component
1903     and archive
1904
1905     @type location: string
1906     @param location: the path of the location, e.g. I{/srv/ftp-master.debian.org/ftp/pool/}
1907
1908     @type component: string
1909     @param component: the component name (if None, no restriction applied)
1910
1911     @type archive: string
1912     @param archive: the archive name (if None, no restriction applied)
1913
1914     @rtype: Location / None
1915     @return: Either a Location object or None if one can't be found
1916     """
1917
1918     q = session.query(Location).filter_by(path=location)
1919
1920     if archive is not None:
1921         q = q.join(Archive).filter_by(archive_name=archive)
1922
1923     if component is not None:
1924         q = q.join(Component).filter_by(component_name=component)
1925
1926     try:
1927         return q.one()
1928     except NoResultFound:
1929         return None
1930
1931 __all__.append('get_location')
1932
1933 ################################################################################
1934
1935 class Maintainer(ORMObject):
1936     def __init__(self, name = None):
1937         self.name = name
1938
1939     def properties(self):
1940         return ['name', 'maintainer_id']
1941
1942     def not_null_constraints(self):
1943         return ['name']
1944
1945     def get_split_maintainer(self):
1946         if not hasattr(self, 'name') or self.name is None:
1947             return ('', '', '', '')
1948
1949         return fix_maintainer(self.name.strip())
1950
1951 __all__.append('Maintainer')
1952
1953 @session_wrapper
1954 def get_or_set_maintainer(name, session=None):
1955     """
1956     Returns Maintainer object for given maintainer name.
1957
1958     If no matching maintainer name is found, a row is inserted.
1959
1960     @type name: string
1961     @param name: The maintainer name to add
1962
1963     @type session: SQLAlchemy
1964     @param session: Optional SQL session object (a temporary one will be
1965     generated if not supplied).  If not passed, a commit will be performed at
1966     the end of the function, otherwise the caller is responsible for commiting.
1967     A flush will be performed either way.
1968
1969     @rtype: Maintainer
1970     @return: the Maintainer object for the given maintainer
1971     """
1972
1973     q = session.query(Maintainer).filter_by(name=name)
1974     try:
1975         ret = q.one()
1976     except NoResultFound:
1977         maintainer = Maintainer()
1978         maintainer.name = name
1979         session.add(maintainer)
1980         session.commit_or_flush()
1981         ret = maintainer
1982
1983     return ret
1984
1985 __all__.append('get_or_set_maintainer')
1986
1987 @session_wrapper
1988 def get_maintainer(maintainer_id, session=None):
1989     """
1990     Return the name of the maintainer behind C{maintainer_id} or None if that
1991     maintainer_id is invalid.
1992
1993     @type maintainer_id: int
1994     @param maintainer_id: the id of the maintainer
1995
1996     @rtype: Maintainer
1997     @return: the Maintainer with this C{maintainer_id}
1998     """
1999
2000     return session.query(Maintainer).get(maintainer_id)
2001
2002 __all__.append('get_maintainer')
2003
2004 ################################################################################
2005
2006 class NewComment(object):
2007     def __init__(self, *args, **kwargs):
2008         pass
2009
2010     def __repr__(self):
2011         return '''<NewComment for '%s %s' (%s)>''' % (self.package, self.version, self.comment_id)
2012
2013 __all__.append('NewComment')
2014
2015 @session_wrapper
2016 def has_new_comment(package, version, session=None):
2017     """
2018     Returns true if the given combination of C{package}, C{version} has a comment.
2019
2020     @type package: string
2021     @param package: name of the package
2022
2023     @type version: string
2024     @param version: package version
2025
2026     @type session: Session
2027     @param session: Optional SQLA session object (a temporary one will be
2028     generated if not supplied)
2029
2030     @rtype: boolean
2031     @return: true/false
2032     """
2033
2034     q = session.query(NewComment)
2035     q = q.filter_by(package=package)
2036     q = q.filter_by(version=version)
2037
2038     return bool(q.count() > 0)
2039
2040 __all__.append('has_new_comment')
2041
2042 @session_wrapper
2043 def get_new_comments(package=None, version=None, comment_id=None, session=None):
2044     """
2045     Returns (possibly empty) list of NewComment objects for the given
2046     parameters
2047
2048     @type package: string (optional)
2049     @param package: name of the package
2050
2051     @type version: string (optional)
2052     @param version: package version
2053
2054     @type comment_id: int (optional)
2055     @param comment_id: An id of a comment
2056
2057     @type session: Session
2058     @param session: Optional SQLA session object (a temporary one will be
2059     generated if not supplied)
2060
2061     @rtype: list
2062     @return: A (possibly empty) list of NewComment objects will be returned
2063     """
2064
2065     q = session.query(NewComment)
2066     if package is not None: q = q.filter_by(package=package)
2067     if version is not None: q = q.filter_by(version=version)
2068     if comment_id is not None: q = q.filter_by(comment_id=comment_id)
2069
2070     return q.all()
2071
2072 __all__.append('get_new_comments')
2073
2074 ################################################################################
2075
2076 class Override(ORMObject):
2077     def __init__(self, package = None, suite = None, component = None, overridetype = None, \
2078         section = None, priority = None):
2079         self.package = package
2080         self.suite = suite
2081         self.component = component
2082         self.overridetype = overridetype
2083         self.section = section
2084         self.priority = priority
2085
2086     def properties(self):
2087         return ['package', 'suite', 'component', 'overridetype', 'section', \
2088             'priority']
2089
2090     def not_null_constraints(self):
2091         return ['package', 'suite', 'component', 'overridetype', 'section']
2092
2093 __all__.append('Override')
2094
2095 @session_wrapper
2096 def get_override(package, suite=None, component=None, overridetype=None, session=None):
2097     """
2098     Returns Override object for the given parameters
2099
2100     @type package: string
2101     @param package: The name of the package
2102
2103     @type suite: string, list or None
2104     @param suite: The name of the suite (or suites if a list) to limit to.  If
2105                   None, don't limit.  Defaults to None.
2106
2107     @type component: string, list or None
2108     @param component: The name of the component (or components if a list) to
2109                       limit to.  If None, don't limit.  Defaults to None.
2110
2111     @type overridetype: string, list or None
2112     @param overridetype: The name of the overridetype (or overridetypes if a list) to
2113                          limit to.  If None, don't limit.  Defaults to None.
2114
2115     @type session: Session
2116     @param session: Optional SQLA session object (a temporary one will be
2117     generated if not supplied)
2118
2119     @rtype: list
2120     @return: A (possibly empty) list of Override objects will be returned
2121     """
2122
2123     q = session.query(Override)
2124     q = q.filter_by(package=package)
2125
2126     if suite is not None:
2127         if not isinstance(suite, list): suite = [suite]
2128         q = q.join(Suite).filter(Suite.suite_name.in_(suite))
2129
2130     if component is not None:
2131         if not isinstance(component, list): component = [component]
2132         q = q.join(Component).filter(Component.component_name.in_(component))
2133
2134     if overridetype is not None:
2135         if not isinstance(overridetype, list): overridetype = [overridetype]
2136         q = q.join(OverrideType).filter(OverrideType.overridetype.in_(overridetype))
2137
2138     return q.all()
2139
2140 __all__.append('get_override')
2141
2142
2143 ################################################################################
2144
2145 class OverrideType(ORMObject):
2146     def __init__(self, overridetype = None):
2147         self.overridetype = overridetype
2148
2149     def properties(self):
2150         return ['overridetype', 'overridetype_id', 'overrides_count']
2151
2152     def not_null_constraints(self):
2153         return ['overridetype']
2154
2155 __all__.append('OverrideType')
2156
2157 @session_wrapper
2158 def get_override_type(override_type, session=None):
2159     """
2160     Returns OverrideType object for given C{override type}.
2161
2162     @type override_type: string
2163     @param override_type: The name of the override type
2164
2165     @type session: Session
2166     @param session: Optional SQLA session object (a temporary one will be
2167     generated if not supplied)
2168
2169     @rtype: int
2170     @return: the database id for the given override type
2171     """
2172
2173     q = session.query(OverrideType).filter_by(overridetype=override_type)
2174
2175     try:
2176         return q.one()
2177     except NoResultFound:
2178         return None
2179
2180 __all__.append('get_override_type')
2181
2182 ################################################################################
2183
2184 class PolicyQueue(object):
2185     def __init__(self, *args, **kwargs):
2186         pass
2187
2188     def __repr__(self):
2189         return '<PolicyQueue %s>' % self.queue_name
2190
2191 __all__.append('PolicyQueue')
2192
2193 @session_wrapper
2194 def get_policy_queue(queuename, session=None):
2195     """
2196     Returns PolicyQueue object for given C{queue name}
2197
2198     @type queuename: string
2199     @param queuename: The name of the queue
2200
2201     @type session: Session
2202     @param session: Optional SQLA session object (a temporary one will be
2203     generated if not supplied)
2204
2205     @rtype: PolicyQueue
2206     @return: PolicyQueue object for the given queue
2207     """
2208
2209     q = session.query(PolicyQueue).filter_by(queue_name=queuename)
2210
2211     try:
2212         return q.one()
2213     except NoResultFound:
2214         return None
2215
2216 __all__.append('get_policy_queue')
2217
2218 @session_wrapper
2219 def get_policy_queue_from_path(pathname, session=None):
2220     """
2221     Returns PolicyQueue object for given C{path name}
2222
2223     @type queuename: string
2224     @param queuename: The path
2225
2226     @type session: Session
2227     @param session: Optional SQLA session object (a temporary one will be
2228     generated if not supplied)
2229
2230     @rtype: PolicyQueue
2231     @return: PolicyQueue object for the given queue
2232     """
2233
2234     q = session.query(PolicyQueue).filter_by(path=pathname)
2235
2236     try:
2237         return q.one()
2238     except NoResultFound:
2239         return None
2240
2241 __all__.append('get_policy_queue_from_path')
2242
2243 ################################################################################
2244
2245 class Priority(ORMObject):
2246     def __init__(self, priority = None, level = None):
2247         self.priority = priority
2248         self.level = level
2249
2250     def properties(self):
2251         return ['priority', 'priority_id', 'level', 'overrides_count']
2252
2253     def not_null_constraints(self):
2254         return ['priority', 'level']
2255
2256     def __eq__(self, val):
2257         if isinstance(val, str):
2258             return (self.priority == val)
2259         # This signals to use the normal comparison operator
2260         return NotImplemented
2261
2262     def __ne__(self, val):
2263         if isinstance(val, str):
2264             return (self.priority != val)
2265         # This signals to use the normal comparison operator
2266         return NotImplemented
2267
2268 __all__.append('Priority')
2269
2270 @session_wrapper
2271 def get_priority(priority, session=None):
2272     """
2273     Returns Priority object for given C{priority name}.
2274
2275     @type priority: string
2276     @param priority: The name of the priority
2277
2278     @type session: Session
2279     @param session: Optional SQLA session object (a temporary one will be
2280     generated if not supplied)
2281
2282     @rtype: Priority
2283     @return: Priority object for the given priority
2284     """
2285
2286     q = session.query(Priority).filter_by(priority=priority)
2287
2288     try:
2289         return q.one()
2290     except NoResultFound:
2291         return None
2292
2293 __all__.append('get_priority')
2294
2295 @session_wrapper
2296 def get_priorities(session=None):
2297     """
2298     Returns dictionary of priority names -> id mappings
2299
2300     @type session: Session
2301     @param session: Optional SQL session object (a temporary one will be
2302     generated if not supplied)
2303
2304     @rtype: dictionary
2305     @return: dictionary of priority names -> id mappings
2306     """
2307
2308     ret = {}
2309     q = session.query(Priority)
2310     for x in q.all():
2311         ret[x.priority] = x.priority_id
2312
2313     return ret
2314
2315 __all__.append('get_priorities')
2316
2317 ################################################################################
2318
2319 class Section(ORMObject):
2320     def __init__(self, section = None):
2321         self.section = section
2322
2323     def properties(self):
2324         return ['section', 'section_id', 'overrides_count']
2325
2326     def not_null_constraints(self):
2327         return ['section']
2328
2329     def __eq__(self, val):
2330         if isinstance(val, str):
2331             return (self.section == val)
2332         # This signals to use the normal comparison operator
2333         return NotImplemented
2334
2335     def __ne__(self, val):
2336         if isinstance(val, str):
2337             return (self.section != val)
2338         # This signals to use the normal comparison operator
2339         return NotImplemented
2340
2341 __all__.append('Section')
2342
2343 @session_wrapper
2344 def get_section(section, session=None):
2345     """
2346     Returns Section object for given C{section name}.
2347
2348     @type section: string
2349     @param section: The name of the section
2350
2351     @type session: Session
2352     @param session: Optional SQLA session object (a temporary one will be
2353     generated if not supplied)
2354
2355     @rtype: Section
2356     @return: Section object for the given section name
2357     """
2358
2359     q = session.query(Section).filter_by(section=section)
2360
2361     try:
2362         return q.one()
2363     except NoResultFound:
2364         return None
2365
2366 __all__.append('get_section')
2367
2368 @session_wrapper
2369 def get_sections(session=None):
2370     """
2371     Returns dictionary of section names -> id mappings
2372
2373     @type session: Session
2374     @param session: Optional SQL session object (a temporary one will be
2375     generated if not supplied)
2376
2377     @rtype: dictionary
2378     @return: dictionary of section names -> id mappings
2379     """
2380
2381     ret = {}
2382     q = session.query(Section)
2383     for x in q.all():
2384         ret[x.section] = x.section_id
2385
2386     return ret
2387
2388 __all__.append('get_sections')
2389
2390 ################################################################################
2391
2392 class SrcContents(ORMObject):
2393     def __init__(self, file = None, source = None):
2394         self.file = file
2395         self.source = source
2396
2397     def properties(self):
2398         return ['file', 'source']
2399
2400 __all__.append('SrcContents')
2401
2402 ################################################################################
2403
2404 from debian.debfile import Deb822
2405
2406 # Temporary Deb822 subclass to fix bugs with : handling; see #597249
2407 class Dak822(Deb822):
2408     def _internal_parser(self, sequence, fields=None):
2409         # The key is non-whitespace, non-colon characters before any colon.
2410         key_part = r"^(?P<key>[^: \t\n\r\f\v]+)\s*:\s*"
2411         single = re.compile(key_part + r"(?P<data>\S.*?)\s*$")
2412         multi = re.compile(key_part + r"$")
2413         multidata = re.compile(r"^\s(?P<data>.+?)\s*$")
2414
2415         wanted_field = lambda f: fields is None or f in fields
2416
2417         if isinstance(sequence, basestring):
2418             sequence = sequence.splitlines()
2419
2420         curkey = None
2421         content = ""
2422         for line in self.gpg_stripped_paragraph(sequence):
2423             m = single.match(line)
2424             if m:
2425                 if curkey:
2426                     self[curkey] = content
2427
2428                 if not wanted_field(m.group('key')):
2429                     curkey = None
2430                     continue
2431
2432                 curkey = m.group('key')
2433                 content = m.group('data')
2434                 continue
2435
2436             m = multi.match(line)
2437             if m:
2438                 if curkey:
2439                     self[curkey] = content
2440
2441                 if not wanted_field(m.group('key')):
2442                     curkey = None
2443                     continue
2444
2445                 curkey = m.group('key')
2446                 content = ""
2447                 continue
2448
2449             m = multidata.match(line)
2450             if m:
2451                 content += '\n' + line # XXX not m.group('data')?
2452                 continue
2453
2454         if curkey:
2455             self[curkey] = content
2456
2457
2458 class DBSource(ORMObject):
2459     def __init__(self, source = None, version = None, maintainer = None, \
2460         changedby = None, poolfile = None, install_date = None):
2461         self.source = source
2462         self.version = version
2463         self.maintainer = maintainer
2464         self.changedby = changedby
2465         self.poolfile = poolfile
2466         self.install_date = install_date
2467
2468     @property
2469     def pkid(self):
2470         return self.source_id
2471
2472     def properties(self):
2473         return ['source', 'source_id', 'maintainer', 'changedby', \
2474             'fingerprint', 'poolfile', 'version', 'suites_count', \
2475             'install_date', 'binaries_count', 'uploaders_count']
2476
2477     def not_null_constraints(self):
2478         return ['source', 'version', 'install_date', 'maintainer', \
2479             'changedby', 'poolfile', 'install_date']
2480
2481     def read_control_fields(self):
2482         '''
2483         Reads the control information from a dsc
2484
2485         @rtype: tuple
2486         @return: fields is the dsc information in a dictionary form
2487         '''
2488         fullpath = self.poolfile.fullpath
2489         fields = Dak822(open(self.poolfile.fullpath, 'r'))
2490         return fields
2491
2492     metadata = association_proxy('key', 'value')
2493
2494     def scan_contents(self):
2495         '''
2496         Returns a set of names for non directories. The path names are
2497         normalized after converting them from either utf-8 or iso8859-1
2498         encoding.
2499         '''
2500         fullpath = self.poolfile.fullpath
2501         from daklib.contents import UnpackedSource
2502         unpacked = UnpackedSource(fullpath)
2503         fileset = set()
2504         for name in unpacked.get_all_filenames():
2505             # enforce proper utf-8 encoding
2506             try:
2507                 name.decode('utf-8')
2508             except UnicodeDecodeError:
2509                 name = name.decode('iso8859-1').encode('utf-8')
2510             fileset.add(name)
2511         return fileset
2512
2513 __all__.append('DBSource')
2514
2515 @session_wrapper
2516 def source_exists(source, source_version, suites = ["any"], session=None):
2517     """
2518     Ensure that source exists somewhere in the archive for the binary
2519     upload being processed.
2520       1. exact match     => 1.0-3
2521       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
2522
2523     @type source: string
2524     @param source: source name
2525
2526     @type source_version: string
2527     @param source_version: expected source version
2528
2529     @type suites: list
2530     @param suites: list of suites to check in, default I{any}
2531
2532     @type session: Session
2533     @param session: Optional SQLA session object (a temporary one will be
2534     generated if not supplied)
2535
2536     @rtype: int
2537     @return: returns 1 if a source with expected version is found, otherwise 0
2538
2539     """
2540
2541     cnf = Config()
2542     ret = True
2543
2544     from daklib.regexes import re_bin_only_nmu
2545     orig_source_version = re_bin_only_nmu.sub('', source_version)
2546
2547     for suite in suites:
2548         q = session.query(DBSource).filter_by(source=source). \
2549             filter(DBSource.version.in_([source_version, orig_source_version]))
2550         if suite != "any":
2551             # source must exist in 'suite' or a suite that is enhanced by 'suite'
2552             s = get_suite(suite, session)
2553             enhances_vcs = session.query(VersionCheck).filter(VersionCheck.suite==s).filter_by(check='Enhances')
2554             considered_suites = [ vc.reference for vc in enhances_vcs ]
2555             considered_suites.append(s)
2556
2557             q = q.filter(DBSource.suites.any(Suite.suite_id.in_([s.suite_id for s in considered_suites])))
2558
2559         if q.count() > 0:
2560             continue
2561
2562         # No source found so return not ok
2563         ret = False
2564
2565     return ret
2566
2567 __all__.append('source_exists')
2568
2569 @session_wrapper
2570 def get_suites_source_in(source, session=None):
2571     """
2572     Returns list of Suite objects which given C{source} name is in
2573
2574     @type source: str
2575     @param source: DBSource package name to search for
2576
2577     @rtype: list
2578     @return: list of Suite objects for the given source
2579     """
2580
2581     return session.query(Suite).filter(Suite.sources.any(source=source)).all()
2582
2583 __all__.append('get_suites_source_in')
2584
2585 @session_wrapper
2586 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
2587     """
2588     Returns list of DBSource objects for given C{source} name and other parameters
2589
2590     @type source: str
2591     @param source: DBSource package name to search for
2592
2593     @type version: str or None
2594     @param version: DBSource version name to search for or None if not applicable
2595
2596     @type dm_upload_allowed: bool
2597     @param dm_upload_allowed: If None, no effect.  If True or False, only
2598     return packages with that dm_upload_allowed setting
2599
2600     @type session: Session
2601     @param session: Optional SQL session object (a temporary one will be
2602     generated if not supplied)
2603
2604     @rtype: list
2605     @return: list of DBSource objects for the given name (may be empty)
2606     """
2607
2608     q = session.query(DBSource).filter_by(source=source)
2609
2610     if version is not None:
2611         q = q.filter_by(version=version)
2612
2613     if dm_upload_allowed is not None:
2614         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
2615
2616     return q.all()
2617
2618 __all__.append('get_sources_from_name')
2619
2620 # FIXME: This function fails badly if it finds more than 1 source package and
2621 # its implementation is trivial enough to be inlined.
2622 @session_wrapper
2623 def get_source_in_suite(source, suite, session=None):
2624     """
2625     Returns a DBSource object for a combination of C{source} and C{suite}.
2626
2627       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2628       - B{suite} - a suite name, eg. I{unstable}
2629
2630     @type source: string
2631     @param source: source package name
2632
2633     @type suite: string
2634     @param suite: the suite name
2635
2636     @rtype: string
2637     @return: the version for I{source} in I{suite}
2638
2639     """
2640
2641     q = get_suite(suite, session).get_sources(source)
2642     try:
2643         return q.one()
2644     except NoResultFound:
2645         return None
2646
2647 __all__.append('get_source_in_suite')
2648
2649 @session_wrapper
2650 def import_metadata_into_db(obj, session=None):
2651     """
2652     This routine works on either DBBinary or DBSource objects and imports
2653     their metadata into the database
2654     """
2655     fields = obj.read_control_fields()
2656     for k in fields.keys():
2657         try:
2658             # Try raw ASCII
2659             val = str(fields[k])
2660         except UnicodeEncodeError:
2661             # Fall back to UTF-8
2662             try:
2663                 val = fields[k].encode('utf-8')
2664             except UnicodeEncodeError:
2665                 # Finally try iso8859-1
2666                 val = fields[k].encode('iso8859-1')
2667                 # Otherwise we allow the exception to percolate up and we cause
2668                 # a reject as someone is playing silly buggers
2669
2670         obj.metadata[get_or_set_metadatakey(k, session)] = val
2671
2672     session.commit_or_flush()
2673
2674 __all__.append('import_metadata_into_db')
2675
2676
2677 ################################################################################
2678
2679 def split_uploaders(uploaders_list):
2680     '''
2681     Split the Uploaders field into the individual uploaders and yield each of
2682     them. Beware: email addresses might contain commas.
2683     '''
2684     import re
2685     for uploader in re.sub(">[ ]*,", ">\t", uploaders_list).split("\t"):
2686         yield uploader.strip()
2687
2688 @session_wrapper
2689 def add_dsc_to_db(u, filename, session=None):
2690     entry = u.pkg.files[filename]
2691     source = DBSource()
2692     pfs = []
2693
2694     source.source = u.pkg.dsc["source"]
2695     source.version = u.pkg.dsc["version"] # NB: not files[file]["version"], that has no epoch
2696     source.maintainer_id = get_or_set_maintainer(u.pkg.dsc["maintainer"], session).maintainer_id
2697     # If Changed-By isn't available, fall back to maintainer
2698     if u.pkg.changes.has_key("changed-by"):
2699         source.changedby_id = get_or_set_maintainer(u.pkg.changes["changed-by"], session).maintainer_id
2700     else:
2701         source.changedby_id = get_or_set_maintainer(u.pkg.dsc["maintainer"], session).maintainer_id
2702     source.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2703     source.install_date = datetime.now().date()
2704
2705     dsc_component = entry["component"]
2706     dsc_location_id = entry["location id"]
2707
2708     source.dm_upload_allowed = (u.pkg.dsc.get("dm-upload-allowed", '') == "yes")
2709
2710     # Set up a new poolfile if necessary
2711     if not entry.has_key("files id") or not entry["files id"]:
2712         filename = entry["pool name"] + filename
2713         poolfile = add_poolfile(filename, entry, dsc_location_id, session)
2714         session.flush()
2715         pfs.append(poolfile)
2716         entry["files id"] = poolfile.file_id
2717
2718     source.poolfile_id = entry["files id"]
2719     session.add(source)
2720
2721     suite_names = u.pkg.changes["distribution"].keys()
2722     source.suites = session.query(Suite). \
2723         filter(Suite.suite_name.in_(suite_names)).all()
2724
2725     # Add the source files to the DB (files and dsc_files)
2726     dscfile = DSCFile()
2727     dscfile.source_id = source.source_id
2728     dscfile.poolfile_id = entry["files id"]
2729     session.add(dscfile)
2730
2731     for dsc_file, dentry in u.pkg.dsc_files.items():
2732         df = DSCFile()
2733         df.source_id = source.source_id
2734
2735         # If the .orig tarball is already in the pool, it's
2736         # files id is stored in dsc_files by check_dsc().
2737         files_id = dentry.get("files id", None)
2738
2739         # Find the entry in the files hash
2740         # TODO: Bail out here properly
2741         dfentry = None
2742         for f, e in u.pkg.files.items():
2743             if f == dsc_file:
2744                 dfentry = e
2745                 break
2746
2747         if files_id is None:
2748             filename = dfentry["pool name"] + dsc_file
2749
2750             (found, obj) = check_poolfile(filename, dentry["size"], dentry["md5sum"], dsc_location_id)
2751             # FIXME: needs to check for -1/-2 and or handle exception
2752             if found and obj is not None:
2753                 files_id = obj.file_id
2754                 pfs.append(obj)
2755
2756             # If still not found, add it
2757             if files_id is None:
2758                 # HACK: Force sha1sum etc into dentry
2759                 dentry["sha1sum"] = dfentry["sha1sum"]
2760                 dentry["sha256sum"] = dfentry["sha256sum"]
2761                 poolfile = add_poolfile(filename, dentry, dsc_location_id, session)
2762                 pfs.append(poolfile)
2763                 files_id = poolfile.file_id
2764         else:
2765             poolfile = get_poolfile_by_id(files_id, session)
2766             if poolfile is None:
2767                 utils.fubar("INTERNAL ERROR. Found no poolfile with id %d" % files_id)
2768             pfs.append(poolfile)
2769
2770         df.poolfile_id = files_id
2771         session.add(df)
2772
2773     # Add the src_uploaders to the DB
2774     session.flush()
2775     session.refresh(source)
2776     source.uploaders = [source.maintainer]
2777     if u.pkg.dsc.has_key("uploaders"):
2778         for up in split_uploaders(u.pkg.dsc["uploaders"]):
2779             source.uploaders.append(get_or_set_maintainer(up, session))
2780
2781     session.flush()
2782
2783     return source, dsc_component, dsc_location_id, pfs
2784
2785 __all__.append('add_dsc_to_db')
2786
2787 @session_wrapper
2788 def add_deb_to_db(u, filename, session=None):
2789     """
2790     Contrary to what you might expect, this routine deals with both
2791     debs and udebs.  That info is in 'dbtype', whilst 'type' is
2792     'deb' for both of them
2793     """
2794     cnf = Config()
2795     entry = u.pkg.files[filename]
2796
2797     bin = DBBinary()
2798     bin.package = entry["package"]
2799     bin.version = entry["version"]
2800     bin.maintainer_id = get_or_set_maintainer(entry["maintainer"], session).maintainer_id
2801     bin.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2802     bin.arch_id = get_architecture(entry["architecture"], session).arch_id
2803     bin.binarytype = entry["dbtype"]
2804
2805     # Find poolfile id
2806     filename = entry["pool name"] + filename
2807     fullpath = os.path.join(cnf["Dir::Pool"], filename)
2808     if not entry.get("location id", None):
2809         entry["location id"] = get_location(cnf["Dir::Pool"], entry["component"], session=session).location_id
2810
2811     if entry.get("files id", None):
2812         poolfile = get_poolfile_by_id(bin.poolfile_id)
2813         bin.poolfile_id = entry["files id"]
2814     else:
2815         poolfile = add_poolfile(filename, entry, entry["location id"], session)
2816         bin.poolfile_id = entry["files id"] = poolfile.file_id
2817
2818     # Find source id
2819     bin_sources = get_sources_from_name(entry["source package"], entry["source version"], session=session)
2820     if len(bin_sources) != 1:
2821         raise NoSourceFieldError, "Unable to find a unique source id for %s (%s), %s, file %s, type %s, signed by %s" % \
2822                                   (bin.package, bin.version, entry["architecture"],
2823                                    filename, bin.binarytype, u.pkg.changes["fingerprint"])
2824
2825     bin.source_id = bin_sources[0].source_id
2826
2827     if entry.has_key("built-using"):
2828         for srcname, version in entry["built-using"]:
2829             exsources = get_sources_from_name(srcname, version, session=session)
2830             if len(exsources) != 1:
2831                 raise NoSourceFieldError, "Unable to find source package (%s = %s) in Built-Using for %s (%s), %s, file %s, type %s, signed by %s" % \
2832                                           (srcname, version, bin.package, bin.version, entry["architecture"],
2833                                            filename, bin.binarytype, u.pkg.changes["fingerprint"])
2834
2835             bin.extra_sources.append(exsources[0])
2836
2837     # Add and flush object so it has an ID
2838     session.add(bin)
2839
2840     suite_names = u.pkg.changes["distribution"].keys()
2841     bin.suites = session.query(Suite). \
2842         filter(Suite.suite_name.in_(suite_names)).all()
2843
2844     session.flush()
2845
2846     # Deal with contents - disabled for now
2847     #contents = copy_temporary_contents(bin.package, bin.version, bin.architecture.arch_string, os.path.basename(filename), None, session)
2848     #if not contents:
2849     #    print "REJECT\nCould not determine contents of package %s" % bin.package
2850     #    session.rollback()
2851     #    raise MissingContents, "No contents stored for package %s, and couldn't determine contents of %s" % (bin.package, filename)
2852
2853     return bin, poolfile
2854
2855 __all__.append('add_deb_to_db')
2856
2857 ################################################################################
2858
2859 class SourceACL(object):
2860     def __init__(self, *args, **kwargs):
2861         pass
2862
2863     def __repr__(self):
2864         return '<SourceACL %s>' % self.source_acl_id
2865
2866 __all__.append('SourceACL')
2867
2868 ################################################################################
2869
2870 class SrcFormat(object):
2871     def __init__(self, *args, **kwargs):
2872         pass
2873
2874     def __repr__(self):
2875         return '<SrcFormat %s>' % (self.format_name)
2876
2877 __all__.append('SrcFormat')
2878
2879 ################################################################################
2880
2881 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
2882                  ('SuiteID', 'suite_id'),
2883                  ('Version', 'version'),
2884                  ('Origin', 'origin'),
2885                  ('Label', 'label'),
2886                  ('Description', 'description'),
2887                  ('Untouchable', 'untouchable'),
2888                  ('Announce', 'announce'),
2889                  ('Codename', 'codename'),
2890                  ('OverrideCodename', 'overridecodename'),
2891                  ('ValidTime', 'validtime'),
2892                  ('Priority', 'priority'),
2893                  ('NotAutomatic', 'notautomatic'),
2894                  ('CopyChanges', 'copychanges'),
2895                  ('OverrideSuite', 'overridesuite')]
2896
2897 # Why the heck don't we have any UNIQUE constraints in table suite?
2898 # TODO: Add UNIQUE constraints for appropriate columns.
2899 class Suite(ORMObject):
2900     def __init__(self, suite_name = None, version = None):
2901         self.suite_name = suite_name
2902         self.version = version
2903
2904     def properties(self):
2905         return ['suite_name', 'version', 'sources_count', 'binaries_count', \
2906             'overrides_count']
2907
2908     def not_null_constraints(self):
2909         return ['suite_name']
2910
2911     def __eq__(self, val):
2912         if isinstance(val, str):
2913             return (self.suite_name == val)
2914         # This signals to use the normal comparison operator
2915         return NotImplemented
2916
2917     def __ne__(self, val):
2918         if isinstance(val, str):
2919             return (self.suite_name != val)
2920         # This signals to use the normal comparison operator
2921         return NotImplemented
2922
2923     def details(self):
2924         ret = []
2925         for disp, field in SUITE_FIELDS:
2926             val = getattr(self, field, None)
2927             if val is not None:
2928                 ret.append("%s: %s" % (disp, val))
2929
2930         return "\n".join(ret)
2931
2932     def get_architectures(self, skipsrc=False, skipall=False):
2933         """
2934         Returns list of Architecture objects
2935
2936         @type skipsrc: boolean
2937         @param skipsrc: Whether to skip returning the 'source' architecture entry
2938         (Default False)
2939
2940         @type skipall: boolean
2941         @param skipall: Whether to skip returning the 'all' architecture entry
2942         (Default False)
2943
2944         @rtype: list
2945         @return: list of Architecture objects for the given name (may be empty)
2946         """
2947
2948         q = object_session(self).query(Architecture).with_parent(self)
2949         if skipsrc:
2950             q = q.filter(Architecture.arch_string != 'source')
2951         if skipall:
2952             q = q.filter(Architecture.arch_string != 'all')
2953         return q.order_by(Architecture.arch_string).all()
2954
2955     def get_sources(self, source):
2956         """
2957         Returns a query object representing DBSource that is part of C{suite}.
2958
2959           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2960
2961         @type source: string
2962         @param source: source package name
2963
2964         @rtype: sqlalchemy.orm.query.Query
2965         @return: a query of DBSource
2966
2967         """
2968
2969         session = object_session(self)
2970         return session.query(DBSource).filter_by(source = source). \
2971             with_parent(self)
2972
2973     def get_overridesuite(self):
2974         if self.overridesuite is None:
2975             return self
2976         else:
2977             return object_session(self).query(Suite).filter_by(suite_name=self.overridesuite).one()
2978
2979 __all__.append('Suite')
2980
2981 @session_wrapper
2982 def get_suite(suite, session=None):
2983     """
2984     Returns Suite object for given C{suite name}.
2985
2986     @type suite: string
2987     @param suite: The name of the suite
2988
2989     @type session: Session
2990     @param session: Optional SQLA session object (a temporary one will be
2991     generated if not supplied)
2992
2993     @rtype: Suite
2994     @return: Suite object for the requested suite name (None if not present)
2995     """
2996
2997     q = session.query(Suite).filter_by(suite_name=suite)
2998
2999     try:
3000         return q.one()
3001     except NoResultFound:
3002         return None
3003
3004 __all__.append('get_suite')
3005
3006 ################################################################################
3007
3008 @session_wrapper
3009 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
3010     """
3011     Returns list of Architecture objects for given C{suite} name. The list is
3012     empty if suite does not exist.
3013
3014     @type suite: str
3015     @param suite: Suite name to search for
3016
3017     @type skipsrc: boolean
3018     @param skipsrc: Whether to skip returning the 'source' architecture entry
3019     (Default False)
3020
3021     @type skipall: boolean
3022     @param skipall: Whether to skip returning the 'all' architecture entry
3023     (Default False)
3024
3025     @type session: Session
3026     @param session: Optional SQL session object (a temporary one will be
3027     generated if not supplied)
3028
3029     @rtype: list
3030     @return: list of Architecture objects for the given name (may be empty)
3031     """
3032
3033     try:
3034         return get_suite(suite, session).get_architectures(skipsrc, skipall)
3035     except AttributeError:
3036         return []
3037
3038 __all__.append('get_suite_architectures')
3039
3040 ################################################################################
3041
3042 class Uid(ORMObject):
3043     def __init__(self, uid = None, name = None):
3044         self.uid = uid
3045         self.name = name
3046
3047     def __eq__(self, val):
3048         if isinstance(val, str):
3049             return (self.uid == val)
3050         # This signals to use the normal comparison operator
3051         return NotImplemented
3052
3053     def __ne__(self, val):
3054         if isinstance(val, str):
3055             return (self.uid != val)
3056         # This signals to use the normal comparison operator
3057         return NotImplemented
3058
3059     def properties(self):
3060         return ['uid', 'name', 'fingerprint']
3061
3062     def not_null_constraints(self):
3063         return ['uid']
3064
3065 __all__.append('Uid')
3066
3067 @session_wrapper
3068 def get_or_set_uid(uidname, session=None):
3069     """
3070     Returns uid object for given uidname.
3071
3072     If no matching uidname is found, a row is inserted.
3073
3074     @type uidname: string
3075     @param uidname: The uid to add
3076
3077     @type session: SQLAlchemy
3078     @param session: Optional SQL session object (a temporary one will be
3079     generated if not supplied).  If not passed, a commit will be performed at
3080     the end of the function, otherwise the caller is responsible for commiting.
3081
3082     @rtype: Uid
3083     @return: the uid object for the given uidname
3084     """
3085
3086     q = session.query(Uid).filter_by(uid=uidname)
3087
3088     try:
3089         ret = q.one()
3090     except NoResultFound:
3091         uid = Uid()
3092         uid.uid = uidname
3093         session.add(uid)
3094         session.commit_or_flush()
3095         ret = uid
3096
3097     return ret
3098
3099 __all__.append('get_or_set_uid')
3100
3101 @session_wrapper
3102 def get_uid_from_fingerprint(fpr, session=None):
3103     q = session.query(Uid)
3104     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
3105
3106     try:
3107         return q.one()
3108     except NoResultFound:
3109         return None
3110
3111 __all__.append('get_uid_from_fingerprint')
3112
3113 ################################################################################
3114
3115 class UploadBlock(object):
3116     def __init__(self, *args, **kwargs):
3117         pass
3118
3119     def __repr__(self):
3120         return '<UploadBlock %s (%s)>' % (self.source, self.upload_block_id)
3121
3122 __all__.append('UploadBlock')
3123
3124 ################################################################################
3125
3126 class MetadataKey(ORMObject):
3127     def __init__(self, key = None):
3128         self.key = key
3129
3130     def properties(self):
3131         return ['key']
3132
3133     def not_null_constraints(self):
3134         return ['key']
3135
3136 __all__.append('MetadataKey')
3137
3138 @session_wrapper
3139 def get_or_set_metadatakey(keyname, session=None):
3140     """
3141     Returns MetadataKey object for given uidname.
3142
3143     If no matching keyname is found, a row is inserted.
3144
3145     @type uidname: string
3146     @param uidname: The keyname to add
3147
3148     @type session: SQLAlchemy
3149     @param session: Optional SQL session object (a temporary one will be
3150     generated if not supplied).  If not passed, a commit will be performed at
3151     the end of the function, otherwise the caller is responsible for commiting.
3152
3153     @rtype: MetadataKey
3154     @return: the metadatakey object for the given keyname
3155     """
3156
3157     q = session.query(MetadataKey).filter_by(key=keyname)
3158
3159     try:
3160         ret = q.one()
3161     except NoResultFound:
3162         ret = MetadataKey(keyname)
3163         session.add(ret)
3164         session.commit_or_flush()
3165
3166     return ret
3167
3168 __all__.append('get_or_set_metadatakey')
3169
3170 ################################################################################
3171
3172 class BinaryMetadata(ORMObject):
3173     def __init__(self, key = None, value = None, binary = None):
3174         self.key = key
3175         self.value = value
3176         self.binary = binary
3177
3178     def properties(self):
3179         return ['binary', 'key', 'value']
3180
3181     def not_null_constraints(self):
3182         return ['value']
3183
3184 __all__.append('BinaryMetadata')
3185
3186 ################################################################################
3187
3188 class SourceMetadata(ORMObject):
3189     def __init__(self, key = None, value = None, source = None):
3190         self.key = key
3191         self.value = value
3192         self.source = source
3193
3194     def properties(self):
3195         return ['source', 'key', 'value']
3196
3197     def not_null_constraints(self):
3198         return ['value']
3199
3200 __all__.append('SourceMetadata')
3201
3202 ################################################################################
3203
3204 class VersionCheck(ORMObject):
3205     def __init__(self, *args, **kwargs):
3206         pass
3207
3208     def properties(self):
3209         #return ['suite_id', 'check', 'reference_id']
3210         return ['check']
3211
3212     def not_null_constraints(self):
3213         return ['suite', 'check', 'reference']
3214
3215 __all__.append('VersionCheck')
3216
3217 @session_wrapper
3218 def get_version_checks(suite_name, check = None, session = None):
3219     suite = get_suite(suite_name, session)
3220     if not suite:
3221         # Make sure that what we return is iterable so that list comprehensions
3222         # involving this don't cause a traceback
3223         return []
3224     q = session.query(VersionCheck).filter_by(suite=suite)
3225     if check:
3226         q = q.filter_by(check=check)
3227     return q.all()
3228
3229 __all__.append('get_version_checks')
3230
3231 ################################################################################
3232
3233 class DBConn(object):
3234     """
3235     database module init.
3236     """
3237     __shared_state = {}
3238
3239     def __init__(self, *args, **kwargs):
3240         self.__dict__ = self.__shared_state
3241
3242         if not getattr(self, 'initialised', False):
3243             self.initialised = True
3244             self.debug = kwargs.has_key('debug')
3245             self.__createconn()
3246
3247     def __setuptables(self):
3248         tables = (
3249             'architecture',
3250             'archive',
3251             'bin_associations',
3252             'bin_contents',
3253             'binaries',
3254             'binaries_metadata',
3255             'binary_acl',
3256             'binary_acl_map',
3257             'build_queue',
3258             'build_queue_files',
3259             'build_queue_policy_files',
3260             'changelogs_text',
3261             'changes',
3262             'component',
3263             'config',
3264             'changes_pending_binaries',
3265             'changes_pending_files',
3266             'changes_pending_source',
3267             'changes_pending_files_map',
3268             'changes_pending_source_files',
3269             'changes_pool_files',
3270             'dsc_files',
3271             'external_overrides',
3272             'extra_src_references',
3273             'files',
3274             'fingerprint',
3275             'keyrings',
3276             'keyring_acl_map',
3277             'location',
3278             'maintainer',
3279             'metadata_keys',
3280             'new_comments',
3281             # TODO: the maintainer column in table override should be removed.
3282             'override',
3283             'override_type',
3284             'policy_queue',
3285             'priority',
3286             'section',
3287             'source',
3288             'source_acl',
3289             'source_metadata',
3290             'src_associations',
3291             'src_contents',
3292             'src_format',
3293             'src_uploaders',
3294             'suite',
3295             'suite_architectures',
3296             'suite_build_queue_copy',
3297             'suite_src_formats',
3298             'uid',
3299             'upload_blocks',
3300             'version_check',
3301         )
3302
3303         views = (
3304             'almost_obsolete_all_associations',
3305             'almost_obsolete_src_associations',
3306             'any_associations_source',
3307             'bin_associations_binaries',
3308             'binaries_suite_arch',
3309             'binfiles_suite_component_arch',
3310             'changelogs',
3311             'file_arch_suite',
3312             'newest_all_associations',
3313             'newest_any_associations',
3314             'newest_source',
3315             'newest_src_association',
3316             'obsolete_all_associations',
3317             'obsolete_any_associations',
3318             'obsolete_any_by_all_associations',
3319             'obsolete_src_associations',
3320             'source_suite',
3321             'src_associations_bin',
3322             'src_associations_src',
3323             'suite_arch_by_name',
3324         )
3325
3326         for table_name in tables:
3327             table = Table(table_name, self.db_meta, \
3328                 autoload=True, useexisting=True)
3329             setattr(self, 'tbl_%s' % table_name, table)
3330
3331         for view_name in views:
3332             view = Table(view_name, self.db_meta, autoload=True)
3333             setattr(self, 'view_%s' % view_name, view)
3334
3335     def __setupmappers(self):
3336         mapper(Architecture, self.tbl_architecture,
3337             properties = dict(arch_id = self.tbl_architecture.c.id,
3338                suites = relation(Suite, secondary=self.tbl_suite_architectures,
3339                    order_by='suite_name',
3340                    backref=backref('architectures', order_by='arch_string'))),
3341             extension = validator)
3342
3343         mapper(Archive, self.tbl_archive,
3344                properties = dict(archive_id = self.tbl_archive.c.id,
3345                                  archive_name = self.tbl_archive.c.name))
3346
3347         mapper(BuildQueue, self.tbl_build_queue,
3348                properties = dict(queue_id = self.tbl_build_queue.c.id))
3349
3350         mapper(BuildQueueFile, self.tbl_build_queue_files,
3351                properties = dict(buildqueue = relation(BuildQueue, backref='queuefiles'),
3352                                  poolfile = relation(PoolFile, backref='buildqueueinstances')))
3353
3354         mapper(BuildQueuePolicyFile, self.tbl_build_queue_policy_files,
3355                properties = dict(
3356                 build_queue = relation(BuildQueue, backref='policy_queue_files'),
3357                 file = relation(ChangePendingFile, lazy='joined')))
3358
3359         mapper(DBBinary, self.tbl_binaries,
3360                properties = dict(binary_id = self.tbl_binaries.c.id,
3361                                  package = self.tbl_binaries.c.package,
3362                                  version = self.tbl_binaries.c.version,
3363                                  maintainer_id = self.tbl_binaries.c.maintainer,
3364                                  maintainer = relation(Maintainer),
3365                                  source_id = self.tbl_binaries.c.source,
3366                                  source = relation(DBSource, backref='binaries'),
3367                                  arch_id = self.tbl_binaries.c.architecture,
3368                                  architecture = relation(Architecture),
3369                                  poolfile_id = self.tbl_binaries.c.file,
3370                                  poolfile = relation(PoolFile, backref=backref('binary', uselist = False)),
3371                                  binarytype = self.tbl_binaries.c.type,
3372                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
3373                                  fingerprint = relation(Fingerprint),
3374                                  install_date = self.tbl_binaries.c.install_date,
3375                                  suites = relation(Suite, secondary=self.tbl_bin_associations,
3376                                      backref=backref('binaries', lazy='dynamic')),
3377                                  extra_sources = relation(DBSource, secondary=self.tbl_extra_src_references,
3378                                      backref=backref('extra_binary_references', lazy='dynamic')),
3379                                  key = relation(BinaryMetadata, cascade='all',
3380                                      collection_class=attribute_mapped_collection('key'))),
3381                 extension = validator)
3382
3383         mapper(BinaryACL, self.tbl_binary_acl,
3384                properties = dict(binary_acl_id = self.tbl_binary_acl.c.id))
3385
3386         mapper(BinaryACLMap, self.tbl_binary_acl_map,
3387                properties = dict(binary_acl_map_id = self.tbl_binary_acl_map.c.id,
3388                                  fingerprint = relation(Fingerprint, backref="binary_acl_map"),
3389                                  architecture = relation(Architecture)))
3390
3391         mapper(Component, self.tbl_component,
3392                properties = dict(component_id = self.tbl_component.c.id,
3393                                  component_name = self.tbl_component.c.name),
3394                extension = validator)
3395
3396         mapper(DBConfig, self.tbl_config,
3397                properties = dict(config_id = self.tbl_config.c.id))
3398
3399         mapper(DSCFile, self.tbl_dsc_files,
3400                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
3401                                  source_id = self.tbl_dsc_files.c.source,
3402                                  source = relation(DBSource),
3403                                  poolfile_id = self.tbl_dsc_files.c.file,
3404                                  poolfile = relation(PoolFile)))
3405
3406         mapper(ExternalOverride, self.tbl_external_overrides,
3407                 properties = dict(
3408                     suite_id = self.tbl_external_overrides.c.suite,
3409                     suite = relation(Suite),
3410                     component_id = self.tbl_external_overrides.c.component,
3411                     component = relation(Component)))
3412
3413         mapper(PoolFile, self.tbl_files,
3414                properties = dict(file_id = self.tbl_files.c.id,
3415                                  filesize = self.tbl_files.c.size,
3416                                  location_id = self.tbl_files.c.location,
3417                                  location = relation(Location,
3418                                      # using lazy='dynamic' in the back
3419                                      # reference because we have A LOT of
3420                                      # files in one location
3421                                      backref=backref('files', lazy='dynamic'))),
3422                 extension = validator)
3423
3424         mapper(Fingerprint, self.tbl_fingerprint,
3425                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
3426                                  uid_id = self.tbl_fingerprint.c.uid,
3427                                  uid = relation(Uid),
3428                                  keyring_id = self.tbl_fingerprint.c.keyring,
3429                                  keyring = relation(Keyring),
3430                                  source_acl = relation(SourceACL),
3431                                  binary_acl = relation(BinaryACL)),
3432                extension = validator)
3433
3434         mapper(Keyring, self.tbl_keyrings,
3435                properties = dict(keyring_name = self.tbl_keyrings.c.name,
3436                                  keyring_id = self.tbl_keyrings.c.id))
3437
3438         mapper(DBChange, self.tbl_changes,
3439                properties = dict(change_id = self.tbl_changes.c.id,
3440                                  poolfiles = relation(PoolFile,
3441                                                       secondary=self.tbl_changes_pool_files,
3442                                                       backref="changeslinks"),
3443                                  seen = self.tbl_changes.c.seen,
3444                                  source = self.tbl_changes.c.source,
3445                                  binaries = self.tbl_changes.c.binaries,
3446                                  architecture = self.tbl_changes.c.architecture,
3447                                  distribution = self.tbl_changes.c.distribution,
3448                                  urgency = self.tbl_changes.c.urgency,
3449                                  maintainer = self.tbl_changes.c.maintainer,
3450                                  changedby = self.tbl_changes.c.changedby,
3451                                  date = self.tbl_changes.c.date,
3452                                  version = self.tbl_changes.c.version,
3453                                  files = relation(ChangePendingFile,
3454                                                   secondary=self.tbl_changes_pending_files_map,
3455                                                   backref="changesfile"),
3456                                  in_queue_id = self.tbl_changes.c.in_queue,
3457                                  in_queue = relation(PolicyQueue,
3458                                                      primaryjoin=(self.tbl_changes.c.in_queue==self.tbl_policy_queue.c.id)),
3459                                  approved_for_id = self.tbl_changes.c.approved_for))
3460
3461         mapper(ChangePendingBinary, self.tbl_changes_pending_binaries,
3462                properties = dict(change_pending_binary_id = self.tbl_changes_pending_binaries.c.id))
3463
3464         mapper(ChangePendingFile, self.tbl_changes_pending_files,
3465                properties = dict(change_pending_file_id = self.tbl_changes_pending_files.c.id,
3466                                  filename = self.tbl_changes_pending_files.c.filename,
3467                                  size = self.tbl_changes_pending_files.c.size,
3468                                  md5sum = self.tbl_changes_pending_files.c.md5sum,
3469                                  sha1sum = self.tbl_changes_pending_files.c.sha1sum,
3470                                  sha256sum = self.tbl_changes_pending_files.c.sha256sum))
3471
3472         mapper(ChangePendingSource, self.tbl_changes_pending_source,
3473                properties = dict(change_pending_source_id = self.tbl_changes_pending_source.c.id,
3474                                  change = relation(DBChange),
3475                                  maintainer = relation(Maintainer,
3476                                                        primaryjoin=(self.tbl_changes_pending_source.c.maintainer_id==self.tbl_maintainer.c.id)),
3477                                  changedby = relation(Maintainer,
3478                                                       primaryjoin=(self.tbl_changes_pending_source.c.changedby_id==self.tbl_maintainer.c.id)),
3479                                  fingerprint = relation(Fingerprint),
3480                                  source_files = relation(ChangePendingFile,
3481                                                          secondary=self.tbl_changes_pending_source_files,
3482                                                          backref="pending_sources")))
3483
3484
3485         mapper(KeyringACLMap, self.tbl_keyring_acl_map,
3486                properties = dict(keyring_acl_map_id = self.tbl_keyring_acl_map.c.id,
3487                                  keyring = relation(Keyring, backref="keyring_acl_map"),
3488                                  architecture = relation(Architecture)))
3489
3490         mapper(Location, self.tbl_location,
3491                properties = dict(location_id = self.tbl_location.c.id,
3492                                  component_id = self.tbl_location.c.component,
3493                                  component = relation(Component, backref='location'),
3494                                  archive_id = self.tbl_location.c.archive,
3495                                  archive = relation(Archive),
3496                                  # FIXME: the 'type' column is old cruft and
3497                                  # should be removed in the future.
3498                                  archive_type = self.tbl_location.c.type),
3499                extension = validator)
3500
3501         mapper(Maintainer, self.tbl_maintainer,
3502                properties = dict(maintainer_id = self.tbl_maintainer.c.id,
3503                    maintains_sources = relation(DBSource, backref='maintainer',
3504                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.maintainer)),
3505                    changed_sources = relation(DBSource, backref='changedby',
3506                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.changedby))),
3507                 extension = validator)
3508
3509         mapper(NewComment, self.tbl_new_comments,
3510                properties = dict(comment_id = self.tbl_new_comments.c.id))
3511
3512         mapper(Override, self.tbl_override,
3513                properties = dict(suite_id = self.tbl_override.c.suite,
3514                                  suite = relation(Suite, \
3515                                     backref=backref('overrides', lazy='dynamic')),
3516                                  package = self.tbl_override.c.package,
3517                                  component_id = self.tbl_override.c.component,
3518                                  component = relation(Component, \
3519                                     backref=backref('overrides', lazy='dynamic')),
3520                                  priority_id = self.tbl_override.c.priority,
3521                                  priority = relation(Priority, \
3522                                     backref=backref('overrides', lazy='dynamic')),
3523                                  section_id = self.tbl_override.c.section,
3524                                  section = relation(Section, \
3525                                     backref=backref('overrides', lazy='dynamic')),
3526                                  overridetype_id = self.tbl_override.c.type,
3527                                  overridetype = relation(OverrideType, \
3528                                     backref=backref('overrides', lazy='dynamic'))))
3529
3530         mapper(OverrideType, self.tbl_override_type,
3531                properties = dict(overridetype = self.tbl_override_type.c.type,
3532                                  overridetype_id = self.tbl_override_type.c.id))
3533
3534         mapper(PolicyQueue, self.tbl_policy_queue,
3535                properties = dict(policy_queue_id = self.tbl_policy_queue.c.id))
3536
3537         mapper(Priority, self.tbl_priority,
3538                properties = dict(priority_id = self.tbl_priority.c.id))
3539
3540         mapper(Section, self.tbl_section,
3541                properties = dict(section_id = self.tbl_section.c.id,
3542                                  section=self.tbl_section.c.section))
3543
3544         mapper(DBSource, self.tbl_source,
3545                properties = dict(source_id = self.tbl_source.c.id,
3546                                  version = self.tbl_source.c.version,
3547                                  maintainer_id = self.tbl_source.c.maintainer,
3548                                  poolfile_id = self.tbl_source.c.file,
3549                                  poolfile = relation(PoolFile, backref=backref('source', uselist = False)),
3550                                  fingerprint_id = self.tbl_source.c.sig_fpr,
3551                                  fingerprint = relation(Fingerprint),
3552                                  changedby_id = self.tbl_source.c.changedby,
3553                                  srcfiles = relation(DSCFile,
3554                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
3555                                  suites = relation(Suite, secondary=self.tbl_src_associations,
3556                                      backref=backref('sources', lazy='dynamic')),
3557                                  uploaders = relation(Maintainer,
3558                                      secondary=self.tbl_src_uploaders),
3559                                  key = relation(SourceMetadata, cascade='all',
3560                                      collection_class=attribute_mapped_collection('key'))),
3561                extension = validator)
3562
3563         mapper(SourceACL, self.tbl_source_acl,
3564                properties = dict(source_acl_id = self.tbl_source_acl.c.id))
3565
3566         mapper(SrcFormat, self.tbl_src_format,
3567                properties = dict(src_format_id = self.tbl_src_format.c.id,
3568                                  format_name = self.tbl_src_format.c.format_name))
3569
3570         mapper(Suite, self.tbl_suite,
3571                properties = dict(suite_id = self.tbl_suite.c.id,
3572                                  policy_queue = relation(PolicyQueue),
3573                                  copy_queues = relation(BuildQueue,
3574                                      secondary=self.tbl_suite_build_queue_copy),
3575                                  srcformats = relation(SrcFormat, secondary=self.tbl_suite_src_formats,
3576                                      backref=backref('suites', lazy='dynamic'))),
3577                 extension = validator)
3578
3579         mapper(Uid, self.tbl_uid,
3580                properties = dict(uid_id = self.tbl_uid.c.id,
3581                                  fingerprint = relation(Fingerprint)),
3582                extension = validator)
3583
3584         mapper(UploadBlock, self.tbl_upload_blocks,
3585                properties = dict(upload_block_id = self.tbl_upload_blocks.c.id,
3586                                  fingerprint = relation(Fingerprint, backref="uploadblocks"),
3587                                  uid = relation(Uid, backref="uploadblocks")))
3588
3589         mapper(BinContents, self.tbl_bin_contents,
3590             properties = dict(
3591                 binary = relation(DBBinary,
3592                     backref=backref('contents', lazy='dynamic', cascade='all')),
3593                 file = self.tbl_bin_contents.c.file))
3594
3595         mapper(SrcContents, self.tbl_src_contents,
3596             properties = dict(
3597                 source = relation(DBSource,
3598                     backref=backref('contents', lazy='dynamic', cascade='all')),
3599                 file = self.tbl_src_contents.c.file))
3600
3601         mapper(MetadataKey, self.tbl_metadata_keys,
3602             properties = dict(
3603                 key_id = self.tbl_metadata_keys.c.key_id,
3604                 key = self.tbl_metadata_keys.c.key))
3605
3606         mapper(BinaryMetadata, self.tbl_binaries_metadata,
3607             properties = dict(
3608                 binary_id = self.tbl_binaries_metadata.c.bin_id,
3609                 binary = relation(DBBinary),
3610                 key_id = self.tbl_binaries_metadata.c.key_id,
3611                 key = relation(MetadataKey),
3612                 value = self.tbl_binaries_metadata.c.value))
3613
3614         mapper(SourceMetadata, self.tbl_source_metadata,
3615             properties = dict(
3616                 source_id = self.tbl_source_metadata.c.src_id,
3617                 source = relation(DBSource),
3618                 key_id = self.tbl_source_metadata.c.key_id,
3619                 key = relation(MetadataKey),
3620                 value = self.tbl_source_metadata.c.value))
3621
3622         mapper(VersionCheck, self.tbl_version_check,
3623             properties = dict(
3624                 suite_id = self.tbl_version_check.c.suite,
3625                 suite = relation(Suite, primaryjoin=self.tbl_version_check.c.suite==self.tbl_suite.c.id),
3626                 reference_id = self.tbl_version_check.c.reference,
3627                 reference = relation(Suite, primaryjoin=self.tbl_version_check.c.reference==self.tbl_suite.c.id, lazy='joined')))
3628
3629     ## Connection functions
3630     def __createconn(self):
3631         from config import Config
3632         cnf = Config()
3633         if cnf.has_key("DB::Service"):
3634             connstr = "postgresql://service=%s" % cnf["DB::Service"]
3635         elif cnf.has_key("DB::Host"):
3636             # TCP/IP
3637             connstr = "postgresql://%s" % cnf["DB::Host"]
3638             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
3639                 connstr += ":%s" % cnf["DB::Port"]
3640             connstr += "/%s" % cnf["DB::Name"]
3641         else:
3642             # Unix Socket
3643             connstr = "postgresql:///%s" % cnf["DB::Name"]
3644             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
3645                 connstr += "?port=%s" % cnf["DB::Port"]
3646
3647         engine_args = { 'echo': self.debug }
3648         if cnf.has_key('DB::PoolSize'):
3649             engine_args['pool_size'] = int(cnf['DB::PoolSize'])
3650         if cnf.has_key('DB::MaxOverflow'):
3651             engine_args['max_overflow'] = int(cnf['DB::MaxOverflow'])
3652         if sa_major_version == '0.6' and cnf.has_key('DB::Unicode') and \
3653             cnf['DB::Unicode'] == 'false':
3654             engine_args['use_native_unicode'] = False
3655
3656         # Monkey patch a new dialect in in order to support service= syntax
3657         import sqlalchemy.dialects.postgresql
3658         from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
3659         class PGDialect_psycopg2_dak(PGDialect_psycopg2):
3660             def create_connect_args(self, url):
3661                 if str(url).startswith('postgresql://service='):
3662                     # Eww
3663                     servicename = str(url)[21:]
3664                     return (['service=%s' % servicename], {})
3665                 else:
3666                     return PGDialect_psycopg2.create_connect_args(self, url)
3667
3668         sqlalchemy.dialects.postgresql.base.dialect = PGDialect_psycopg2_dak
3669
3670         self.db_pg   = create_engine(connstr, **engine_args)
3671         self.db_meta = MetaData()
3672         self.db_meta.bind = self.db_pg
3673         self.db_smaker = sessionmaker(bind=self.db_pg,
3674                                       autoflush=True,
3675                                       autocommit=False)
3676
3677         self.__setuptables()
3678         self.__setupmappers()
3679         self.pid = os.getpid()
3680
3681     def session(self, work_mem = 0):
3682         '''
3683         Returns a new session object. If a work_mem parameter is provided a new
3684         transaction is started and the work_mem parameter is set for this
3685         transaction. The work_mem parameter is measured in MB. A default value
3686         will be used if the parameter is not set.
3687         '''
3688         # reinitialize DBConn in new processes
3689         if self.pid != os.getpid():
3690             clear_mappers()
3691             self.__createconn()
3692         session = self.db_smaker()
3693         if work_mem > 0:
3694             session.execute("SET LOCAL work_mem TO '%d MB'" % work_mem)
3695         return session
3696
3697 __all__.append('DBConn')
3698
3699