]> git.decadent.org.uk Git - dak.git/blob - dak/check_archive.py
show_deferred: drop workaround for old python-debian bug
[dak.git] / dak / check_archive.py
1 #!/usr/bin/env python
2
3 """ Various different sanity checks
4
5 @contact: Debian FTP Master <ftpmaster@debian.org>
6 @copyright: (C) 2000, 2001, 2002, 2003, 2004, 2006  James Troup <james@nocrew.org>
7 @license: GNU General Public License version 2 or later
8 """
9
10 # This program is free software; you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation; either version 2 of the License, or
13 # (at your option) any later version.
14
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 # GNU General Public License for more details.
19
20 # You should have received a copy of the GNU General Public License
21 # along with this program; if not, write to the Free Software
22 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
23
24 ################################################################################
25
26 #   And, lo, a great and menacing voice rose from the depths, and with
27 #   great wrath and vehemence it's voice boomed across the
28 #   land... ``hehehehehehe... that *tickles*''
29 #                                                       -- aj on IRC
30
31 ################################################################################
32
33 import commands
34 import os
35 import stat
36 import sys
37 import time
38 import apt_pkg
39 import apt_inst
40
41 from daklib.dbconn import *
42 from daklib import utils
43 from daklib.config import Config
44 from daklib.dak_exceptions import InvalidDscError, ChangesUnicodeError, CantOpenError
45
46 ################################################################################
47
48 db_files = {}                  #: Cache of filenames as known by the database
49 waste = 0.0                    #: How many bytes are "wasted" by files not referenced in database
50 excluded = {}                  #: List of files which are excluded from files check
51 current_file = None
52 future_files = {}
53 current_time = time.time()     #: now()
54
55 ################################################################################
56
57 def usage(exit_code=0):
58     print """Usage: dak check-archive MODE
59 Run various sanity checks of the archive and/or database.
60
61   -h, --help                show this help and exit.
62
63 The following MODEs are available:
64
65   checksums          - validate the checksums stored in the database
66   files              - check files in the database against what's in the archive
67   dsc-syntax         - validate the syntax of .dsc files in the archive
68   missing-overrides  - check for missing overrides
69   source-in-one-dir  - ensure the source for each package is in one directory
70   timestamps         - check for future timestamps in .deb's
71   files-in-dsc       - ensure each .dsc references appropriate Files
72   validate-indices   - ensure files mentioned in Packages & Sources exist
73   files-not-symlinks - check files in the database aren't symlinks
74   validate-builddeps - validate build-dependencies of .dsc files in the archive
75   add-missing-source-checksums - add missing checksums for source packages
76 """
77     sys.exit(exit_code)
78
79 ################################################################################
80
81 def process_dir (unused, dirname, filenames):
82     """
83     Process a directory and output every files name which is not listed already
84     in the C{filenames} or global C{excluded} dictionaries.
85
86     @type dirname: string
87     @param dirname: the directory to look at
88
89     @type filenames: dict
90     @param filenames: Known filenames to ignore
91     """
92     global waste, db_files, excluded
93
94     if dirname.find('/disks-') != -1 or dirname.find('upgrade-') != -1:
95         return
96     # hack; can't handle .changes files
97     if dirname.find('proposed-updates') != -1:
98         return
99     for name in filenames:
100         filename = os.path.abspath(os.path.join(dirname,name))
101         if os.path.isfile(filename) and not os.path.islink(filename) and not db_files.has_key(filename) and not excluded.has_key(filename):
102             waste += os.stat(filename)[stat.ST_SIZE]
103             print "%s" % (filename)
104
105 ################################################################################
106
107 def check_files():
108     """
109     Prepare the dictionary of existing filenames, then walk through the archive
110     pool/ directory to compare it.
111     """
112     cnf = Config()
113     session = DBConn().session()
114
115     query = """
116         SELECT archive.name, suite.suite_name, f.filename
117           FROM binaries b
118           JOIN bin_associations ba ON b.id = ba.bin
119           JOIN suite ON ba.suite = suite.id
120           JOIN archive ON suite.archive_id = archive.id
121           JOIN files f ON b.file = f.id
122          WHERE NOT EXISTS (SELECT 1 FROM files_archive_map af
123                             WHERE af.archive_id = suite.archive_id
124                               AND af.file_id = b.file)
125          ORDER BY archive.name, suite.suite_name, f.filename
126         """
127     for row in session.execute(query):
128         print "MISSING-ARCHIVE-FILE {0} {1} {2}".vformat(row)
129
130     query = """
131         SELECT archive.name, suite.suite_name, f.filename
132           FROM source s
133           JOIN src_associations sa ON s.id = sa.source
134           JOIN suite ON sa.suite = suite.id
135           JOIN archive ON suite.archive_id = archive.id
136           JOIN dsc_files df ON s.id = df.source
137           JOIN files f ON df.file = f.id
138          WHERE NOT EXISTS (SELECT 1 FROM files_archive_map af
139                             WHERE af.archive_id = suite.archive_id
140                               AND af.file_id = df.file)
141          ORDER BY archive.name, suite.suite_name, f.filename
142         """
143     for row in session.execute(query):
144         print "MISSING-ARCHIVE-FILE {0} {1} {2}".vformat(row)
145
146     archive_files = session.query(ArchiveFile) \
147         .join(ArchiveFile.archive).join(ArchiveFile.file) \
148         .order_by(Archive.archive_name, PoolFile.filename)
149
150     expected_files = set()
151     for af in archive_files:
152         path = af.path
153         expected_files.add(af.path)
154         if not os.path.exists(path):
155             print "MISSING-FILE {0} {1} {2}".format(af.archive.archive_name, af.file.filename, path)
156
157     archives = session.query(Archive).order_by(Archive.archive_name)
158
159     for a in archives:
160         top = os.path.join(a.path, 'pool')
161         for dirpath, dirnames, filenames in os.walk(top):
162             for fn in filenames:
163                 path = os.path.join(dirpath, fn)
164                 if path in expected_files:
165                     continue
166                 print "UNEXPECTED-FILE {0} {1}".format(a.archive_name, path)
167
168 ################################################################################
169
170 def check_dscs():
171     """
172     Parse every .dsc file in the archive and check for it's validity.
173     """
174
175     count = 0
176
177     for src in DBConn().session().query(DBSource).order_by(DBSource.source, DBSource.version):
178         f = src.poolfile.fullpath
179         try:
180             utils.parse_changes(f, signing_rules=1, dsc_file=1)
181         except InvalidDscError:
182             utils.warn("syntax error in .dsc file %s" % f)
183             count += 1
184         except ChangesUnicodeError:
185             utils.warn("found invalid dsc file (%s), not properly utf-8 encoded" % f)
186             count += 1
187         except CantOpenError:
188             utils.warn("missing dsc file (%s)" % f)
189             count += 1
190         except Exception as e:
191             utils.warn("miscellaneous error parsing dsc file (%s): %s" % (f, str(e)))
192             count += 1
193
194     if count:
195         utils.warn("Found %s invalid .dsc files." % (count))
196
197 ################################################################################
198
199 def check_override():
200     """
201     Check for missing overrides in stable and unstable.
202     """
203     session = DBConn().session()
204
205     for suite_name in [ "stable", "unstable" ]:
206         print suite_name
207         print "-" * len(suite_name)
208         print
209         suite = get_suite(suite_name)
210         q = session.execute("""
211 SELECT DISTINCT b.package FROM binaries b, bin_associations ba
212  WHERE b.id = ba.bin AND ba.suite = :suiteid AND NOT EXISTS
213        (SELECT 1 FROM override o WHERE o.suite = :suiteid AND o.package = b.package)"""
214                           % {'suiteid': suite.suite_id})
215
216         for j in q.fetchall():
217             print j[0]
218
219         q = session.execute("""
220 SELECT DISTINCT s.source FROM source s, src_associations sa
221   WHERE s.id = sa.source AND sa.suite = :suiteid AND NOT EXISTS
222        (SELECT 1 FROM override o WHERE o.suite = :suiteid and o.package = s.source)"""
223                           % {'suiteid': suite.suite_id})
224         for j in q.fetchall():
225             print j[0]
226
227 ################################################################################
228
229
230 def check_source_in_one_dir():
231     """
232     Ensure that the source files for any given package is all in one
233     directory so that 'apt-get source' works...
234     """
235
236     # Not the most enterprising method, but hey...
237     broken_count = 0
238
239     session = DBConn().session()
240
241     q = session.query(DBSource)
242     for s in q.all():
243         first_path = ""
244         first_filename = ""
245         broken = False
246
247         qf = session.query(PoolFile).join(Location).join(DSCFile).filter_by(source_id=s.source_id)
248         for f in qf.all():
249             # 0: path
250             # 1: filename
251             filename = os.path.join(f.location.path, f.filename)
252             path = os.path.dirname(filename)
253
254             if first_path == "":
255                 first_path = path
256                 first_filename = filename
257             elif first_path != path:
258                 symlink = path + '/' + os.path.basename(first_filename)
259                 if not os.path.exists(symlink):
260                     broken = True
261                     print "WOAH, we got a live one here... %s [%s] {%s}" % (filename, s.source_id, symlink)
262         if broken:
263             broken_count += 1
264
265     print "Found %d source packages where the source is not all in one directory." % (broken_count)
266
267 ################################################################################
268 def check_checksums():
269     """
270     Validate all files
271     """
272     print "Getting file information from database..."
273     q = DBConn().session().query(PoolFile)
274
275     print "Checking file checksums & sizes..."
276     for f in q:
277         filename = f.fullpath
278
279         try:
280             fi = utils.open_file(filename)
281         except:
282             utils.warn("can't open '%s'." % (filename))
283             continue
284
285         size = os.stat(filename)[stat.ST_SIZE]
286         if size != f.filesize:
287             utils.warn("**WARNING** size mismatch for '%s' ('%s' [current] vs. '%s' [db])." % (filename, size, f.filesize))
288
289         md5sum = apt_pkg.md5sum(fi)
290         if md5sum != f.md5sum:
291             utils.warn("**WARNING** md5sum mismatch for '%s' ('%s' [current] vs. '%s' [db])." % (filename, md5sum, f.md5sum))
292
293         fi.seek(0)
294         sha1sum = apt_pkg.sha1sum(fi)
295         if sha1sum != f.sha1sum:
296             utils.warn("**WARNING** sha1sum mismatch for '%s' ('%s' [current] vs. '%s' [db])." % (filename, sha1sum, f.sha1sum))
297
298         fi.seek(0)
299         sha256sum = apt_pkg.sha256sum(fi)
300         if sha256sum != f.sha256sum:
301             utils.warn("**WARNING** sha256sum mismatch for '%s' ('%s' [current] vs. '%s' [db])." % (filename, sha256sum, f.sha256sum))
302
303     print "Done."
304
305 ################################################################################
306 #
307
308 def Ent(Kind,Name,Link,Mode,UID,GID,Size,MTime,Major,Minor):
309     global future_files
310
311     if MTime > current_time:
312         future_files[current_file] = MTime
313         print "%s: %s '%s','%s',%u,%u,%u,%u,%u,%u,%u" % (current_file, Kind,Name,Link,Mode,UID,GID,Size, MTime, Major, Minor)
314
315 def check_timestamps():
316     """
317     Check all files for timestamps in the future; common from hardware
318     (e.g. alpha) which have far-future dates as their default dates.
319     """
320
321     global current_file
322
323     q = DBConn().session().query(PoolFile).filter(PoolFile.filename.like('.deb$'))
324
325     db_files.clear()
326     count = 0
327
328     for pf in q.all():
329         filename = os.path.abspath(os.path.join(pf.location.path, pf.filename))
330         if os.access(filename, os.R_OK):
331             f = utils.open_file(filename)
332             current_file = filename
333             sys.stderr.write("Processing %s.\n" % (filename))
334             apt_inst.debExtract(f, Ent, "control.tar.gz")
335             f.seek(0)
336             apt_inst.debExtract(f, Ent, "data.tar.gz")
337             count += 1
338
339     print "Checked %d files (out of %d)." % (count, len(db_files.keys()))
340
341 ################################################################################
342
343 def check_files_in_dsc():
344     """
345     Ensure each .dsc lists appropriate files in its Files field (according
346     to the format announced in its Format field).
347     """
348     count = 0
349
350     print "Building list of database files..."
351     q = DBConn().session().query(PoolFile).filter(PoolFile.filename.like('.dsc$'))
352
353     if q.count() > 0:
354         print "Checking %d files..." % len(ql)
355     else:
356         print "No files to check."
357
358     for pf in q.all():
359         filename = os.path.abspath(os.path.join(pf.location.path + pf.filename))
360
361         try:
362             # NB: don't enforce .dsc syntax
363             dsc = utils.parse_changes(filename, dsc_file=1)
364         except:
365             utils.fubar("error parsing .dsc file '%s'." % (filename))
366
367         reasons = utils.check_dsc_files(filename, dsc)
368         for r in reasons:
369             utils.warn(r)
370
371         if len(reasons) > 0:
372             count += 1
373
374     if count:
375         utils.warn("Found %s invalid .dsc files." % (count))
376
377
378 ################################################################################
379
380 def validate_sources(suite, component):
381     """
382     Ensure files mentioned in Sources exist
383     """
384     filename = "%s/dists/%s/%s/source/Sources.gz" % (Cnf["Dir::Root"], suite, component)
385     print "Processing %s..." % (filename)
386     # apt_pkg.TagFile needs a real file handle and can't handle a GzipFile instance...
387     (fd, temp_filename) = utils.temp_filename()
388     (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_filename))
389     if (result != 0):
390         sys.stderr.write("Gunzip invocation failed!\n%s\n" % (output))
391         sys.exit(result)
392     sources = utils.open_file(temp_filename)
393     Sources = apt_pkg.TagFile(sources)
394     while Sources.step():
395         source = Sources.section.find('Package')
396         directory = Sources.section.find('Directory')
397         files = Sources.section.find('Files')
398         for i in files.split('\n'):
399             (md5, size, name) = i.split()
400             filename = "%s/%s/%s" % (Cnf["Dir::Root"], directory, name)
401             if not os.path.exists(filename):
402                 if directory.find("potato") == -1:
403                     print "W: %s missing." % (filename)
404                 else:
405                     pool_location = utils.poolify (source, component)
406                     pool_filename = "%s/%s/%s" % (Cnf["Dir::Pool"], pool_location, name)
407                     if not os.path.exists(pool_filename):
408                         print "E: %s missing (%s)." % (filename, pool_filename)
409                     else:
410                         # Create symlink
411                         pool_filename = os.path.normpath(pool_filename)
412                         filename = os.path.normpath(filename)
413                         src = utils.clean_symlink(pool_filename, filename, Cnf["Dir::Root"])
414                         print "Symlinking: %s -> %s" % (filename, src)
415                         #os.symlink(src, filename)
416     sources.close()
417     os.unlink(temp_filename)
418
419 ########################################
420
421 def validate_packages(suite, component, architecture):
422     """
423     Ensure files mentioned in Packages exist
424     """
425     filename = "%s/dists/%s/%s/binary-%s/Packages.gz" \
426                % (Cnf["Dir::Root"], suite, component, architecture)
427     print "Processing %s..." % (filename)
428     # apt_pkg.TagFile needs a real file handle and can't handle a GzipFile instance...
429     (fd, temp_filename) = utils.temp_filename()
430     (result, output) = commands.getstatusoutput("gunzip -c %s > %s" % (filename, temp_filename))
431     if (result != 0):
432         sys.stderr.write("Gunzip invocation failed!\n%s\n" % (output))
433         sys.exit(result)
434     packages = utils.open_file(temp_filename)
435     Packages = apt_pkg.TagFile(packages)
436     while Packages.step():
437         filename = "%s/%s" % (Cnf["Dir::Root"], Packages.section.find('Filename'))
438         if not os.path.exists(filename):
439             print "W: %s missing." % (filename)
440     packages.close()
441     os.unlink(temp_filename)
442
443 ########################################
444
445 def check_indices_files_exist():
446     """
447     Ensure files mentioned in Packages & Sources exist
448     """
449     for suite in [ "stable", "testing", "unstable" ]:
450         for component in get_component_names():
451             architectures = get_suite_architectures(suite)
452             for arch in [ i.arch_string.lower() for i in architectures ]:
453                 if arch == "source":
454                     validate_sources(suite, component)
455                 elif arch == "all":
456                     continue
457                 else:
458                     validate_packages(suite, component, arch)
459
460 ################################################################################
461
462 def check_files_not_symlinks():
463     """
464     Check files in the database aren't symlinks
465     """
466     print "Building list of database files... ",
467     before = time.time()
468     q = DBConn().session().query(PoolFile).filter(PoolFile.filename.like('.dsc$'))
469
470     for pf in q.all():
471         filename = os.path.abspath(os.path.join(pf.location.path, pf.filename))
472         if os.access(filename, os.R_OK) == 0:
473             utils.warn("%s: doesn't exist." % (filename))
474         else:
475             if os.path.islink(filename):
476                 utils.warn("%s: is a symlink." % (filename))
477
478 ################################################################################
479
480 def chk_bd_process_dir (unused, dirname, filenames):
481     for name in filenames:
482         if not name.endswith(".dsc"):
483             continue
484         filename = os.path.abspath(dirname+'/'+name)
485         dsc = utils.parse_changes(filename, dsc_file=1)
486         for field_name in [ "build-depends", "build-depends-indep" ]:
487             field = dsc.get(field_name)
488             if field:
489                 try:
490                     apt_pkg.parse_src_depends(field)
491                 except:
492                     print "E: [%s] %s: %s" % (filename, field_name, field)
493                     pass
494
495 ################################################################################
496
497 def check_build_depends():
498     """ Validate build-dependencies of .dsc files in the archive """
499     cnf = Config()
500     os.path.walk(cnf["Dir::Root"], chk_bd_process_dir, None)
501
502 ################################################################################
503
504 _add_missing_source_checksums_query = R"""
505 INSERT INTO source_metadata
506   (src_id, key_id, value)
507 SELECT
508   s.id,
509   :checksum_key,
510   E'\n' ||
511     (SELECT STRING_AGG(' ' || tmp.checksum || ' ' || tmp.size || ' ' || tmp.basename, E'\n' ORDER BY tmp.basename)
512      FROM
513        (SELECT
514             CASE :checksum_type
515               WHEN 'Files' THEN f.md5sum
516               WHEN 'Checksums-Sha1' THEN f.sha1sum
517               WHEN 'Checksums-Sha256' THEN f.sha256sum
518             END AS checksum,
519             f.size,
520             SUBSTRING(f.filename FROM E'/([^/]*)\\Z') AS basename
521           FROM files f JOIN dsc_files ON f.id = dsc_files.file
522           WHERE dsc_files.source = s.id AND f.id != s.file
523        ) AS tmp
524     )
525
526   FROM
527     source s
528   WHERE NOT EXISTS (SELECT 1 FROM source_metadata md WHERE md.src_id=s.id AND md.key_id = :checksum_key);
529 """
530
531 def add_missing_source_checksums():
532     """ Add missing source checksums to source_metadata """
533     session = DBConn().session()
534     for checksum in ['Files', 'Checksums-Sha1', 'Checksums-Sha256']:
535         checksum_key = get_or_set_metadatakey(checksum, session).key_id
536         rows = session.execute(_add_missing_source_checksums_query,
537             {'checksum_key': checksum_key, 'checksum_type': checksum}).rowcount
538         if rows > 0:
539             print "Added {0} missing entries for {1}".format(rows, checksum)
540     session.commit()
541
542 ################################################################################
543
544 def main ():
545     global db_files, waste, excluded
546
547     cnf = Config()
548
549     Arguments = [('h',"help","Check-Archive::Options::Help")]
550     for i in [ "help" ]:
551         if not cnf.has_key("Check-Archive::Options::%s" % (i)):
552             cnf["Check-Archive::Options::%s" % (i)] = ""
553
554     args = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv)
555
556     Options = cnf.subtree("Check-Archive::Options")
557     if Options["Help"]:
558         usage()
559
560     if len(args) < 1:
561         utils.warn("dak check-archive requires at least one argument")
562         usage(1)
563     elif len(args) > 1:
564         utils.warn("dak check-archive accepts only one argument")
565         usage(1)
566     mode = args[0].lower()
567
568     # Initialize DB
569     DBConn()
570
571     if mode == "checksums":
572         check_checksums()
573     elif mode == "files":
574         check_files()
575     elif mode == "dsc-syntax":
576         check_dscs()
577     elif mode == "missing-overrides":
578         check_override()
579     elif mode == "source-in-one-dir":
580         check_source_in_one_dir()
581     elif mode == "timestamps":
582         check_timestamps()
583     elif mode == "files-in-dsc":
584         check_files_in_dsc()
585     elif mode == "validate-indices":
586         check_indices_files_exist()
587     elif mode == "files-not-symlinks":
588         check_files_not_symlinks()
589     elif mode == "validate-builddeps":
590         check_build_depends()
591     elif mode == "add-missing-source-checksums":
592         add_missing_source_checksums()
593     else:
594         utils.warn("unknown mode '%s'" % (mode))
595         usage(1)
596
597 ################################################################################
598
599 if __name__ == '__main__':
600     main()