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