]> git.decadent.org.uk Git - dak.git/blob - daklib/dbconn.py
Merge commit 'ftpmaster/master'
[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  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 os
37 import psycopg2
38 import psycopg2.extras
39 import traceback
40
41 from singleton import Singleton
42 from config import Config
43
44 ################################################################################
45
46 class Cache(object):
47     def __init__(self, hashfunc=None):
48         if hashfunc:
49             self.hashfunc = hashfunc
50         else:
51             self.hashfunc = lambda x: x['value']
52
53         self.data = {}
54
55     def SetValue(self, keys, value):
56         self.data[self.hashfunc(keys)] = value
57
58     def GetValue(self, keys):
59         return self.data.get(self.hashfunc(keys))
60
61 ################################################################################
62
63 class DBConn(Singleton):
64     """
65     database module init.
66     """
67     def __init__(self, *args, **kwargs):
68         super(DBConn, self).__init__(*args, **kwargs)
69
70     def _startup(self, *args, **kwargs):
71         self.__createconn()
72         self.__init_caches()
73
74     ## Connection functions
75     def __createconn(self):
76         cnf = Config()
77         connstr = "dbname=%s" % cnf["DB::Name"]
78         if cnf["DB::Host"]:
79            connstr += " host=%s" % cnf["DB::Host"]
80         if cnf["DB::Port"] and cnf["DB::Port"] != "-1":
81            connstr += " port=%s" % cnf["DB::Port"]
82
83         self.db_con = psycopg2.connect(connstr)
84
85     def reconnect(self):
86         try:
87             self.db_con.close()
88         except psycopg2.InterfaceError:
89             pass
90
91         self.db_con = None
92         self.__createconn()
93
94     ## Cache functions
95     def __init_caches(self):
96         self.caches = {'suite':         Cache(),
97                        'section':       Cache(),
98                        'priority':      Cache(),
99                        'override_type': Cache(),
100                        'architecture':  Cache(),
101                        'archive':       Cache(),
102                        'component':     Cache(),
103                        'content_path_names':     Cache(),
104                        'content_file_names':     Cache(),
105                        'location':      Cache(lambda x: '%s_%s_%s' % (x['location'], x['component'], x['location'])),
106                        'maintainer':    {}, # TODO
107                        'keyring':       {}, # TODO
108                        'source':        Cache(lambda x: '%s_%s_' % (x['source'], x['version'])),
109                        'files':         Cache(lambda x: '%s_%s_' % (x['filename'], x['location'])),
110                        'maintainer':    {}, # TODO
111                        'fingerprint':   {}, # TODO
112                        'queue':         {}, # TODO
113                        'uid':           {}, # TODO
114                        'suite_version': Cache(lambda x: '%s_%s' % (x['source'], x['suite'])),
115                       }
116
117         self.prepared_statements = {}
118
119     def prepare(self,name,statement):
120         if not self.prepared_statements.has_key(name):
121             c = self.cursor()
122             c.execute(statement)
123             self.prepared_statements[name] = statement
124
125     def clear_caches(self):
126         self.__init_caches()
127
128     ## Functions to pass through to the database connector
129     def cursor(self):
130         return self.db_con.cursor()
131
132     def commit(self):
133         return self.db_con.commit()
134
135     ## Get functions
136     def __get_single_row(self, query, values):
137         c = self.db_con.cursor(cursor_factory=psycopg2.extras.DictCursor)
138         c.execute(query, values)
139
140         if c.rowcount < 1:
141             return None
142
143         res = c.fetchone()
144
145         return res
146
147     def __get_single_id(self, query, values, cachename=None):
148         # This is a bit of a hack but it's an internal function only
149         if cachename is not None:
150             res = self.caches[cachename].GetValue(values)
151             if res:
152                 return res
153
154         c = self.db_con.cursor()
155         c.execute(query, values)
156
157         if c.rowcount != 1:
158             return None
159
160         res = c.fetchone()[0]
161
162         if cachename is not None:
163             self.caches[cachename].SetValue(values, res)
164
165         return res
166
167     def __get_id(self, retfield, table, qfield, value):
168         query = "SELECT %s FROM %s WHERE %s = %%(value)s" % (retfield, table, qfield)
169         return self.__get_single_id(query, {'value': value}, cachename=table)
170
171     def get_suite_id(self, suite):
172         """
173         Returns database id for given C{suite}.
174         Results are kept in a cache during runtime to minimize database queries.
175
176         @type suite: string
177         @param suite: The name of the suite
178
179         @rtype: int
180         @return: the database id for the given suite
181
182         """
183         return int(self.__get_id('id', 'suite', 'suite_name', suite))
184
185     def get_section_id(self, section):
186         """
187         Returns database id for given C{section}.
188         Results are kept in a cache during runtime to minimize database queries.
189
190         @type section: string
191         @param section: The name of the section
192
193         @rtype: int
194         @return: the database id for the given section
195
196         """
197         return self.__get_id('id', 'section', 'section', section)
198
199     def get_priority_id(self, priority):
200         """
201         Returns database id for given C{priority}.
202         Results are kept in a cache during runtime to minimize database queries.
203
204         @type priority: string
205         @param priority: The name of the priority
206
207         @rtype: int
208         @return: the database id for the given priority
209
210         """
211         return self.__get_id('id', 'priority', 'priority', priority)
212
213     def get_override_type_id(self, override_type):
214         """
215         Returns database id for given override C{type}.
216         Results are kept in a cache during runtime to minimize database queries.
217
218         @type type: string
219         @param type: The name of the override type
220
221         @rtype: int
222         @return: the database id for the given override type
223
224         """
225         return self.__get_id('id', 'override_type', 'type', override_type)
226
227     def get_architecture_id(self, architecture):
228         """
229         Returns database id for given C{architecture}.
230         Results are kept in a cache during runtime to minimize database queries.
231
232         @type architecture: string
233         @param architecture: The name of the override type
234
235         @rtype: int
236         @return: the database id for the given architecture
237
238         """
239         return self.__get_id('id', 'architecture', 'arch_string', architecture)
240
241     def get_archive_id(self, archive):
242         """
243         returns database id for given c{archive}.
244         results are kept in a cache during runtime to minimize database queries.
245
246         @type archive: string
247         @param archive: the name of the override type
248
249         @rtype: int
250         @return: the database id for the given archive
251
252         """
253         return self.__get_id('id', 'archive', 'lower(name)', archive)
254
255     def get_component_id(self, component):
256         """
257         Returns database id for given C{component}.
258         Results are kept in a cache during runtime to minimize database queries.
259
260         @type component: string
261         @param component: The name of the override type
262
263         @rtype: int
264         @return: the database id for the given component
265
266         """
267         return self.__get_id('id', 'component', 'lower(name)', component)
268
269     def get_location_id(self, location, component, archive):
270         """
271         Returns database id for the location behind the given combination of
272           - B{location} - the path of the location, eg. I{/srv/ftp.debian.org/ftp/pool/}
273           - B{component} - the id of the component as returned by L{get_component_id}
274           - B{archive} - the id of the archive as returned by L{get_archive_id}
275         Results are kept in a cache during runtime to minimize database queries.
276
277         @type location: string
278         @param location: the path of the location
279
280         @type component: int
281         @param component: the id of the component
282
283         @type archive: int
284         @param archive: the id of the archive
285
286         @rtype: int
287         @return: the database id for the location
288
289         """
290
291         archive_id = self.get_archive_id(archive)
292
293         if not archive_id:
294             return None
295
296         res = None
297
298         if component:
299             component_id = self.get_component_id(component)
300             if component_id:
301                 res = self.__get_single_id("SELECT id FROM location WHERE path=%(location)s AND component=%(component)s AND archive=%(archive)s",
302                         {'location': location,
303                          'archive': int(archive_id),
304                          'component': component_id}, cachename='location')
305         else:
306             res = self.__get_single_id("SELECT id FROM location WHERE path=%(location)s AND archive=%(archive)d",
307                     {'location': location, 'archive': archive_id, 'component': ''}, cachename='location')
308
309         return res
310
311     def get_source_id(self, source, version):
312         """
313         Returns database id for the combination of C{source} and C{version}
314           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
315           - B{version}
316         Results are kept in a cache during runtime to minimize database queries.
317
318         @type source: string
319         @param source: source package name
320
321         @type version: string
322         @param version: the source version
323
324         @rtype: int
325         @return: the database id for the source
326
327         """
328         return self.__get_single_id("SELECT id FROM source s WHERE s.source=%(source)s AND s.version=%(version)s",
329                                  {'source': source, 'version': version}, cachename='source')
330
331     def get_suite(self, suite):
332         if isinstance(suite, str):
333             suite_id = self.get_suite_id(suite.lower())
334         elif type(suite) == int:
335             suite_id = suite
336
337         return self.__get_single_row("SELECT * FROM suite WHERE id = %(id)s",
338                                      {'id': suite_id})
339
340     def get_suite_version(self, source, suite):
341         """
342         Returns database id for a combination of C{source} and C{suite}.
343
344           - B{source} - source package name, eg. I{mailfilter}, I{bbdb}, I{glibc}
345           - B{suite} - a suite name, eg. I{unstable}
346
347         Results are kept in a cache during runtime to minimize database queries.
348
349         @type source: string
350         @param source: source package name
351
352         @type suite: string
353         @param suite: the suite name
354
355         @rtype: string
356         @return: the version for I{source} in I{suite}
357
358         """
359         return self.__get_single_id("""
360         SELECT s.version FROM source s, suite su, src_associations sa
361         WHERE sa.source=s.id
362           AND sa.suite=su.id
363           AND su.suite_name=%(suite)s
364           AND s.source=%(source)""", {'suite': suite, 'source': source}, cachename='suite_version')
365
366
367     def get_files_id (self, filename, size, md5sum, location_id):
368         """
369         Returns -1, -2 or the file_id for filename, if its C{size} and C{md5sum} match an
370         existing copy.
371
372         The database is queried using the C{filename} and C{location_id}. If a file does exist
373         at that location, the existing size and md5sum are checked against the provided
374         parameters. A size or checksum mismatch returns -2. If more than one entry is
375         found within the database, a -1 is returned, no result returns None, otherwise
376         the file id.
377
378         Results are kept in a cache during runtime to minimize database queries.
379
380         @type filename: string
381         @param filename: the filename of the file to check against the DB
382
383         @type size: int
384         @param size: the size of the file to check against the DB
385
386         @type md5sum: string
387         @param md5sum: the md5sum of the file to check against the DB
388
389         @type location_id: int
390         @param location_id: the id of the location as returned by L{get_location_id}
391
392         @rtype: int / None
393         @return: Various return values are possible:
394                    - -2: size/checksum error
395                    - -1: more than one file found in database
396                    - None: no file found in database
397                    - int: file id
398
399         """
400         values = {'filename' : filename,
401                   'location' : location_id}
402
403         res = self.caches['files'].GetValue( values )
404
405         if not res:
406             query = """SELECT id, size, md5sum
407                        FROM files
408                        WHERE filename = %(filename)s AND location = %(location)s"""
409
410             cursor = self.db_con.cursor()
411             cursor.execute( query, values )
412
413             if cursor.rowcount == 0:
414                 res = None
415
416             elif cursor.rowcount != 1:
417                 res = -1
418
419             else:
420                 row = cursor.fetchone()
421
422                 if row[1] != int(size) or row[2] != md5sum:
423                     res =  -2
424
425                 else:
426                     self.caches['files'].SetValue(values, row[0])
427                     res = row[0]
428
429         return res
430
431
432     def get_or_set_contents_file_id(self, filename):
433         """
434         Returns database id for given filename.
435
436         Results are kept in a cache during runtime to minimize database queries.
437         If no matching file is found, a row is inserted.
438
439         @type filename: string
440         @param filename: The filename
441
442         @rtype: int
443         @return: the database id for the given component
444         """
445         try:
446             values={'value': filename}
447             query = "SELECT id FROM content_file_names WHERE file = %(value)s"
448             id = self.__get_single_id(query, values, cachename='content_file_names')
449             if not id:
450                 c = self.db_con.cursor()
451                 c.execute( "INSERT INTO content_file_names VALUES (DEFAULT, %(value)s) RETURNING id",
452                            values )
453
454                 id = c.fetchone()[0]
455                 self.caches['content_file_names'].SetValue(values, id)
456
457             return id
458         except:
459             traceback.print_exc()
460             raise
461
462     def get_or_set_contents_path_id(self, path):
463         """
464         Returns database id for given path.
465
466         Results are kept in a cache during runtime to minimize database queries.
467         If no matching file is found, a row is inserted.
468
469         @type path: string
470         @param path: The filename
471
472         @rtype: int
473         @return: the database id for the given component
474         """
475         try:
476             values={'value': path}
477             query = "SELECT id FROM content_file_paths WHERE path = %(value)s"
478             id = self.__get_single_id(query, values, cachename='content_path_names')
479             if not id:
480                 c = self.db_con.cursor()
481                 c.execute( "INSERT INTO content_file_paths VALUES (DEFAULT, %(value)s) RETURNING id",
482                            values )
483
484                 id = c.fetchone()[0]
485                 self.caches['content_path_names'].SetValue(values, id)
486
487             return id
488         except:
489             traceback.print_exc()
490             raise
491
492     def get_suite_architectures(self, suite):
493         """
494         Returns list of architectures for C{suite}.
495
496         @type suite: string, int
497         @param suite: the suite name or the suite_id
498
499         @rtype: list
500         @return: the list of architectures for I{suite}
501         """
502
503         suite_id = None
504         if type(suite) == str:
505             suite_id = self.get_suite_id(suite)
506         elif type(suite) == int:
507             suite_id = suite
508         else:
509             return None
510
511         c = self.db_con.cursor()
512         c.execute( """SELECT a.arch_string FROM suite_architectures sa
513                       JOIN architecture a ON (a.id = sa.architecture)
514                       WHERE suite='%s'""" % suite_id )
515
516         return map(lambda x: x[0], c.fetchall())
517
518     def insert_content_paths(self, bin_id, fullpaths):
519         """
520         Make sure given path is associated with given binary id
521
522         @type bin_id: int
523         @param bin_id: the id of the binary
524         @type fullpath: string
525         @param fullpath: the path of the file being associated with the binary
526
527         @return True upon success
528         """
529
530         c = self.db_con.cursor()
531
532         c.execute("BEGIN WORK")
533         try:
534
535             for fullpath in fullpaths:
536                 (path, file) = os.path.split(fullpath)
537
538                 # Get the necessary IDs ...
539                 file_id = self.get_or_set_contents_file_id(file)
540                 path_id = self.get_or_set_contents_path_id(path)
541
542                 c.execute("""INSERT INTO content_associations
543                                (binary_pkg, filepath, filename)
544                            VALUES ( '%d', '%d', '%d')""" % (bin_id, path_id, file_id) )
545
546             c.execute("COMMIT")
547             return True
548         except:
549             traceback.print_exc()
550             c.execute("ROLLBACK")
551             return False
552
553     def insert_pending_content_paths(self, package, fullpaths):
554         """
555         Make sure given paths are temporarily associated with given
556         package
557
558         @type package: dict
559         @param package: the package to associate with should have been read in from the binary control file
560         @type fullpaths: list
561         @param fullpaths: the list of paths of the file being associated with the binary
562
563         @return True upon success
564         """
565
566         c = self.db_con.cursor()
567
568         c.execute("BEGIN WORK")
569         try:
570             arch_id = self.get_architecture_id(package['Architecture'])
571
572             # Remove any already existing recorded files for this package
573             c.execute("""DELETE FROM pending_content_associations
574                          WHERE package=%(Package)s
575                          AND version=%(Version)s
576                          AND architecture=%(ArchID)s""", {'Package': package['Package'],
577                                                           'Version': package['Version'],
578                                                           'ArchID':  arch_id})
579
580             for fullpath in fullpaths:
581                 (path, file) = os.path.split(fullpath)
582
583                 if path.startswith( "./" ):
584                     path = path[2:]
585                 # Get the necessary IDs ...
586                 file_id = self.get_or_set_contents_file_id(file)
587                 path_id = self.get_or_set_contents_path_id(path)
588
589                 c.execute("""INSERT INTO pending_content_associations
590                                (package, version, architecture, filepath, filename)
591                             VALUES (%%(Package)s, %%(Version)s, '%d', '%d', '%d')"""
592                     % (arch_id, path_id, file_id), package )
593
594             c.execute("COMMIT")
595             return True
596         except:
597             traceback.print_exc()
598             c.execute("ROLLBACK")
599             return False
600
601 ################################################################################
602
603 class Suite(object):
604     # This should be kept in sync with the suites table;
605     # we should probably just do introspection on the table
606     # (or maybe use an ORM)
607     _fieldnames = ['announce', 'changelogbase', 'codename', 'commentsdir',
608                    'copychanges', 'copydotdak', 'description', 'id',
609                    'label', 'notautomatic', 'origin', 'overridecodename',
610                    'overridesuite', 'policy_engine', 'priority', 'suite_name',
611                    'untouchable', 'validtime', 'version']
612
613     def __init_fields(self):
614         for k in self._fieldnames:
615             setattr(self, k, None)
616
617     def __init__(self, suite):
618         self.__init_fields()
619         if suite is not None:
620             db_conn = DBConn()
621             suite_data = db_conn.get_suite(suite)
622             if suite_data is not None:
623                 for k in suite_data.keys():
624                     setattr(self, k, suite_data[k])
625
626
627