]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
Improve splitting of Uploaders list.
[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")
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 ################################################################################
1136
1137 class DBConfig(object):
1138     def __init__(self, *args, **kwargs):
1139         pass
1140
1141     def __repr__(self):
1142         return '<DBConfig %s>' % self.name
1143
1144 __all__.append('DBConfig')
1145
1146 ################################################################################
1147
1148 @session_wrapper
1149 def get_or_set_contents_file_id(filename, session=None):
1150     """
1151     Returns database id for given filename.
1152
1153     If no matching file is found, a row is inserted.
1154
1155     @type filename: string
1156     @param filename: The filename
1157     @type session: SQLAlchemy
1158     @param session: Optional SQL session object (a temporary one will be
1159     generated if not supplied).  If not passed, a commit will be performed at
1160     the end of the function, otherwise the caller is responsible for commiting.
1161
1162     @rtype: int
1163     @return: the database id for the given component
1164     """
1165
1166     q = session.query(ContentFilename).filter_by(filename=filename)
1167
1168     try:
1169         ret = q.one().cafilename_id
1170     except NoResultFound:
1171         cf = ContentFilename()
1172         cf.filename = filename
1173         session.add(cf)
1174         session.commit_or_flush()
1175         ret = cf.cafilename_id
1176
1177     return ret
1178
1179 __all__.append('get_or_set_contents_file_id')
1180
1181 @session_wrapper
1182 def get_contents(suite, overridetype, section=None, session=None):
1183     """
1184     Returns contents for a suite / overridetype combination, limiting
1185     to a section if not None.
1186
1187     @type suite: Suite
1188     @param suite: Suite object
1189
1190     @type overridetype: OverrideType
1191     @param overridetype: OverrideType object
1192
1193     @type section: Section
1194     @param section: Optional section object to limit results to
1195
1196     @type session: SQLAlchemy
1197     @param session: Optional SQL session object (a temporary one will be
1198     generated if not supplied)
1199
1200     @rtype: ResultsProxy
1201     @return: ResultsProxy object set up to return tuples of (filename, section,
1202     package, arch_id)
1203     """
1204
1205     # find me all of the contents for a given suite
1206     contents_q = """SELECT (p.path||'/'||n.file) AS fn,
1207                             s.section,
1208                             b.package,
1209                             b.architecture
1210                    FROM content_associations c join content_file_paths p ON (c.filepath=p.id)
1211                    JOIN content_file_names n ON (c.filename=n.id)
1212                    JOIN binaries b ON (b.id=c.binary_pkg)
1213                    JOIN override o ON (o.package=b.package)
1214                    JOIN section s ON (s.id=o.section)
1215                    WHERE o.suite = :suiteid AND o.type = :overridetypeid
1216                    AND b.type=:overridetypename"""
1217
1218     vals = {'suiteid': suite.suite_id,
1219             'overridetypeid': overridetype.overridetype_id,
1220             'overridetypename': overridetype.overridetype}
1221
1222     if section is not None:
1223         contents_q += " AND s.id = :sectionid"
1224         vals['sectionid'] = section.section_id
1225
1226     contents_q += " ORDER BY fn"
1227
1228     return session.execute(contents_q, vals)
1229
1230 __all__.append('get_contents')
1231
1232 ################################################################################
1233
1234 class ContentFilepath(object):
1235     def __init__(self, *args, **kwargs):
1236         pass
1237
1238     def __repr__(self):
1239         return '<ContentFilepath %s>' % self.filepath
1240
1241 __all__.append('ContentFilepath')
1242
1243 @session_wrapper
1244 def get_or_set_contents_path_id(filepath, session=None):
1245     """
1246     Returns database id for given path.
1247
1248     If no matching file is found, a row is inserted.
1249
1250     @type filepath: string
1251     @param filepath: The filepath
1252
1253     @type session: SQLAlchemy
1254     @param session: Optional SQL session object (a temporary one will be
1255     generated if not supplied).  If not passed, a commit will be performed at
1256     the end of the function, otherwise the caller is responsible for commiting.
1257
1258     @rtype: int
1259     @return: the database id for the given path
1260     """
1261
1262     q = session.query(ContentFilepath).filter_by(filepath=filepath)
1263
1264     try:
1265         ret = q.one().cafilepath_id
1266     except NoResultFound:
1267         cf = ContentFilepath()
1268         cf.filepath = filepath
1269         session.add(cf)
1270         session.commit_or_flush()
1271         ret = cf.cafilepath_id
1272
1273     return ret
1274
1275 __all__.append('get_or_set_contents_path_id')
1276
1277 ################################################################################
1278
1279 class ContentAssociation(object):
1280     def __init__(self, *args, **kwargs):
1281         pass
1282
1283     def __repr__(self):
1284         return '<ContentAssociation %s>' % self.ca_id
1285
1286 __all__.append('ContentAssociation')
1287
1288 def insert_content_paths(binary_id, fullpaths, session=None):
1289     """
1290     Make sure given path is associated with given binary id
1291
1292     @type binary_id: int
1293     @param binary_id: the id of the binary
1294     @type fullpaths: list
1295     @param fullpaths: the list of paths of the file being associated with the binary
1296     @type session: SQLAlchemy session
1297     @param session: Optional SQLAlchemy session.  If this is passed, the caller
1298     is responsible for ensuring a transaction has begun and committing the
1299     results or rolling back based on the result code.  If not passed, a commit
1300     will be performed at the end of the function, otherwise the caller is
1301     responsible for commiting.
1302
1303     @return: True upon success
1304     """
1305
1306     privatetrans = False
1307     if session is None:
1308         session = DBConn().session()
1309         privatetrans = True
1310
1311     try:
1312         # Insert paths
1313         def generate_path_dicts():
1314             for fullpath in fullpaths:
1315                 if fullpath.startswith( './' ):
1316                     fullpath = fullpath[2:]
1317
1318                 yield {'filename':fullpath, 'id': binary_id }
1319
1320         for d in generate_path_dicts():
1321             session.execute( "INSERT INTO bin_contents ( file, binary_id ) VALUES ( :filename, :id )",
1322                          d )
1323
1324         session.commit()
1325         if privatetrans:
1326             session.close()
1327         return True
1328
1329     except:
1330         traceback.print_exc()
1331
1332         # Only rollback if we set up the session ourself
1333         if privatetrans:
1334             session.rollback()
1335             session.close()
1336
1337         return False
1338
1339 __all__.append('insert_content_paths')
1340
1341 ################################################################################
1342
1343 class DSCFile(object):
1344     def __init__(self, *args, **kwargs):
1345         pass
1346
1347     def __repr__(self):
1348         return '<DSCFile %s>' % self.dscfile_id
1349
1350 __all__.append('DSCFile')
1351
1352 @session_wrapper
1353 def get_dscfiles(dscfile_id=None, source_id=None, poolfile_id=None, session=None):
1354     """
1355     Returns a list of DSCFiles which may be empty
1356
1357     @type dscfile_id: int (optional)
1358     @param dscfile_id: the dscfile_id of the DSCFiles to find
1359
1360     @type source_id: int (optional)
1361     @param source_id: the source id related to the DSCFiles to find
1362
1363     @type poolfile_id: int (optional)
1364     @param poolfile_id: the poolfile id related to the DSCFiles to find
1365
1366     @rtype: list
1367     @return: Possibly empty list of DSCFiles
1368     """
1369
1370     q = session.query(DSCFile)
1371
1372     if dscfile_id is not None:
1373         q = q.filter_by(dscfile_id=dscfile_id)
1374
1375     if source_id is not None:
1376         q = q.filter_by(source_id=source_id)
1377
1378     if poolfile_id is not None:
1379         q = q.filter_by(poolfile_id=poolfile_id)
1380
1381     return q.all()
1382
1383 __all__.append('get_dscfiles')
1384
1385 ################################################################################
1386
1387 class ExternalOverride(ORMObject):
1388     def __init__(self, *args, **kwargs):
1389         pass
1390
1391     def __repr__(self):
1392         return '<ExternalOverride %s = %s: %s>' % (self.package, self.key, self.value)
1393
1394 __all__.append('ExternalOverride')
1395
1396 ################################################################################
1397
1398 class PoolFile(ORMObject):
1399     def __init__(self, filename = None, location = None, filesize = -1, \
1400         md5sum = None):
1401         self.filename = filename
1402         self.location = location
1403         self.filesize = filesize
1404         self.md5sum = md5sum
1405
1406     @property
1407     def fullpath(self):
1408         return os.path.join(self.location.path, self.filename)
1409
1410     def is_valid(self, filesize = -1, md5sum = None):
1411         return self.filesize == long(filesize) and self.md5sum == md5sum
1412
1413     def properties(self):
1414         return ['filename', 'file_id', 'filesize', 'md5sum', 'sha1sum', \
1415             'sha256sum', 'location', 'source', 'binary', 'last_used']
1416
1417     def not_null_constraints(self):
1418         return ['filename', 'md5sum', 'location']
1419
1420     def identical_to(self, filename):
1421         """
1422         compare size and hash with the given file
1423
1424         @rtype: bool
1425         @return: true if the given file has the same size and hash as this object; false otherwise
1426         """
1427         st = os.stat(filename)
1428         if self.filesize != st.st_size:
1429             return False
1430
1431         f = open(filename, "r")
1432         sha256sum = apt_pkg.sha256sum(f)
1433         if sha256sum != self.sha256sum:
1434             return False
1435
1436         return True
1437
1438 __all__.append('PoolFile')
1439
1440 @session_wrapper
1441 def check_poolfile(filename, filesize, md5sum, location_id, session=None):
1442     """
1443     Returns a tuple:
1444     (ValidFileFound [boolean], PoolFile object or None)
1445
1446     @type filename: string
1447     @param filename: the filename of the file to check against the DB
1448
1449     @type filesize: int
1450     @param filesize: the size of the file to check against the DB
1451
1452     @type md5sum: string
1453     @param md5sum: the md5sum of the file to check against the DB
1454
1455     @type location_id: int
1456     @param location_id: the id of the location to look in
1457
1458     @rtype: tuple
1459     @return: Tuple of length 2.
1460                  - If valid pool file found: (C{True}, C{PoolFile object})
1461                  - If valid pool file not found:
1462                      - (C{False}, C{None}) if no file found
1463                      - (C{False}, C{PoolFile object}) if file found with size/md5sum mismatch
1464     """
1465
1466     poolfile = session.query(Location).get(location_id). \
1467         files.filter_by(filename=filename).first()
1468     valid = False
1469     if poolfile and poolfile.is_valid(filesize = filesize, md5sum = md5sum):
1470         valid = True
1471
1472     return (valid, poolfile)
1473
1474 __all__.append('check_poolfile')
1475
1476 # TODO: the implementation can trivially be inlined at the place where the
1477 # function is called
1478 @session_wrapper
1479 def get_poolfile_by_id(file_id, session=None):
1480     """
1481     Returns a PoolFile objects or None for the given id
1482
1483     @type file_id: int
1484     @param file_id: the id of the file to look for
1485
1486     @rtype: PoolFile or None
1487     @return: either the PoolFile object or None
1488     """
1489
1490     return session.query(PoolFile).get(file_id)
1491
1492 __all__.append('get_poolfile_by_id')
1493
1494 @session_wrapper
1495 def get_poolfile_like_name(filename, session=None):
1496     """
1497     Returns an array of PoolFile objects which are like the given name
1498
1499     @type filename: string
1500     @param filename: the filename of the file to check against the DB
1501
1502     @rtype: array
1503     @return: array of PoolFile objects
1504     """
1505
1506     # TODO: There must be a way of properly using bind parameters with %FOO%
1507     q = session.query(PoolFile).filter(PoolFile.filename.like('%%/%s' % filename))
1508
1509     return q.all()
1510
1511 __all__.append('get_poolfile_like_name')
1512
1513 @session_wrapper
1514 def add_poolfile(filename, datadict, location_id, session=None):
1515     """
1516     Add a new file to the pool
1517
1518     @type filename: string
1519     @param filename: filename
1520
1521     @type datadict: dict
1522     @param datadict: dict with needed data
1523
1524     @type location_id: int
1525     @param location_id: database id of the location
1526
1527     @rtype: PoolFile
1528     @return: the PoolFile object created
1529     """
1530     poolfile = PoolFile()
1531     poolfile.filename = filename
1532     poolfile.filesize = datadict["size"]
1533     poolfile.md5sum = datadict["md5sum"]
1534     poolfile.sha1sum = datadict["sha1sum"]
1535     poolfile.sha256sum = datadict["sha256sum"]
1536     poolfile.location_id = location_id
1537
1538     session.add(poolfile)
1539     # Flush to get a file id (NB: This is not a commit)
1540     session.flush()
1541
1542     return poolfile
1543
1544 __all__.append('add_poolfile')
1545
1546 ################################################################################
1547
1548 class Fingerprint(ORMObject):
1549     def __init__(self, fingerprint = None):
1550         self.fingerprint = fingerprint
1551
1552     def properties(self):
1553         return ['fingerprint', 'fingerprint_id', 'keyring', 'uid', \
1554             'binary_reject']
1555
1556     def not_null_constraints(self):
1557         return ['fingerprint']
1558
1559 __all__.append('Fingerprint')
1560
1561 @session_wrapper
1562 def get_fingerprint(fpr, session=None):
1563     """
1564     Returns Fingerprint object for given fpr.
1565
1566     @type fpr: string
1567     @param fpr: The fpr to find / add
1568
1569     @type session: SQLAlchemy
1570     @param session: Optional SQL session object (a temporary one will be
1571     generated if not supplied).
1572
1573     @rtype: Fingerprint
1574     @return: the Fingerprint object for the given fpr or None
1575     """
1576
1577     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1578
1579     try:
1580         ret = q.one()
1581     except NoResultFound:
1582         ret = None
1583
1584     return ret
1585
1586 __all__.append('get_fingerprint')
1587
1588 @session_wrapper
1589 def get_or_set_fingerprint(fpr, session=None):
1590     """
1591     Returns Fingerprint object for given fpr.
1592
1593     If no matching fpr is found, a row is inserted.
1594
1595     @type fpr: string
1596     @param fpr: The fpr to find / add
1597
1598     @type session: SQLAlchemy
1599     @param session: Optional SQL session object (a temporary one will be
1600     generated if not supplied).  If not passed, a commit will be performed at
1601     the end of the function, otherwise the caller is responsible for commiting.
1602     A flush will be performed either way.
1603
1604     @rtype: Fingerprint
1605     @return: the Fingerprint object for the given fpr
1606     """
1607
1608     q = session.query(Fingerprint).filter_by(fingerprint=fpr)
1609
1610     try:
1611         ret = q.one()
1612     except NoResultFound:
1613         fingerprint = Fingerprint()
1614         fingerprint.fingerprint = fpr
1615         session.add(fingerprint)
1616         session.commit_or_flush()
1617         ret = fingerprint
1618
1619     return ret
1620
1621 __all__.append('get_or_set_fingerprint')
1622
1623 ################################################################################
1624
1625 # Helper routine for Keyring class
1626 def get_ldap_name(entry):
1627     name = []
1628     for k in ["cn", "mn", "sn"]:
1629         ret = entry.get(k)
1630         if ret and ret[0] != "" and ret[0] != "-":
1631             name.append(ret[0])
1632     return " ".join(name)
1633
1634 ################################################################################
1635
1636 class Keyring(object):
1637     gpg_invocation = "gpg --no-default-keyring --keyring %s" +\
1638                      " --with-colons --fingerprint --fingerprint"
1639
1640     keys = {}
1641     fpr_lookup = {}
1642
1643     def __init__(self, *args, **kwargs):
1644         pass
1645
1646     def __repr__(self):
1647         return '<Keyring %s>' % self.keyring_name
1648
1649     def de_escape_gpg_str(self, txt):
1650         esclist = re.split(r'(\\x..)', txt)
1651         for x in range(1,len(esclist),2):
1652             esclist[x] = "%c" % (int(esclist[x][2:],16))
1653         return "".join(esclist)
1654
1655     def parse_address(self, uid):
1656         """parses uid and returns a tuple of real name and email address"""
1657         import email.Utils
1658         (name, address) = email.Utils.parseaddr(uid)
1659         name = re.sub(r"\s*[(].*[)]", "", name)
1660         name = self.de_escape_gpg_str(name)
1661         if name == "":
1662             name = uid
1663         return (name, address)
1664
1665     def load_keys(self, keyring):
1666         if not self.keyring_id:
1667             raise Exception('Must be initialized with database information')
1668
1669         k = os.popen(self.gpg_invocation % keyring, "r")
1670         key = None
1671         signingkey = False
1672
1673         for line in k.xreadlines():
1674             field = line.split(":")
1675             if field[0] == "pub":
1676                 key = field[4]
1677                 self.keys[key] = {}
1678                 (name, addr) = self.parse_address(field[9])
1679                 if "@" in addr:
1680                     self.keys[key]["email"] = addr
1681                     self.keys[key]["name"] = name
1682                 self.keys[key]["fingerprints"] = []
1683                 signingkey = True
1684             elif key and field[0] == "sub" and len(field) >= 12:
1685                 signingkey = ("s" in field[11])
1686             elif key and field[0] == "uid":
1687                 (name, addr) = self.parse_address(field[9])
1688                 if "email" not in self.keys[key] and "@" in addr:
1689                     self.keys[key]["email"] = addr
1690                     self.keys[key]["name"] = name
1691             elif signingkey and field[0] == "fpr":
1692                 self.keys[key]["fingerprints"].append(field[9])
1693                 self.fpr_lookup[field[9]] = key
1694
1695     def import_users_from_ldap(self, session):
1696         import ldap
1697         cnf = Config()
1698
1699         LDAPDn = cnf["Import-LDAP-Fingerprints::LDAPDn"]
1700         LDAPServer = cnf["Import-LDAP-Fingerprints::LDAPServer"]
1701
1702         l = ldap.open(LDAPServer)
1703         l.simple_bind_s("","")
1704         Attrs = l.search_s(LDAPDn, ldap.SCOPE_ONELEVEL,
1705                "(&(keyfingerprint=*)(gidnumber=%s))" % (cnf["Import-Users-From-Passwd::ValidGID"]),
1706                ["uid", "keyfingerprint", "cn", "mn", "sn"])
1707
1708         ldap_fin_uid_id = {}
1709
1710         byuid = {}
1711         byname = {}
1712
1713         for i in Attrs:
1714             entry = i[1]
1715             uid = entry["uid"][0]
1716             name = get_ldap_name(entry)
1717             fingerprints = entry["keyFingerPrint"]
1718             keyid = None
1719             for f in fingerprints:
1720                 key = self.fpr_lookup.get(f, None)
1721                 if key not in self.keys:
1722                     continue
1723                 self.keys[key]["uid"] = uid
1724
1725                 if keyid != None:
1726                     continue
1727                 keyid = get_or_set_uid(uid, session).uid_id
1728                 byuid[keyid] = (uid, name)
1729                 byname[uid] = (keyid, name)
1730
1731         return (byname, byuid)
1732
1733     def generate_users_from_keyring(self, format, session):
1734         byuid = {}
1735         byname = {}
1736         any_invalid = False
1737         for x in self.keys.keys():
1738             if "email" not in self.keys[x]:
1739                 any_invalid = True
1740                 self.keys[x]["uid"] = format % "invalid-uid"
1741             else:
1742                 uid = format % self.keys[x]["email"]
1743                 keyid = get_or_set_uid(uid, session).uid_id
1744                 byuid[keyid] = (uid, self.keys[x]["name"])
1745                 byname[uid] = (keyid, self.keys[x]["name"])
1746                 self.keys[x]["uid"] = uid
1747
1748         if any_invalid:
1749             uid = format % "invalid-uid"
1750             keyid = get_or_set_uid(uid, session).uid_id
1751             byuid[keyid] = (uid, "ungeneratable user id")
1752             byname[uid] = (keyid, "ungeneratable user id")
1753
1754         return (byname, byuid)
1755
1756 __all__.append('Keyring')
1757
1758 @session_wrapper
1759 def get_keyring(keyring, session=None):
1760     """
1761     If C{keyring} does not have an entry in the C{keyrings} table yet, return None
1762     If C{keyring} already has an entry, simply return the existing Keyring
1763
1764     @type keyring: string
1765     @param keyring: the keyring name
1766
1767     @rtype: Keyring
1768     @return: the Keyring object for this keyring
1769     """
1770
1771     q = session.query(Keyring).filter_by(keyring_name=keyring)
1772
1773     try:
1774         return q.one()
1775     except NoResultFound:
1776         return None
1777
1778 __all__.append('get_keyring')
1779
1780 ################################################################################
1781
1782 class KeyringACLMap(object):
1783     def __init__(self, *args, **kwargs):
1784         pass
1785
1786     def __repr__(self):
1787         return '<KeyringACLMap %s>' % self.keyring_acl_map_id
1788
1789 __all__.append('KeyringACLMap')
1790
1791 ################################################################################
1792
1793 class DBChange(object):
1794     def __init__(self, *args, **kwargs):
1795         pass
1796
1797     def __repr__(self):
1798         return '<DBChange %s>' % self.changesname
1799
1800     def clean_from_queue(self):
1801         session = DBConn().session().object_session(self)
1802
1803         # Remove changes_pool_files entries
1804         self.poolfiles = []
1805
1806         # Remove changes_pending_files references
1807         self.files = []
1808
1809         # Clear out of queue
1810         self.in_queue = None
1811         self.approved_for_id = None
1812
1813 __all__.append('DBChange')
1814
1815 @session_wrapper
1816 def get_dbchange(filename, session=None):
1817     """
1818     returns DBChange object for given C{filename}.
1819
1820     @type filename: string
1821     @param filename: the name of the file
1822
1823     @type session: Session
1824     @param session: Optional SQLA session object (a temporary one will be
1825     generated if not supplied)
1826
1827     @rtype: DBChange
1828     @return:  DBChange object for the given filename (C{None} if not present)
1829
1830     """
1831     q = session.query(DBChange).filter_by(changesname=filename)
1832
1833     try:
1834         return q.one()
1835     except NoResultFound:
1836         return None
1837
1838 __all__.append('get_dbchange')
1839
1840 ################################################################################
1841
1842 class Location(ORMObject):
1843     def __init__(self, path = None, component = None):
1844         self.path = path
1845         self.component = component
1846         # the column 'type' should go away, see comment at mapper
1847         self.archive_type = 'pool'
1848
1849     def properties(self):
1850         return ['path', 'location_id', 'archive_type', 'component', \
1851             'files_count']
1852
1853     def not_null_constraints(self):
1854         return ['path', 'archive_type']
1855
1856 __all__.append('Location')
1857
1858 @session_wrapper
1859 def get_location(location, component=None, archive=None, session=None):
1860     """
1861     Returns Location object for the given combination of location, component
1862     and archive
1863
1864     @type location: string
1865     @param location: the path of the location, e.g. I{/srv/ftp-master.debian.org/ftp/pool/}
1866
1867     @type component: string
1868     @param component: the component name (if None, no restriction applied)
1869
1870     @type archive: string
1871     @param archive: the archive name (if None, no restriction applied)
1872
1873     @rtype: Location / None
1874     @return: Either a Location object or None if one can't be found
1875     """
1876
1877     q = session.query(Location).filter_by(path=location)
1878
1879     if archive is not None:
1880         q = q.join(Archive).filter_by(archive_name=archive)
1881
1882     if component is not None:
1883         q = q.join(Component).filter_by(component_name=component)
1884
1885     try:
1886         return q.one()
1887     except NoResultFound:
1888         return None
1889
1890 __all__.append('get_location')
1891
1892 ################################################################################
1893
1894 class Maintainer(ORMObject):
1895     def __init__(self, name = None):
1896         self.name = name
1897
1898     def properties(self):
1899         return ['name', 'maintainer_id']
1900
1901     def not_null_constraints(self):
1902         return ['name']
1903
1904     def get_split_maintainer(self):
1905         if not hasattr(self, 'name') or self.name is None:
1906             return ('', '', '', '')
1907
1908         return fix_maintainer(self.name.strip())
1909
1910 __all__.append('Maintainer')
1911
1912 @session_wrapper
1913 def get_or_set_maintainer(name, session=None):
1914     """
1915     Returns Maintainer object for given maintainer name.
1916
1917     If no matching maintainer name is found, a row is inserted.
1918
1919     @type name: string
1920     @param name: The maintainer name to add
1921
1922     @type session: SQLAlchemy
1923     @param session: Optional SQL session object (a temporary one will be
1924     generated if not supplied).  If not passed, a commit will be performed at
1925     the end of the function, otherwise the caller is responsible for commiting.
1926     A flush will be performed either way.
1927
1928     @rtype: Maintainer
1929     @return: the Maintainer object for the given maintainer
1930     """
1931
1932     q = session.query(Maintainer).filter_by(name=name)
1933     try:
1934         ret = q.one()
1935     except NoResultFound:
1936         maintainer = Maintainer()
1937         maintainer.name = name
1938         session.add(maintainer)
1939         session.commit_or_flush()
1940         ret = maintainer
1941
1942     return ret
1943
1944 __all__.append('get_or_set_maintainer')
1945
1946 @session_wrapper
1947 def get_maintainer(maintainer_id, session=None):
1948     """
1949     Return the name of the maintainer behind C{maintainer_id} or None if that
1950     maintainer_id is invalid.
1951
1952     @type maintainer_id: int
1953     @param maintainer_id: the id of the maintainer
1954
1955     @rtype: Maintainer
1956     @return: the Maintainer with this C{maintainer_id}
1957     """
1958
1959     return session.query(Maintainer).get(maintainer_id)
1960
1961 __all__.append('get_maintainer')
1962
1963 ################################################################################
1964
1965 class NewComment(object):
1966     def __init__(self, *args, **kwargs):
1967         pass
1968
1969     def __repr__(self):
1970         return '''<NewComment for '%s %s' (%s)>''' % (self.package, self.version, self.comment_id)
1971
1972 __all__.append('NewComment')
1973
1974 @session_wrapper
1975 def has_new_comment(package, version, session=None):
1976     """
1977     Returns true if the given combination of C{package}, C{version} has a comment.
1978
1979     @type package: string
1980     @param package: name of the package
1981
1982     @type version: string
1983     @param version: package version
1984
1985     @type session: Session
1986     @param session: Optional SQLA session object (a temporary one will be
1987     generated if not supplied)
1988
1989     @rtype: boolean
1990     @return: true/false
1991     """
1992
1993     q = session.query(NewComment)
1994     q = q.filter_by(package=package)
1995     q = q.filter_by(version=version)
1996
1997     return bool(q.count() > 0)
1998
1999 __all__.append('has_new_comment')
2000
2001 @session_wrapper
2002 def get_new_comments(package=None, version=None, comment_id=None, session=None):
2003     """
2004     Returns (possibly empty) list of NewComment objects for the given
2005     parameters
2006
2007     @type package: string (optional)
2008     @param package: name of the package
2009
2010     @type version: string (optional)
2011     @param version: package version
2012
2013     @type comment_id: int (optional)
2014     @param comment_id: An id of a comment
2015
2016     @type session: Session
2017     @param session: Optional SQLA session object (a temporary one will be
2018     generated if not supplied)
2019
2020     @rtype: list
2021     @return: A (possibly empty) list of NewComment objects will be returned
2022     """
2023
2024     q = session.query(NewComment)
2025     if package is not None: q = q.filter_by(package=package)
2026     if version is not None: q = q.filter_by(version=version)
2027     if comment_id is not None: q = q.filter_by(comment_id=comment_id)
2028
2029     return q.all()
2030
2031 __all__.append('get_new_comments')
2032
2033 ################################################################################
2034
2035 class Override(ORMObject):
2036     def __init__(self, package = None, suite = None, component = None, overridetype = None, \
2037         section = None, priority = None):
2038         self.package = package
2039         self.suite = suite
2040         self.component = component
2041         self.overridetype = overridetype
2042         self.section = section
2043         self.priority = priority
2044
2045     def properties(self):
2046         return ['package', 'suite', 'component', 'overridetype', 'section', \
2047             'priority']
2048
2049     def not_null_constraints(self):
2050         return ['package', 'suite', 'component', 'overridetype', 'section']
2051
2052 __all__.append('Override')
2053
2054 @session_wrapper
2055 def get_override(package, suite=None, component=None, overridetype=None, session=None):
2056     """
2057     Returns Override object for the given parameters
2058
2059     @type package: string
2060     @param package: The name of the package
2061
2062     @type suite: string, list or None
2063     @param suite: The name of the suite (or suites if a list) to limit to.  If
2064                   None, don't limit.  Defaults to None.
2065
2066     @type component: string, list or None
2067     @param component: The name of the component (or components if a list) to
2068                       limit to.  If None, don't limit.  Defaults to None.
2069
2070     @type overridetype: string, list or None
2071     @param overridetype: The name of the overridetype (or overridetypes if a list) to
2072                          limit to.  If None, don't limit.  Defaults to None.
2073
2074     @type session: Session
2075     @param session: Optional SQLA session object (a temporary one will be
2076     generated if not supplied)
2077
2078     @rtype: list
2079     @return: A (possibly empty) list of Override objects will be returned
2080     """
2081
2082     q = session.query(Override)
2083     q = q.filter_by(package=package)
2084
2085     if suite is not None:
2086         if not isinstance(suite, list): suite = [suite]
2087         q = q.join(Suite).filter(Suite.suite_name.in_(suite))
2088
2089     if component is not None:
2090         if not isinstance(component, list): component = [component]
2091         q = q.join(Component).filter(Component.component_name.in_(component))
2092
2093     if overridetype is not None:
2094         if not isinstance(overridetype, list): overridetype = [overridetype]
2095         q = q.join(OverrideType).filter(OverrideType.overridetype.in_(overridetype))
2096
2097     return q.all()
2098
2099 __all__.append('get_override')
2100
2101
2102 ################################################################################
2103
2104 class OverrideType(ORMObject):
2105     def __init__(self, overridetype = None):
2106         self.overridetype = overridetype
2107
2108     def properties(self):
2109         return ['overridetype', 'overridetype_id', 'overrides_count']
2110
2111     def not_null_constraints(self):
2112         return ['overridetype']
2113
2114 __all__.append('OverrideType')
2115
2116 @session_wrapper
2117 def get_override_type(override_type, session=None):
2118     """
2119     Returns OverrideType object for given C{override type}.
2120
2121     @type override_type: string
2122     @param override_type: The name of the override type
2123
2124     @type session: Session
2125     @param session: Optional SQLA session object (a temporary one will be
2126     generated if not supplied)
2127
2128     @rtype: int
2129     @return: the database id for the given override type
2130     """
2131
2132     q = session.query(OverrideType).filter_by(overridetype=override_type)
2133
2134     try:
2135         return q.one()
2136     except NoResultFound:
2137         return None
2138
2139 __all__.append('get_override_type')
2140
2141 ################################################################################
2142
2143 class PolicyQueue(object):
2144     def __init__(self, *args, **kwargs):
2145         pass
2146
2147     def __repr__(self):
2148         return '<PolicyQueue %s>' % self.queue_name
2149
2150 __all__.append('PolicyQueue')
2151
2152 @session_wrapper
2153 def get_policy_queue(queuename, session=None):
2154     """
2155     Returns PolicyQueue object for given C{queue name}
2156
2157     @type queuename: string
2158     @param queuename: The name of the queue
2159
2160     @type session: Session
2161     @param session: Optional SQLA session object (a temporary one will be
2162     generated if not supplied)
2163
2164     @rtype: PolicyQueue
2165     @return: PolicyQueue object for the given queue
2166     """
2167
2168     q = session.query(PolicyQueue).filter_by(queue_name=queuename)
2169
2170     try:
2171         return q.one()
2172     except NoResultFound:
2173         return None
2174
2175 __all__.append('get_policy_queue')
2176
2177 @session_wrapper
2178 def get_policy_queue_from_path(pathname, session=None):
2179     """
2180     Returns PolicyQueue object for given C{path name}
2181
2182     @type queuename: string
2183     @param queuename: The path
2184
2185     @type session: Session
2186     @param session: Optional SQLA session object (a temporary one will be
2187     generated if not supplied)
2188
2189     @rtype: PolicyQueue
2190     @return: PolicyQueue object for the given queue
2191     """
2192
2193     q = session.query(PolicyQueue).filter_by(path=pathname)
2194
2195     try:
2196         return q.one()
2197     except NoResultFound:
2198         return None
2199
2200 __all__.append('get_policy_queue_from_path')
2201
2202 ################################################################################
2203
2204 class Priority(ORMObject):
2205     def __init__(self, priority = None, level = None):
2206         self.priority = priority
2207         self.level = level
2208
2209     def properties(self):
2210         return ['priority', 'priority_id', 'level', 'overrides_count']
2211
2212     def not_null_constraints(self):
2213         return ['priority', 'level']
2214
2215     def __eq__(self, val):
2216         if isinstance(val, str):
2217             return (self.priority == val)
2218         # This signals to use the normal comparison operator
2219         return NotImplemented
2220
2221     def __ne__(self, val):
2222         if isinstance(val, str):
2223             return (self.priority != val)
2224         # This signals to use the normal comparison operator
2225         return NotImplemented
2226
2227 __all__.append('Priority')
2228
2229 @session_wrapper
2230 def get_priority(priority, session=None):
2231     """
2232     Returns Priority object for given C{priority name}.
2233
2234     @type priority: string
2235     @param priority: The name of the priority
2236
2237     @type session: Session
2238     @param session: Optional SQLA session object (a temporary one will be
2239     generated if not supplied)
2240
2241     @rtype: Priority
2242     @return: Priority object for the given priority
2243     """
2244
2245     q = session.query(Priority).filter_by(priority=priority)
2246
2247     try:
2248         return q.one()
2249     except NoResultFound:
2250         return None
2251
2252 __all__.append('get_priority')
2253
2254 @session_wrapper
2255 def get_priorities(session=None):
2256     """
2257     Returns dictionary of priority names -> id mappings
2258
2259     @type session: Session
2260     @param session: Optional SQL session object (a temporary one will be
2261     generated if not supplied)
2262
2263     @rtype: dictionary
2264     @return: dictionary of priority names -> id mappings
2265     """
2266
2267     ret = {}
2268     q = session.query(Priority)
2269     for x in q.all():
2270         ret[x.priority] = x.priority_id
2271
2272     return ret
2273
2274 __all__.append('get_priorities')
2275
2276 ################################################################################
2277
2278 class Section(ORMObject):
2279     def __init__(self, section = None):
2280         self.section = section
2281
2282     def properties(self):
2283         return ['section', 'section_id', 'overrides_count']
2284
2285     def not_null_constraints(self):
2286         return ['section']
2287
2288     def __eq__(self, val):
2289         if isinstance(val, str):
2290             return (self.section == val)
2291         # This signals to use the normal comparison operator
2292         return NotImplemented
2293
2294     def __ne__(self, val):
2295         if isinstance(val, str):
2296             return (self.section != val)
2297         # This signals to use the normal comparison operator
2298         return NotImplemented
2299
2300 __all__.append('Section')
2301
2302 @session_wrapper
2303 def get_section(section, session=None):
2304     """
2305     Returns Section object for given C{section name}.
2306
2307     @type section: string
2308     @param section: The name of the section
2309
2310     @type session: Session
2311     @param session: Optional SQLA session object (a temporary one will be
2312     generated if not supplied)
2313
2314     @rtype: Section
2315     @return: Section object for the given section name
2316     """
2317
2318     q = session.query(Section).filter_by(section=section)
2319
2320     try:
2321         return q.one()
2322     except NoResultFound:
2323         return None
2324
2325 __all__.append('get_section')
2326
2327 @session_wrapper
2328 def get_sections(session=None):
2329     """
2330     Returns dictionary of section names -> id mappings
2331
2332     @type session: Session
2333     @param session: Optional SQL session object (a temporary one will be
2334     generated if not supplied)
2335
2336     @rtype: dictionary
2337     @return: dictionary of section names -> id mappings
2338     """
2339
2340     ret = {}
2341     q = session.query(Section)
2342     for x in q.all():
2343         ret[x.section] = x.section_id
2344
2345     return ret
2346
2347 __all__.append('get_sections')
2348
2349 ################################################################################
2350
2351 class SrcContents(ORMObject):
2352     def __init__(self, file = None, source = None):
2353         self.file = file
2354         self.source = source
2355
2356     def properties(self):
2357         return ['file', 'source']
2358
2359 __all__.append('SrcContents')
2360
2361 ################################################################################
2362
2363 from debian.debfile import Deb822
2364
2365 # Temporary Deb822 subclass to fix bugs with : handling; see #597249
2366 class Dak822(Deb822):
2367     def _internal_parser(self, sequence, fields=None):
2368         # The key is non-whitespace, non-colon characters before any colon.
2369         key_part = r"^(?P<key>[^: \t\n\r\f\v]+)\s*:\s*"
2370         single = re.compile(key_part + r"(?P<data>\S.*?)\s*$")
2371         multi = re.compile(key_part + r"$")
2372         multidata = re.compile(r"^\s(?P<data>.+?)\s*$")
2373
2374         wanted_field = lambda f: fields is None or f in fields
2375
2376         if isinstance(sequence, basestring):
2377             sequence = sequence.splitlines()
2378
2379         curkey = None
2380         content = ""
2381         for line in self.gpg_stripped_paragraph(sequence):
2382             m = single.match(line)
2383             if m:
2384                 if curkey:
2385                     self[curkey] = content
2386
2387                 if not wanted_field(m.group('key')):
2388                     curkey = None
2389                     continue
2390
2391                 curkey = m.group('key')
2392                 content = m.group('data')
2393                 continue
2394
2395             m = multi.match(line)
2396             if m:
2397                 if curkey:
2398                     self[curkey] = content
2399
2400                 if not wanted_field(m.group('key')):
2401                     curkey = None
2402                     continue
2403
2404                 curkey = m.group('key')
2405                 content = ""
2406                 continue
2407
2408             m = multidata.match(line)
2409             if m:
2410                 content += '\n' + line # XXX not m.group('data')?
2411                 continue
2412
2413         if curkey:
2414             self[curkey] = content
2415
2416
2417 class DBSource(ORMObject):
2418     def __init__(self, source = None, version = None, maintainer = None, \
2419         changedby = None, poolfile = None, install_date = None):
2420         self.source = source
2421         self.version = version
2422         self.maintainer = maintainer
2423         self.changedby = changedby
2424         self.poolfile = poolfile
2425         self.install_date = install_date
2426
2427     @property
2428     def pkid(self):
2429         return self.source_id
2430
2431     def properties(self):
2432         return ['source', 'source_id', 'maintainer', 'changedby', \
2433             'fingerprint', 'poolfile', 'version', 'suites_count', \
2434             'install_date', 'binaries_count', 'uploaders_count']
2435
2436     def not_null_constraints(self):
2437         return ['source', 'version', 'install_date', 'maintainer', \
2438             'changedby', 'poolfile', 'install_date']
2439
2440     def read_control_fields(self):
2441         '''
2442         Reads the control information from a dsc
2443
2444         @rtype: tuple
2445         @return: fields is the dsc information in a dictionary form
2446         '''
2447         fullpath = self.poolfile.fullpath
2448         fields = Dak822(open(self.poolfile.fullpath, 'r'))
2449         return fields
2450
2451     metadata = association_proxy('key', 'value')
2452
2453     def scan_contents(self):
2454         '''
2455         Returns a set of names for non directories. The path names are
2456         normalized after converting them from either utf-8 or iso8859-1
2457         encoding.
2458         '''
2459         fullpath = self.poolfile.fullpath
2460         from daklib.contents import UnpackedSource
2461         unpacked = UnpackedSource(fullpath)
2462         fileset = set()
2463         for name in unpacked.get_all_filenames():
2464             # enforce proper utf-8 encoding
2465             try:
2466                 name.decode('utf-8')
2467             except UnicodeDecodeError:
2468                 name = name.decode('iso8859-1').encode('utf-8')
2469             fileset.add(name)
2470         return fileset
2471
2472 __all__.append('DBSource')
2473
2474 @session_wrapper
2475 def source_exists(source, source_version, suites = ["any"], session=None):
2476     """
2477     Ensure that source exists somewhere in the archive for the binary
2478     upload being processed.
2479       1. exact match     => 1.0-3
2480       2. bin-only NMU    => 1.0-3+b1 , 1.0-3.1+b1
2481
2482     @type source: string
2483     @param source: source name
2484
2485     @type source_version: string
2486     @param source_version: expected source version
2487
2488     @type suites: list
2489     @param suites: list of suites to check in, default I{any}
2490
2491     @type session: Session
2492     @param session: Optional SQLA session object (a temporary one will be
2493     generated if not supplied)
2494
2495     @rtype: int
2496     @return: returns 1 if a source with expected version is found, otherwise 0
2497
2498     """
2499
2500     cnf = Config()
2501     ret = True
2502
2503     from daklib.regexes import re_bin_only_nmu
2504     orig_source_version = re_bin_only_nmu.sub('', source_version)
2505
2506     for suite in suites:
2507         q = session.query(DBSource).filter_by(source=source). \
2508             filter(DBSource.version.in_([source_version, orig_source_version]))
2509         if suite != "any":
2510             # source must exist in 'suite' or a suite that is enhanced by 'suite'
2511             s = get_suite(suite, session)
2512             enhances_vcs = session.query(VersionCheck).filter(VersionCheck.suite==s).filter_by(check='Enhances')
2513             considered_suites = [ vc.reference for vc in enhances_vcs ]
2514             considered_suites.append(s)
2515
2516             q = q.filter(DBSource.suites.any(Suite.suite_id.in_([s.suite_id for s in considered_suites])))
2517
2518         if q.count() > 0:
2519             continue
2520
2521         # No source found so return not ok
2522         ret = False
2523
2524     return ret
2525
2526 __all__.append('source_exists')
2527
2528 @session_wrapper
2529 def get_suites_source_in(source, session=None):
2530     """
2531     Returns list of Suite objects which given C{source} name is in
2532
2533     @type source: str
2534     @param source: DBSource package name to search for
2535
2536     @rtype: list
2537     @return: list of Suite objects for the given source
2538     """
2539
2540     return session.query(Suite).filter(Suite.sources.any(source=source)).all()
2541
2542 __all__.append('get_suites_source_in')
2543
2544 @session_wrapper
2545 def get_sources_from_name(source, version=None, dm_upload_allowed=None, session=None):
2546     """
2547     Returns list of DBSource objects for given C{source} name and other parameters
2548
2549     @type source: str
2550     @param source: DBSource package name to search for
2551
2552     @type version: str or None
2553     @param version: DBSource version name to search for or None if not applicable
2554
2555     @type dm_upload_allowed: bool
2556     @param dm_upload_allowed: If None, no effect.  If True or False, only
2557     return packages with that dm_upload_allowed setting
2558
2559     @type session: Session
2560     @param session: Optional SQL session object (a temporary one will be
2561     generated if not supplied)
2562
2563     @rtype: list
2564     @return: list of DBSource objects for the given name (may be empty)
2565     """
2566
2567     q = session.query(DBSource).filter_by(source=source)
2568
2569     if version is not None:
2570         q = q.filter_by(version=version)
2571
2572     if dm_upload_allowed is not None:
2573         q = q.filter_by(dm_upload_allowed=dm_upload_allowed)
2574
2575     return q.all()
2576
2577 __all__.append('get_sources_from_name')
2578
2579 # FIXME: This function fails badly if it finds more than 1 source package and
2580 # its implementation is trivial enough to be inlined.
2581 @session_wrapper
2582 def get_source_in_suite(source, suite, session=None):
2583     """
2584     Returns a DBSource object for a combination of C{source} and C{suite}.
2585
2586       - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2587       - B{suite} - a suite name, eg. I{unstable}
2588
2589     @type source: string
2590     @param source: source package name
2591
2592     @type suite: string
2593     @param suite: the suite name
2594
2595     @rtype: string
2596     @return: the version for I{source} in I{suite}
2597
2598     """
2599
2600     q = get_suite(suite, session).get_sources(source)
2601     try:
2602         return q.one()
2603     except NoResultFound:
2604         return None
2605
2606 __all__.append('get_source_in_suite')
2607
2608 @session_wrapper
2609 def import_metadata_into_db(obj, session=None):
2610     """
2611     This routine works on either DBBinary or DBSource objects and imports
2612     their metadata into the database
2613     """
2614     fields = obj.read_control_fields()
2615     for k in fields.keys():
2616         try:
2617             # Try raw ASCII
2618             val = str(fields[k])
2619         except UnicodeEncodeError:
2620             # Fall back to UTF-8
2621             try:
2622                 val = fields[k].encode('utf-8')
2623             except UnicodeEncodeError:
2624                 # Finally try iso8859-1
2625                 val = fields[k].encode('iso8859-1')
2626                 # Otherwise we allow the exception to percolate up and we cause
2627                 # a reject as someone is playing silly buggers
2628
2629         obj.metadata[get_or_set_metadatakey(k, session)] = val
2630
2631     session.commit_or_flush()
2632
2633 __all__.append('import_metadata_into_db')
2634
2635
2636 ################################################################################
2637
2638 def split_uploaders(uploaders_list):
2639     '''
2640     Split the Uploaders field into the individual uploaders and yield each of
2641     them. Beware: email addresses might contain commas.
2642     '''
2643     import re
2644     for uploader in re.sub(">[ ]*,", ">\t", uploaders_list).split("\t"):
2645         yield uploader.strip()
2646
2647 @session_wrapper
2648 def add_dsc_to_db(u, filename, session=None):
2649     entry = u.pkg.files[filename]
2650     source = DBSource()
2651     pfs = []
2652
2653     source.source = u.pkg.dsc["source"]
2654     source.version = u.pkg.dsc["version"] # NB: not files[file]["version"], that has no epoch
2655     source.maintainer_id = get_or_set_maintainer(u.pkg.dsc["maintainer"], session).maintainer_id
2656     # If Changed-By isn't available, fall back to maintainer
2657     if u.pkg.changes.has_key("changed-by"):
2658         source.changedby_id = get_or_set_maintainer(u.pkg.changes["changed-by"], session).maintainer_id
2659     else:
2660         source.changedby_id = get_or_set_maintainer(u.pkg.dsc["maintainer"], session).maintainer_id
2661     source.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2662     source.install_date = datetime.now().date()
2663
2664     dsc_component = entry["component"]
2665     dsc_location_id = entry["location id"]
2666
2667     source.dm_upload_allowed = (u.pkg.dsc.get("dm-upload-allowed", '') == "yes")
2668
2669     # Set up a new poolfile if necessary
2670     if not entry.has_key("files id") or not entry["files id"]:
2671         filename = entry["pool name"] + filename
2672         poolfile = add_poolfile(filename, entry, dsc_location_id, session)
2673         session.flush()
2674         pfs.append(poolfile)
2675         entry["files id"] = poolfile.file_id
2676
2677     source.poolfile_id = entry["files id"]
2678     session.add(source)
2679
2680     suite_names = u.pkg.changes["distribution"].keys()
2681     source.suites = session.query(Suite). \
2682         filter(Suite.suite_name.in_(suite_names)).all()
2683
2684     # Add the source files to the DB (files and dsc_files)
2685     dscfile = DSCFile()
2686     dscfile.source_id = source.source_id
2687     dscfile.poolfile_id = entry["files id"]
2688     session.add(dscfile)
2689
2690     for dsc_file, dentry in u.pkg.dsc_files.items():
2691         df = DSCFile()
2692         df.source_id = source.source_id
2693
2694         # If the .orig tarball is already in the pool, it's
2695         # files id is stored in dsc_files by check_dsc().
2696         files_id = dentry.get("files id", None)
2697
2698         # Find the entry in the files hash
2699         # TODO: Bail out here properly
2700         dfentry = None
2701         for f, e in u.pkg.files.items():
2702             if f == dsc_file:
2703                 dfentry = e
2704                 break
2705
2706         if files_id is None:
2707             filename = dfentry["pool name"] + dsc_file
2708
2709             (found, obj) = check_poolfile(filename, dentry["size"], dentry["md5sum"], dsc_location_id)
2710             # FIXME: needs to check for -1/-2 and or handle exception
2711             if found and obj is not None:
2712                 files_id = obj.file_id
2713                 pfs.append(obj)
2714
2715             # If still not found, add it
2716             if files_id is None:
2717                 # HACK: Force sha1sum etc into dentry
2718                 dentry["sha1sum"] = dfentry["sha1sum"]
2719                 dentry["sha256sum"] = dfentry["sha256sum"]
2720                 poolfile = add_poolfile(filename, dentry, dsc_location_id, session)
2721                 pfs.append(poolfile)
2722                 files_id = poolfile.file_id
2723         else:
2724             poolfile = get_poolfile_by_id(files_id, session)
2725             if poolfile is None:
2726                 utils.fubar("INTERNAL ERROR. Found no poolfile with id %d" % files_id)
2727             pfs.append(poolfile)
2728
2729         df.poolfile_id = files_id
2730         session.add(df)
2731
2732     # Add the src_uploaders to the DB
2733     session.flush()
2734     session.refresh(source)
2735     source.uploaders = [source.maintainer]
2736     if u.pkg.dsc.has_key("uploaders"):
2737         for up in split_uploaders(u.pkg.dsc["uploaders"]):
2738             source.uploaders.append(get_or_set_maintainer(up, session))
2739
2740     session.flush()
2741
2742     return source, dsc_component, dsc_location_id, pfs
2743
2744 __all__.append('add_dsc_to_db')
2745
2746 @session_wrapper
2747 def add_deb_to_db(u, filename, session=None):
2748     """
2749     Contrary to what you might expect, this routine deals with both
2750     debs and udebs.  That info is in 'dbtype', whilst 'type' is
2751     'deb' for both of them
2752     """
2753     cnf = Config()
2754     entry = u.pkg.files[filename]
2755
2756     bin = DBBinary()
2757     bin.package = entry["package"]
2758     bin.version = entry["version"]
2759     bin.maintainer_id = get_or_set_maintainer(entry["maintainer"], session).maintainer_id
2760     bin.fingerprint_id = get_or_set_fingerprint(u.pkg.changes["fingerprint"], session).fingerprint_id
2761     bin.arch_id = get_architecture(entry["architecture"], session).arch_id
2762     bin.binarytype = entry["dbtype"]
2763
2764     # Find poolfile id
2765     filename = entry["pool name"] + filename
2766     fullpath = os.path.join(cnf["Dir::Pool"], filename)
2767     if not entry.get("location id", None):
2768         entry["location id"] = get_location(cnf["Dir::Pool"], entry["component"], session=session).location_id
2769
2770     if entry.get("files id", None):
2771         poolfile = get_poolfile_by_id(bin.poolfile_id)
2772         bin.poolfile_id = entry["files id"]
2773     else:
2774         poolfile = add_poolfile(filename, entry, entry["location id"], session)
2775         bin.poolfile_id = entry["files id"] = poolfile.file_id
2776
2777     # Find source id
2778     bin_sources = get_sources_from_name(entry["source package"], entry["source version"], session=session)
2779     if len(bin_sources) != 1:
2780         raise NoSourceFieldError, "Unable to find a unique source id for %s (%s), %s, file %s, type %s, signed by %s" % \
2781                                   (bin.package, bin.version, entry["architecture"],
2782                                    filename, bin.binarytype, u.pkg.changes["fingerprint"])
2783
2784     bin.source_id = bin_sources[0].source_id
2785
2786     if entry.has_key("built-using"):
2787         for srcname, version in entry["built-using"]:
2788             exsources = get_sources_from_name(srcname, version, session=session)
2789             if len(exsources) != 1:
2790                 raise NoSourceFieldError, "Unable to find source package (%s = %s) in Built-Using for %s (%s), %s, file %s, type %s, signed by %s" % \
2791                                           (srcname, version, bin.package, bin.version, entry["architecture"],
2792                                            filename, bin.binarytype, u.pkg.changes["fingerprint"])
2793
2794             bin.extra_sources.append(exsources[0])
2795
2796     # Add and flush object so it has an ID
2797     session.add(bin)
2798
2799     suite_names = u.pkg.changes["distribution"].keys()
2800     bin.suites = session.query(Suite). \
2801         filter(Suite.suite_name.in_(suite_names)).all()
2802
2803     session.flush()
2804
2805     # Deal with contents - disabled for now
2806     #contents = copy_temporary_contents(bin.package, bin.version, bin.architecture.arch_string, os.path.basename(filename), None, session)
2807     #if not contents:
2808     #    print "REJECT\nCould not determine contents of package %s" % bin.package
2809     #    session.rollback()
2810     #    raise MissingContents, "No contents stored for package %s, and couldn't determine contents of %s" % (bin.package, filename)
2811
2812     return bin, poolfile
2813
2814 __all__.append('add_deb_to_db')
2815
2816 ################################################################################
2817
2818 class SourceACL(object):
2819     def __init__(self, *args, **kwargs):
2820         pass
2821
2822     def __repr__(self):
2823         return '<SourceACL %s>' % self.source_acl_id
2824
2825 __all__.append('SourceACL')
2826
2827 ################################################################################
2828
2829 class SrcFormat(object):
2830     def __init__(self, *args, **kwargs):
2831         pass
2832
2833     def __repr__(self):
2834         return '<SrcFormat %s>' % (self.format_name)
2835
2836 __all__.append('SrcFormat')
2837
2838 ################################################################################
2839
2840 SUITE_FIELDS = [ ('SuiteName', 'suite_name'),
2841                  ('SuiteID', 'suite_id'),
2842                  ('Version', 'version'),
2843                  ('Origin', 'origin'),
2844                  ('Label', 'label'),
2845                  ('Description', 'description'),
2846                  ('Untouchable', 'untouchable'),
2847                  ('Announce', 'announce'),
2848                  ('Codename', 'codename'),
2849                  ('OverrideCodename', 'overridecodename'),
2850                  ('ValidTime', 'validtime'),
2851                  ('Priority', 'priority'),
2852                  ('NotAutomatic', 'notautomatic'),
2853                  ('CopyChanges', 'copychanges'),
2854                  ('OverrideSuite', 'overridesuite')]
2855
2856 # Why the heck don't we have any UNIQUE constraints in table suite?
2857 # TODO: Add UNIQUE constraints for appropriate columns.
2858 class Suite(ORMObject):
2859     def __init__(self, suite_name = None, version = None):
2860         self.suite_name = suite_name
2861         self.version = version
2862
2863     def properties(self):
2864         return ['suite_name', 'version', 'sources_count', 'binaries_count', \
2865             'overrides_count']
2866
2867     def not_null_constraints(self):
2868         return ['suite_name']
2869
2870     def __eq__(self, val):
2871         if isinstance(val, str):
2872             return (self.suite_name == val)
2873         # This signals to use the normal comparison operator
2874         return NotImplemented
2875
2876     def __ne__(self, val):
2877         if isinstance(val, str):
2878             return (self.suite_name != val)
2879         # This signals to use the normal comparison operator
2880         return NotImplemented
2881
2882     def details(self):
2883         ret = []
2884         for disp, field in SUITE_FIELDS:
2885             val = getattr(self, field, None)
2886             if val is not None:
2887                 ret.append("%s: %s" % (disp, val))
2888
2889         return "\n".join(ret)
2890
2891     def get_architectures(self, skipsrc=False, skipall=False):
2892         """
2893         Returns list of Architecture objects
2894
2895         @type skipsrc: boolean
2896         @param skipsrc: Whether to skip returning the 'source' architecture entry
2897         (Default False)
2898
2899         @type skipall: boolean
2900         @param skipall: Whether to skip returning the 'all' architecture entry
2901         (Default False)
2902
2903         @rtype: list
2904         @return: list of Architecture objects for the given name (may be empty)
2905         """
2906
2907         q = object_session(self).query(Architecture).with_parent(self)
2908         if skipsrc:
2909             q = q.filter(Architecture.arch_string != 'source')
2910         if skipall:
2911             q = q.filter(Architecture.arch_string != 'all')
2912         return q.order_by(Architecture.arch_string).all()
2913
2914     def get_sources(self, source):
2915         """
2916         Returns a query object representing DBSource that is part of C{suite}.
2917
2918           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
2919
2920         @type source: string
2921         @param source: source package name
2922
2923         @rtype: sqlalchemy.orm.query.Query
2924         @return: a query of DBSource
2925
2926         """
2927
2928         session = object_session(self)
2929         return session.query(DBSource).filter_by(source = source). \
2930             with_parent(self)
2931
2932     def get_overridesuite(self):
2933         if self.overridesuite is None:
2934             return self
2935         else:
2936             return object_session(self).query(Suite).filter_by(suite_name=self.overridesuite).one()
2937
2938 __all__.append('Suite')
2939
2940 @session_wrapper
2941 def get_suite(suite, session=None):
2942     """
2943     Returns Suite object for given C{suite name}.
2944
2945     @type suite: string
2946     @param suite: The name of the suite
2947
2948     @type session: Session
2949     @param session: Optional SQLA session object (a temporary one will be
2950     generated if not supplied)
2951
2952     @rtype: Suite
2953     @return: Suite object for the requested suite name (None if not present)
2954     """
2955
2956     q = session.query(Suite).filter_by(suite_name=suite)
2957
2958     try:
2959         return q.one()
2960     except NoResultFound:
2961         return None
2962
2963 __all__.append('get_suite')
2964
2965 ################################################################################
2966
2967 # TODO: should be removed because the implementation is too trivial
2968 @session_wrapper
2969 def get_suite_architectures(suite, skipsrc=False, skipall=False, session=None):
2970     """
2971     Returns list of Architecture objects for given C{suite} name
2972
2973     @type suite: str
2974     @param suite: Suite name to search for
2975
2976     @type skipsrc: boolean
2977     @param skipsrc: Whether to skip returning the 'source' architecture entry
2978     (Default False)
2979
2980     @type skipall: boolean
2981     @param skipall: Whether to skip returning the 'all' architecture entry
2982     (Default False)
2983
2984     @type session: Session
2985     @param session: Optional SQL session object (a temporary one will be
2986     generated if not supplied)
2987
2988     @rtype: list
2989     @return: list of Architecture objects for the given name (may be empty)
2990     """
2991
2992     return get_suite(suite, session).get_architectures(skipsrc, skipall)
2993
2994 __all__.append('get_suite_architectures')
2995
2996 ################################################################################
2997
2998 class SuiteSrcFormat(object):
2999     def __init__(self, *args, **kwargs):
3000         pass
3001
3002     def __repr__(self):
3003         return '<SuiteSrcFormat (%s, %s)>' % (self.suite_id, self.src_format_id)
3004
3005 __all__.append('SuiteSrcFormat')
3006
3007 @session_wrapper
3008 def get_suite_src_formats(suite, session=None):
3009     """
3010     Returns list of allowed SrcFormat for C{suite}.
3011
3012     @type suite: str
3013     @param suite: Suite name to search for
3014
3015     @type session: Session
3016     @param session: Optional SQL session object (a temporary one will be
3017     generated if not supplied)
3018
3019     @rtype: list
3020     @return: the list of allowed source formats for I{suite}
3021     """
3022
3023     q = session.query(SrcFormat)
3024     q = q.join(SuiteSrcFormat)
3025     q = q.join(Suite).filter_by(suite_name=suite)
3026     q = q.order_by('format_name')
3027
3028     return q.all()
3029
3030 __all__.append('get_suite_src_formats')
3031
3032 ################################################################################
3033
3034 class Uid(ORMObject):
3035     def __init__(self, uid = None, name = None):
3036         self.uid = uid
3037         self.name = name
3038
3039     def __eq__(self, val):
3040         if isinstance(val, str):
3041             return (self.uid == val)
3042         # This signals to use the normal comparison operator
3043         return NotImplemented
3044
3045     def __ne__(self, val):
3046         if isinstance(val, str):
3047             return (self.uid != val)
3048         # This signals to use the normal comparison operator
3049         return NotImplemented
3050
3051     def properties(self):
3052         return ['uid', 'name', 'fingerprint']
3053
3054     def not_null_constraints(self):
3055         return ['uid']
3056
3057 __all__.append('Uid')
3058
3059 @session_wrapper
3060 def get_or_set_uid(uidname, session=None):
3061     """
3062     Returns uid object for given uidname.
3063
3064     If no matching uidname is found, a row is inserted.
3065
3066     @type uidname: string
3067     @param uidname: The uid to add
3068
3069     @type session: SQLAlchemy
3070     @param session: Optional SQL session object (a temporary one will be
3071     generated if not supplied).  If not passed, a commit will be performed at
3072     the end of the function, otherwise the caller is responsible for commiting.
3073
3074     @rtype: Uid
3075     @return: the uid object for the given uidname
3076     """
3077
3078     q = session.query(Uid).filter_by(uid=uidname)
3079
3080     try:
3081         ret = q.one()
3082     except NoResultFound:
3083         uid = Uid()
3084         uid.uid = uidname
3085         session.add(uid)
3086         session.commit_or_flush()
3087         ret = uid
3088
3089     return ret
3090
3091 __all__.append('get_or_set_uid')
3092
3093 @session_wrapper
3094 def get_uid_from_fingerprint(fpr, session=None):
3095     q = session.query(Uid)
3096     q = q.join(Fingerprint).filter_by(fingerprint=fpr)
3097
3098     try:
3099         return q.one()
3100     except NoResultFound:
3101         return None
3102
3103 __all__.append('get_uid_from_fingerprint')
3104
3105 ################################################################################
3106
3107 class UploadBlock(object):
3108     def __init__(self, *args, **kwargs):
3109         pass
3110
3111     def __repr__(self):
3112         return '<UploadBlock %s (%s)>' % (self.source, self.upload_block_id)
3113
3114 __all__.append('UploadBlock')
3115
3116 ################################################################################
3117
3118 class MetadataKey(ORMObject):
3119     def __init__(self, key = None):
3120         self.key = key
3121
3122     def properties(self):
3123         return ['key']
3124
3125     def not_null_constraints(self):
3126         return ['key']
3127
3128 __all__.append('MetadataKey')
3129
3130 @session_wrapper
3131 def get_or_set_metadatakey(keyname, session=None):
3132     """
3133     Returns MetadataKey object for given uidname.
3134
3135     If no matching keyname is found, a row is inserted.
3136
3137     @type uidname: string
3138     @param uidname: The keyname to add
3139
3140     @type session: SQLAlchemy
3141     @param session: Optional SQL session object (a temporary one will be
3142     generated if not supplied).  If not passed, a commit will be performed at
3143     the end of the function, otherwise the caller is responsible for commiting.
3144
3145     @rtype: MetadataKey
3146     @return: the metadatakey object for the given keyname
3147     """
3148
3149     q = session.query(MetadataKey).filter_by(key=keyname)
3150
3151     try:
3152         ret = q.one()
3153     except NoResultFound:
3154         ret = MetadataKey(keyname)
3155         session.add(ret)
3156         session.commit_or_flush()
3157
3158     return ret
3159
3160 __all__.append('get_or_set_metadatakey')
3161
3162 ################################################################################
3163
3164 class BinaryMetadata(ORMObject):
3165     def __init__(self, key = None, value = None, binary = None):
3166         self.key = key
3167         self.value = value
3168         self.binary = binary
3169
3170     def properties(self):
3171         return ['binary', 'key', 'value']
3172
3173     def not_null_constraints(self):
3174         return ['value']
3175
3176 __all__.append('BinaryMetadata')
3177
3178 ################################################################################
3179
3180 class SourceMetadata(ORMObject):
3181     def __init__(self, key = None, value = None, source = None):
3182         self.key = key
3183         self.value = value
3184         self.source = source
3185
3186     def properties(self):
3187         return ['source', 'key', 'value']
3188
3189     def not_null_constraints(self):
3190         return ['value']
3191
3192 __all__.append('SourceMetadata')
3193
3194 ################################################################################
3195
3196 class VersionCheck(ORMObject):
3197     def __init__(self, *args, **kwargs):
3198         pass
3199
3200     def properties(self):
3201         #return ['suite_id', 'check', 'reference_id']
3202         return ['check']
3203
3204     def not_null_constraints(self):
3205         return ['suite', 'check', 'reference']
3206
3207 __all__.append('VersionCheck')
3208
3209 @session_wrapper
3210 def get_version_checks(suite_name, check = None, session = None):
3211     suite = get_suite(suite_name, session)
3212     if not suite:
3213         # Make sure that what we return is iterable so that list comprehensions
3214         # involving this don't cause a traceback
3215         return []
3216     q = session.query(VersionCheck).filter_by(suite=suite)
3217     if check:
3218         q = q.filter_by(check=check)
3219     return q.all()
3220
3221 __all__.append('get_version_checks')
3222
3223 ################################################################################
3224
3225 class DBConn(object):
3226     """
3227     database module init.
3228     """
3229     __shared_state = {}
3230
3231     def __init__(self, *args, **kwargs):
3232         self.__dict__ = self.__shared_state
3233
3234         if not getattr(self, 'initialised', False):
3235             self.initialised = True
3236             self.debug = kwargs.has_key('debug')
3237             self.__createconn()
3238
3239     def __setuptables(self):
3240         tables = (
3241             'architecture',
3242             'archive',
3243             'bin_associations',
3244             'bin_contents',
3245             'binaries',
3246             'binaries_metadata',
3247             'binary_acl',
3248             'binary_acl_map',
3249             'build_queue',
3250             'build_queue_files',
3251             'build_queue_policy_files',
3252             'changelogs_text',
3253             'changes',
3254             'component',
3255             'config',
3256             'changes_pending_binaries',
3257             'changes_pending_files',
3258             'changes_pending_source',
3259             'changes_pending_files_map',
3260             'changes_pending_source_files',
3261             'changes_pool_files',
3262             'dsc_files',
3263             'external_overrides',
3264             'extra_src_references',
3265             'files',
3266             'fingerprint',
3267             'keyrings',
3268             'keyring_acl_map',
3269             'location',
3270             'maintainer',
3271             'metadata_keys',
3272             'new_comments',
3273             # TODO: the maintainer column in table override should be removed.
3274             'override',
3275             'override_type',
3276             'policy_queue',
3277             'priority',
3278             'section',
3279             'source',
3280             'source_acl',
3281             'source_metadata',
3282             'src_associations',
3283             'src_contents',
3284             'src_format',
3285             'src_uploaders',
3286             'suite',
3287             'suite_architectures',
3288             'suite_build_queue_copy',
3289             'suite_src_formats',
3290             'uid',
3291             'upload_blocks',
3292             'version_check',
3293         )
3294
3295         views = (
3296             'almost_obsolete_all_associations',
3297             'almost_obsolete_src_associations',
3298             'any_associations_source',
3299             'bin_associations_binaries',
3300             'binaries_suite_arch',
3301             'binfiles_suite_component_arch',
3302             'changelogs',
3303             'file_arch_suite',
3304             'newest_all_associations',
3305             'newest_any_associations',
3306             'newest_source',
3307             'newest_src_association',
3308             'obsolete_all_associations',
3309             'obsolete_any_associations',
3310             'obsolete_any_by_all_associations',
3311             'obsolete_src_associations',
3312             'source_suite',
3313             'src_associations_bin',
3314             'src_associations_src',
3315             'suite_arch_by_name',
3316         )
3317
3318         for table_name in tables:
3319             table = Table(table_name, self.db_meta, \
3320                 autoload=True, useexisting=True)
3321             setattr(self, 'tbl_%s' % table_name, table)
3322
3323         for view_name in views:
3324             view = Table(view_name, self.db_meta, autoload=True)
3325             setattr(self, 'view_%s' % view_name, view)
3326
3327     def __setupmappers(self):
3328         mapper(Architecture, self.tbl_architecture,
3329             properties = dict(arch_id = self.tbl_architecture.c.id,
3330                suites = relation(Suite, secondary=self.tbl_suite_architectures,
3331                    order_by='suite_name',
3332                    backref=backref('architectures', order_by='arch_string'))),
3333             extension = validator)
3334
3335         mapper(Archive, self.tbl_archive,
3336                properties = dict(archive_id = self.tbl_archive.c.id,
3337                                  archive_name = self.tbl_archive.c.name))
3338
3339         mapper(BuildQueue, self.tbl_build_queue,
3340                properties = dict(queue_id = self.tbl_build_queue.c.id))
3341
3342         mapper(BuildQueueFile, self.tbl_build_queue_files,
3343                properties = dict(buildqueue = relation(BuildQueue, backref='queuefiles'),
3344                                  poolfile = relation(PoolFile, backref='buildqueueinstances')))
3345
3346         mapper(BuildQueuePolicyFile, self.tbl_build_queue_policy_files,
3347                properties = dict(
3348                 build_queue = relation(BuildQueue, backref='policy_queue_files'),
3349                 file = relation(ChangePendingFile, lazy='joined')))
3350
3351         mapper(DBBinary, self.tbl_binaries,
3352                properties = dict(binary_id = self.tbl_binaries.c.id,
3353                                  package = self.tbl_binaries.c.package,
3354                                  version = self.tbl_binaries.c.version,
3355                                  maintainer_id = self.tbl_binaries.c.maintainer,
3356                                  maintainer = relation(Maintainer),
3357                                  source_id = self.tbl_binaries.c.source,
3358                                  source = relation(DBSource, backref='binaries'),
3359                                  arch_id = self.tbl_binaries.c.architecture,
3360                                  architecture = relation(Architecture),
3361                                  poolfile_id = self.tbl_binaries.c.file,
3362                                  poolfile = relation(PoolFile, backref=backref('binary', uselist = False)),
3363                                  binarytype = self.tbl_binaries.c.type,
3364                                  fingerprint_id = self.tbl_binaries.c.sig_fpr,
3365                                  fingerprint = relation(Fingerprint),
3366                                  install_date = self.tbl_binaries.c.install_date,
3367                                  suites = relation(Suite, secondary=self.tbl_bin_associations,
3368                                      backref=backref('binaries', lazy='dynamic')),
3369                                  extra_sources = relation(DBSource, secondary=self.tbl_extra_src_references,
3370                                      backref=backref('extra_binary_references', lazy='dynamic')),
3371                                  key = relation(BinaryMetadata, cascade='all',
3372                                      collection_class=attribute_mapped_collection('key'))),
3373                 extension = validator)
3374
3375         mapper(BinaryACL, self.tbl_binary_acl,
3376                properties = dict(binary_acl_id = self.tbl_binary_acl.c.id))
3377
3378         mapper(BinaryACLMap, self.tbl_binary_acl_map,
3379                properties = dict(binary_acl_map_id = self.tbl_binary_acl_map.c.id,
3380                                  fingerprint = relation(Fingerprint, backref="binary_acl_map"),
3381                                  architecture = relation(Architecture)))
3382
3383         mapper(Component, self.tbl_component,
3384                properties = dict(component_id = self.tbl_component.c.id,
3385                                  component_name = self.tbl_component.c.name),
3386                extension = validator)
3387
3388         mapper(DBConfig, self.tbl_config,
3389                properties = dict(config_id = self.tbl_config.c.id))
3390
3391         mapper(DSCFile, self.tbl_dsc_files,
3392                properties = dict(dscfile_id = self.tbl_dsc_files.c.id,
3393                                  source_id = self.tbl_dsc_files.c.source,
3394                                  source = relation(DBSource),
3395                                  poolfile_id = self.tbl_dsc_files.c.file,
3396                                  poolfile = relation(PoolFile)))
3397
3398         mapper(ExternalOverride, self.tbl_external_overrides,
3399                 properties = dict(
3400                     suite_id = self.tbl_external_overrides.c.suite,
3401                     suite = relation(Suite),
3402                     component_id = self.tbl_external_overrides.c.component,
3403                     component = relation(Component)))
3404
3405         mapper(PoolFile, self.tbl_files,
3406                properties = dict(file_id = self.tbl_files.c.id,
3407                                  filesize = self.tbl_files.c.size,
3408                                  location_id = self.tbl_files.c.location,
3409                                  location = relation(Location,
3410                                      # using lazy='dynamic' in the back
3411                                      # reference because we have A LOT of
3412                                      # files in one location
3413                                      backref=backref('files', lazy='dynamic'))),
3414                 extension = validator)
3415
3416         mapper(Fingerprint, self.tbl_fingerprint,
3417                properties = dict(fingerprint_id = self.tbl_fingerprint.c.id,
3418                                  uid_id = self.tbl_fingerprint.c.uid,
3419                                  uid = relation(Uid),
3420                                  keyring_id = self.tbl_fingerprint.c.keyring,
3421                                  keyring = relation(Keyring),
3422                                  source_acl = relation(SourceACL),
3423                                  binary_acl = relation(BinaryACL)),
3424                extension = validator)
3425
3426         mapper(Keyring, self.tbl_keyrings,
3427                properties = dict(keyring_name = self.tbl_keyrings.c.name,
3428                                  keyring_id = self.tbl_keyrings.c.id))
3429
3430         mapper(DBChange, self.tbl_changes,
3431                properties = dict(change_id = self.tbl_changes.c.id,
3432                                  poolfiles = relation(PoolFile,
3433                                                       secondary=self.tbl_changes_pool_files,
3434                                                       backref="changeslinks"),
3435                                  seen = self.tbl_changes.c.seen,
3436                                  source = self.tbl_changes.c.source,
3437                                  binaries = self.tbl_changes.c.binaries,
3438                                  architecture = self.tbl_changes.c.architecture,
3439                                  distribution = self.tbl_changes.c.distribution,
3440                                  urgency = self.tbl_changes.c.urgency,
3441                                  maintainer = self.tbl_changes.c.maintainer,
3442                                  changedby = self.tbl_changes.c.changedby,
3443                                  date = self.tbl_changes.c.date,
3444                                  version = self.tbl_changes.c.version,
3445                                  files = relation(ChangePendingFile,
3446                                                   secondary=self.tbl_changes_pending_files_map,
3447                                                   backref="changesfile"),
3448                                  in_queue_id = self.tbl_changes.c.in_queue,
3449                                  in_queue = relation(PolicyQueue,
3450                                                      primaryjoin=(self.tbl_changes.c.in_queue==self.tbl_policy_queue.c.id)),
3451                                  approved_for_id = self.tbl_changes.c.approved_for))
3452
3453         mapper(ChangePendingBinary, self.tbl_changes_pending_binaries,
3454                properties = dict(change_pending_binary_id = self.tbl_changes_pending_binaries.c.id))
3455
3456         mapper(ChangePendingFile, self.tbl_changes_pending_files,
3457                properties = dict(change_pending_file_id = self.tbl_changes_pending_files.c.id,
3458                                  filename = self.tbl_changes_pending_files.c.filename,
3459                                  size = self.tbl_changes_pending_files.c.size,
3460                                  md5sum = self.tbl_changes_pending_files.c.md5sum,
3461                                  sha1sum = self.tbl_changes_pending_files.c.sha1sum,
3462                                  sha256sum = self.tbl_changes_pending_files.c.sha256sum))
3463
3464         mapper(ChangePendingSource, self.tbl_changes_pending_source,
3465                properties = dict(change_pending_source_id = self.tbl_changes_pending_source.c.id,
3466                                  change = relation(DBChange),
3467                                  maintainer = relation(Maintainer,
3468                                                        primaryjoin=(self.tbl_changes_pending_source.c.maintainer_id==self.tbl_maintainer.c.id)),
3469                                  changedby = relation(Maintainer,
3470                                                       primaryjoin=(self.tbl_changes_pending_source.c.changedby_id==self.tbl_maintainer.c.id)),
3471                                  fingerprint = relation(Fingerprint),
3472                                  source_files = relation(ChangePendingFile,
3473                                                          secondary=self.tbl_changes_pending_source_files,
3474                                                          backref="pending_sources")))
3475
3476
3477         mapper(KeyringACLMap, self.tbl_keyring_acl_map,
3478                properties = dict(keyring_acl_map_id = self.tbl_keyring_acl_map.c.id,
3479                                  keyring = relation(Keyring, backref="keyring_acl_map"),
3480                                  architecture = relation(Architecture)))
3481
3482         mapper(Location, self.tbl_location,
3483                properties = dict(location_id = self.tbl_location.c.id,
3484                                  component_id = self.tbl_location.c.component,
3485                                  component = relation(Component, backref='location'),
3486                                  archive_id = self.tbl_location.c.archive,
3487                                  archive = relation(Archive),
3488                                  # FIXME: the 'type' column is old cruft and
3489                                  # should be removed in the future.
3490                                  archive_type = self.tbl_location.c.type),
3491                extension = validator)
3492
3493         mapper(Maintainer, self.tbl_maintainer,
3494                properties = dict(maintainer_id = self.tbl_maintainer.c.id,
3495                    maintains_sources = relation(DBSource, backref='maintainer',
3496                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.maintainer)),
3497                    changed_sources = relation(DBSource, backref='changedby',
3498                        primaryjoin=(self.tbl_maintainer.c.id==self.tbl_source.c.changedby))),
3499                 extension = validator)
3500
3501         mapper(NewComment, self.tbl_new_comments,
3502                properties = dict(comment_id = self.tbl_new_comments.c.id))
3503
3504         mapper(Override, self.tbl_override,
3505                properties = dict(suite_id = self.tbl_override.c.suite,
3506                                  suite = relation(Suite, \
3507                                     backref=backref('overrides', lazy='dynamic')),
3508                                  package = self.tbl_override.c.package,
3509                                  component_id = self.tbl_override.c.component,
3510                                  component = relation(Component, \
3511                                     backref=backref('overrides', lazy='dynamic')),
3512                                  priority_id = self.tbl_override.c.priority,
3513                                  priority = relation(Priority, \
3514                                     backref=backref('overrides', lazy='dynamic')),
3515                                  section_id = self.tbl_override.c.section,
3516                                  section = relation(Section, \
3517                                     backref=backref('overrides', lazy='dynamic')),
3518                                  overridetype_id = self.tbl_override.c.type,
3519                                  overridetype = relation(OverrideType, \
3520                                     backref=backref('overrides', lazy='dynamic'))))
3521
3522         mapper(OverrideType, self.tbl_override_type,
3523                properties = dict(overridetype = self.tbl_override_type.c.type,
3524                                  overridetype_id = self.tbl_override_type.c.id))
3525
3526         mapper(PolicyQueue, self.tbl_policy_queue,
3527                properties = dict(policy_queue_id = self.tbl_policy_queue.c.id))
3528
3529         mapper(Priority, self.tbl_priority,
3530                properties = dict(priority_id = self.tbl_priority.c.id))
3531
3532         mapper(Section, self.tbl_section,
3533                properties = dict(section_id = self.tbl_section.c.id,
3534                                  section=self.tbl_section.c.section))
3535
3536         mapper(DBSource, self.tbl_source,
3537                properties = dict(source_id = self.tbl_source.c.id,
3538                                  version = self.tbl_source.c.version,
3539                                  maintainer_id = self.tbl_source.c.maintainer,
3540                                  poolfile_id = self.tbl_source.c.file,
3541                                  poolfile = relation(PoolFile, backref=backref('source', uselist = False)),
3542                                  fingerprint_id = self.tbl_source.c.sig_fpr,
3543                                  fingerprint = relation(Fingerprint),
3544                                  changedby_id = self.tbl_source.c.changedby,
3545                                  srcfiles = relation(DSCFile,
3546                                                      primaryjoin=(self.tbl_source.c.id==self.tbl_dsc_files.c.source)),
3547                                  suites = relation(Suite, secondary=self.tbl_src_associations,
3548                                      backref=backref('sources', lazy='dynamic')),
3549                                  uploaders = relation(Maintainer,
3550                                      secondary=self.tbl_src_uploaders),
3551                                  key = relation(SourceMetadata, cascade='all',
3552                                      collection_class=attribute_mapped_collection('key'))),
3553                extension = validator)
3554
3555         mapper(SourceACL, self.tbl_source_acl,
3556                properties = dict(source_acl_id = self.tbl_source_acl.c.id))
3557
3558         mapper(SrcFormat, self.tbl_src_format,
3559                properties = dict(src_format_id = self.tbl_src_format.c.id,
3560                                  format_name = self.tbl_src_format.c.format_name))
3561
3562         mapper(Suite, self.tbl_suite,
3563                properties = dict(suite_id = self.tbl_suite.c.id,
3564                                  policy_queue = relation(PolicyQueue),
3565                                  copy_queues = relation(BuildQueue,
3566                                      secondary=self.tbl_suite_build_queue_copy)),
3567                 extension = validator)
3568
3569         mapper(SuiteSrcFormat, self.tbl_suite_src_formats,
3570                properties = dict(suite_id = self.tbl_suite_src_formats.c.suite,
3571                                  suite = relation(Suite, backref='suitesrcformats'),
3572                                  src_format_id = self.tbl_suite_src_formats.c.src_format,
3573                                  src_format = relation(SrcFormat)))
3574
3575         mapper(Uid, self.tbl_uid,
3576                properties = dict(uid_id = self.tbl_uid.c.id,
3577                                  fingerprint = relation(Fingerprint)),
3578                extension = validator)
3579
3580         mapper(UploadBlock, self.tbl_upload_blocks,
3581                properties = dict(upload_block_id = self.tbl_upload_blocks.c.id,
3582                                  fingerprint = relation(Fingerprint, backref="uploadblocks"),
3583                                  uid = relation(Uid, backref="uploadblocks")))
3584
3585         mapper(BinContents, self.tbl_bin_contents,
3586             properties = dict(
3587                 binary = relation(DBBinary,
3588                     backref=backref('contents', lazy='dynamic', cascade='all')),
3589                 file = self.tbl_bin_contents.c.file))
3590
3591         mapper(SrcContents, self.tbl_src_contents,
3592             properties = dict(
3593                 source = relation(DBSource,
3594                     backref=backref('contents', lazy='dynamic', cascade='all')),
3595                 file = self.tbl_src_contents.c.file))
3596
3597         mapper(MetadataKey, self.tbl_metadata_keys,
3598             properties = dict(
3599                 key_id = self.tbl_metadata_keys.c.key_id,
3600                 key = self.tbl_metadata_keys.c.key))
3601
3602         mapper(BinaryMetadata, self.tbl_binaries_metadata,
3603             properties = dict(
3604                 binary_id = self.tbl_binaries_metadata.c.bin_id,
3605                 binary = relation(DBBinary),
3606                 key_id = self.tbl_binaries_metadata.c.key_id,
3607                 key = relation(MetadataKey),
3608                 value = self.tbl_binaries_metadata.c.value))
3609
3610         mapper(SourceMetadata, self.tbl_source_metadata,
3611             properties = dict(
3612                 source_id = self.tbl_source_metadata.c.src_id,
3613                 source = relation(DBSource),
3614                 key_id = self.tbl_source_metadata.c.key_id,
3615                 key = relation(MetadataKey),
3616                 value = self.tbl_source_metadata.c.value))
3617
3618         mapper(VersionCheck, self.tbl_version_check,
3619             properties = dict(
3620                 suite_id = self.tbl_version_check.c.suite,
3621                 suite = relation(Suite, primaryjoin=self.tbl_version_check.c.suite==self.tbl_suite.c.id),
3622                 reference_id = self.tbl_version_check.c.reference,
3623                 reference = relation(Suite, primaryjoin=self.tbl_version_check.c.reference==self.tbl_suite.c.id, lazy='joined')))
3624
3625     ## Connection functions
3626     def __createconn(self):
3627         from config import Config
3628         cnf = Config()
3629         if cnf.has_key("DB::Service"):
3630             connstr = "postgresql://service=%s" % cnf["DB::Service"]
3631         elif cnf.has_key("DB::Host"):
3632             # TCP/IP
3633             connstr = "postgresql://%s" % cnf["DB::Host"]
3634             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
3635                 connstr += ":%s" % cnf["DB::Port"]
3636             connstr += "/%s" % cnf["DB::Name"]
3637         else:
3638             # Unix Socket
3639             connstr = "postgresql:///%s" % cnf["DB::Name"]
3640             if cnf.has_key("DB::Port") and cnf["DB::Port"] != "-1":
3641                 connstr += "?port=%s" % cnf["DB::Port"]
3642
3643         engine_args = { 'echo': self.debug }
3644         if cnf.has_key('DB::PoolSize'):
3645             engine_args['pool_size'] = int(cnf['DB::PoolSize'])
3646         if cnf.has_key('DB::MaxOverflow'):
3647             engine_args['max_overflow'] = int(cnf['DB::MaxOverflow'])
3648         if sa_major_version == '0.6' and cnf.has_key('DB::Unicode') and \
3649             cnf['DB::Unicode'] == 'false':
3650             engine_args['use_native_unicode'] = False
3651
3652         # Monkey patch a new dialect in in order to support service= syntax
3653         import sqlalchemy.dialects.postgresql
3654         from sqlalchemy.dialects.postgresql.psycopg2 import PGDialect_psycopg2
3655         class PGDialect_psycopg2_dak(PGDialect_psycopg2):
3656             def create_connect_args(self, url):
3657                 if str(url).startswith('postgresql://service='):
3658                     # Eww
3659                     servicename = str(url)[21:]
3660                     return (['service=%s' % servicename], {})
3661                 else:
3662                     return PGDialect_psycopg2.create_connect_args(self, url)
3663
3664         sqlalchemy.dialects.postgresql.base.dialect = PGDialect_psycopg2_dak
3665
3666         self.db_pg   = create_engine(connstr, **engine_args)
3667         self.db_meta = MetaData()
3668         self.db_meta.bind = self.db_pg
3669         self.db_smaker = sessionmaker(bind=self.db_pg,
3670                                       autoflush=True,
3671                                       autocommit=False)
3672
3673         self.__setuptables()
3674         self.__setupmappers()
3675         self.pid = os.getpid()
3676
3677     def session(self, work_mem = 0):
3678         '''
3679         Returns a new session object. If a work_mem parameter is provided a new
3680         transaction is started and the work_mem parameter is set for this
3681         transaction. The work_mem parameter is measured in MB. A default value
3682         will be used if the parameter is not set.
3683         '''
3684         # reinitialize DBConn in new processes
3685         if self.pid != os.getpid():
3686             clear_mappers()
3687             self.__createconn()
3688         session = self.db_smaker()
3689         if work_mem > 0:
3690             session.execute("SET LOCAL work_mem TO '%d MB'" % work_mem)
3691         return session
3692
3693 __all__.append('DBConn')
3694
3695