]> git.decadent.org.uk Git - dak.git/blob - dak/clean_suites.py
Merge commit 'mhy/master' into merge
[dak.git] / dak / clean_suites.py
1 #!/usr/bin/env python
2
3 """ Cleans up unassociated binary and source packages """
4 # Copyright (C) 2000, 2001, 2002, 2003, 2006  James Troup <james@nocrew.org>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 ################################################################################
21
22 # 07:05|<elmo> well.. *shrug*.. no, probably not.. but to fix it,
23 #      |       we're going to have to implement reference counting
24 #      |       through dependencies.. do we really want to go down
25 #      |       that road?
26 #
27 # 07:05|<Culus> elmo: Augh! <brain jumps out of skull>
28
29 ################################################################################
30
31 import os, stat, sys, time
32 import apt_pkg
33 from datetime import datetime, timedelta
34
35 from daklib.config import Config
36 from daklib.dbconn import *
37 from daklib import utils
38
39 ################################################################################
40
41 Options = None
42
43 ################################################################################
44
45 def usage (exit_code=0):
46     print """Usage: dak clean-suites [OPTIONS]
47 Clean old packages from suites.
48
49   -n, --no-action            don't do anything
50   -h, --help                 show this help and exit
51   -m, --maximum              maximum number of files to remove"""
52     sys.exit(exit_code)
53
54 ################################################################################
55
56 def check_binaries(now_date, delete_date, max_delete, session):
57     print "Checking for orphaned binary packages..."
58
59     # Get the list of binary packages not in a suite and mark them for
60     # deletion.
61
62     # TODO: This can be a single SQL UPDATE statement
63     q = session.execute("""
64 SELECT b.file FROM binaries b, files f
65  WHERE f.last_used IS NULL AND b.file = f.id
66    AND NOT EXISTS (SELECT 1 FROM bin_associations ba WHERE ba.bin = b.id)""")
67
68     for i in q.fetchall():
69         session.execute("UPDATE files SET last_used = :lastused WHERE id = :fileid AND last_used IS NULL",
70                         {'lastused': now_date, 'fileid': i[0]})
71     session.commit()
72
73     # Check for any binaries which are marked for eventual deletion
74     # but are now used again.
75
76     # TODO: This can be a single SQL UPDATE statement
77     q = session.execute("""
78 SELECT b.file FROM binaries b, files f
79    WHERE f.last_used IS NOT NULL AND f.id = b.file
80     AND EXISTS (SELECT 1 FROM bin_associations ba WHERE ba.bin = b.id)""")
81
82     for i in q.fetchall():
83         session.execute("UPDATE files SET last_used = NULL WHERE id = :fileid", {'fileid': i[0]})
84     session.commit()
85
86 ########################################
87
88 def check_sources(now_date, delete_date, max_delete, session):
89     print "Checking for orphaned source packages..."
90
91     # Get the list of source packages not in a suite and not used by
92     # any binaries.
93     q = session.execute("""
94 SELECT s.id, s.file FROM source s, files f
95   WHERE f.last_used IS NULL AND s.file = f.id
96     AND NOT EXISTS (SELECT 1 FROM src_associations sa WHERE sa.source = s.id)
97     AND NOT EXISTS (SELECT 1 FROM binaries b WHERE b.source = s.id)""")
98
99     #### XXX: this should ignore cases where the files for the binary b
100     ####      have been marked for deletion (so the delay between bins go
101     ####      byebye and sources go byebye is 0 instead of StayOfExecution)
102
103     for i in q.fetchall():
104         source_id = i[0]
105         dsc_file_id = i[1]
106
107         # Mark the .dsc file for deletion
108         session.execute("""UPDATE files SET last_used = :last_used
109                                     WHERE id = :dscfileid AND last_used IS NULL""",
110                         {'last_used': now_date, 'dscfileid': dsc_file_id})
111
112         # Mark all other files references by .dsc too if they're not used by anyone else
113         x = session.execute("""SELECT f.id FROM files f, dsc_files d
114                               WHERE d.source = :sourceid AND d.file = f.id""",
115                              {'sourceid': source_id})
116         for j in x.fetchall():
117             file_id = j[0]
118             y = session.execute("SELECT id FROM dsc_files d WHERE d.file = :fileid", {'fileid': file_id})
119             if len(y.fetchall()) == 1:
120                 session.execute("""UPDATE files SET last_used = :lastused
121                                   WHERE id = :fileid AND last_used IS NULL""",
122                                 {'lastused': now_date, 'fileid': file_id})
123
124     session.commit()
125
126     # Check for any sources which are marked for deletion but which
127     # are now used again.
128
129     q = session.execute("""
130 SELECT f.id FROM source s, files f, dsc_files df
131   WHERE f.last_used IS NOT NULL AND s.id = df.source AND df.file = f.id
132     AND ((EXISTS (SELECT 1 FROM src_associations sa WHERE sa.source = s.id))
133       OR (EXISTS (SELECT 1 FROM binaries b WHERE b.source = s.id)))""")
134
135     #### XXX: this should also handle deleted binaries specially (ie, not
136     ####      reinstate sources because of them
137
138     # Could be done in SQL; but left this way for hysterical raisins
139     # [and freedom to innovate don'cha know?]
140     for i in q.fetchall():
141         session.execute("UPDATE files SET last_used = NULL WHERE id = :fileid",
142                         {'fileid': i[0]})
143
144     session.commit()
145
146 ########################################
147
148 def check_files(now_date, delete_date, max_delete, session):
149     # FIXME: this is evil; nothing should ever be in this state.  if
150     # they are, it's a bug.
151
152     # However, we've discovered it happens sometimes so we print a huge warning
153     # and then mark the file for deletion.  This probably masks a bug somwhere
154     # else but is better than collecting cruft forever
155
156     print "Checking for unused files..."
157     q = session.execute("""
158 SELECT id, filename FROM files f
159   WHERE NOT EXISTS (SELECT 1 FROM binaries b WHERE b.file = f.id)
160     AND NOT EXISTS (SELECT 1 FROM dsc_files df WHERE df.file = f.id)
161     ORDER BY filename""")
162
163     ql = q.fetchall()
164     if len(ql) > 0:
165         print "WARNING: check_files found something it shouldn't"
166         for x in ql:
167             print x
168             session.execute("UPDATE files SET last_used = :lastused WHERE id = :fileid",
169                             {'lastused': now_date, 'fileid': x[0]})
170
171         session.commit()
172
173 def clean_binaries(now_date, delete_date, max_delete, session):
174     # We do this here so that the binaries we remove will have their
175     # source also removed (if possible).
176
177     # XXX: why doesn't this remove the files here as well? I don't think it
178     #      buys anything keeping this separate
179     print "Cleaning binaries from the DB..."
180     if not Options["No-Action"]:
181         print "Deleting from binaries table... "
182         session.execute("""DELETE FROM binaries WHERE EXISTS
183                               (SELECT 1 FROM files WHERE binaries.file = files.id
184                                          AND files.last_used <= :deldate)""",
185                            {'deldate': delete_date})
186
187 ########################################
188
189 def clean(now_date, delete_date, max_delete, session):
190     cnf = Config()
191
192     count = 0
193     size = 0
194
195     print "Cleaning out packages..."
196
197     cur_date = now_date.strftime("%Y-%m-%d")
198     dest = os.path.join(cnf["Dir::Morgue"], cnf["Clean-Suites::MorgueSubDir"], cur_date)
199     if not os.path.exists(dest):
200         os.mkdir(dest)
201
202     # Delete from source
203     if not Options["No-Action"]:
204         print "Deleting from source table... "
205         session.execute("""DELETE FROM dsc_files
206                             WHERE EXISTS
207                                (SELECT 1 FROM source s, files f, dsc_files df
208                                  WHERE f.last_used <= :deletedate
209                                    AND s.file = f.id AND s.id = df.source
210                                    AND df.id = dsc_files.id)""", {'deletedate': delete_date})
211         session.execute("""DELETE FROM source
212                             WHERE EXISTS
213                                (SELECT 1 FROM files
214                                  WHERE source.file = files.id
215                                    AND files.last_used <= :deletedate)""", {'deletedate': delete_date})
216
217         session.commit()
218
219     # Delete files from the pool
220     query = """SELECT l.path, f.filename FROM location l, files f
221               WHERE f.last_used <= :deletedate AND l.id = f.location"""
222     if max_delete is not None:
223         query += " LIMIT %d" % max_delete
224         print "Limiting removals to %d" % max_delete
225
226     q = session.execute(query, {'deletedate': delete_date})
227     for i in q.fetchall():
228         filename = i[0] + i[1]
229         if not os.path.exists(filename):
230             utils.warn("can not find '%s'." % (filename))
231             continue
232         if os.path.isfile(filename):
233             if os.path.islink(filename):
234                 count += 1
235                 if Options["No-Action"]:
236                     print "Removing symlink %s..." % (filename)
237                 else:
238                     os.unlink(filename)
239             else:
240                 size += os.stat(filename)[stat.ST_SIZE]
241                 count += 1
242
243                 dest_filename = dest + '/' + os.path.basename(filename)
244                 # If the destination file exists; try to find another filename to use
245                 if os.path.exists(dest_filename):
246                     dest_filename = utils.find_next_free(dest_filename)
247
248                 if Options["No-Action"]:
249                     print "Cleaning %s -> %s ..." % (filename, dest_filename)
250                 else:
251                     utils.move(filename, dest_filename)
252         else:
253             utils.fubar("%s is neither symlink nor file?!" % (filename))
254
255     # Delete from the 'files' table
256     # XXX: I've a horrible feeling that the max_delete stuff breaks here - mhy
257     # TODO: Change it so we do the DELETEs as we go; it'll be slower but
258     #       more reliable
259     if not Options["No-Action"]:
260         print "Deleting from files table... "
261         session.execute("DELETE FROM files WHERE last_used <= :deletedate", {'deletedate': delete_date})
262         session.commit()
263
264     if count > 0:
265         print "Cleaned %d files, %s." % (count, utils.size_type(size))
266
267 ################################################################################
268
269 def clean_maintainers(now_date, delete_date, max_delete, session):
270     print "Cleaning out unused Maintainer entries..."
271
272     # TODO Replace this whole thing with one SQL statement
273     q = session.execute("""
274 SELECT m.id FROM maintainer m
275   WHERE NOT EXISTS (SELECT 1 FROM binaries b WHERE b.maintainer = m.id)
276     AND NOT EXISTS (SELECT 1 FROM source s WHERE s.maintainer = m.id OR s.changedby = m.id)
277     AND NOT EXISTS (SELECT 1 FROM src_uploaders u WHERE u.maintainer = m.id)""")
278
279     count = 0
280
281     for i in q.fetchall():
282         maintainer_id = i[0]
283         if not Options["No-Action"]:
284             session.execute("DELETE FROM maintainer WHERE id = :maint", {'maint': maintainer_id})
285             count += 1
286
287     if not Options["No-Action"]:
288         session.commit()
289
290     if count > 0:
291         print "Cleared out %d maintainer entries." % (count)
292
293 ################################################################################
294
295 def clean_fingerprints(now_date, delete_date, max_delete, session):
296     print "Cleaning out unused fingerprint entries..."
297
298     # TODO Replace this whole thing with one SQL statement
299     q = session.execute("""
300 SELECT f.id FROM fingerprint f
301   WHERE f.keyring IS NULL
302     AND NOT EXISTS (SELECT 1 FROM binaries b WHERE b.sig_fpr = f.id)
303     AND NOT EXISTS (SELECT 1 FROM source s WHERE s.sig_fpr = f.id)""")
304
305     count = 0
306
307     for i in q.fetchall():
308         fingerprint_id = i[0]
309         if not Options["No-Action"]:
310             session.execute("DELETE FROM fingerprint WHERE id = :fpr", {'fpr': fingerprint_id})
311             count += 1
312
313     if not Options["No-Action"]:
314         session.commit()
315
316     if count > 0:
317         print "Cleared out %d fingerprint entries." % (count)
318
319 ################################################################################
320
321 def clean_queue_build(now_date, delete_date, max_delete, session):
322
323     cnf = Config()
324
325     if not cnf.ValueList("Dinstall::QueueBuildSuites") or Options["No-Action"]:
326         return
327
328     print "Cleaning out queue build symlinks..."
329
330     our_delete_date = now_date - timedelta(seconds = int(cnf["Clean-Suites::QueueBuildStayOfExecution"]))
331     count = 0
332
333     q = session.execute("SELECT filename FROM queue_build WHERE last_used <= :deletedate",
334                         {'deletedate': our_delete_date})
335     for i in q.fetchall():
336         filename = i[0]
337         if not os.path.exists(filename):
338             utils.warn("%s (from queue_build) doesn't exist." % (filename))
339             continue
340
341         if not cnf.FindB("Dinstall::SecurityQueueBuild") and not os.path.islink(filename):
342             utils.fubar("%s (from queue_build) should be a symlink but isn't." % (filename))
343
344         os.unlink(filename)
345         count += 1
346
347     session.execute("DELETE FROM queue_build WHERE last_used <= :deletedate",
348                     {'deletedate': our_delete_date})
349
350     session.commit()
351
352     if count:
353         print "Cleaned %d queue_build files." % (count)
354
355 ################################################################################
356
357 def main():
358     global Options
359
360     cnf = Config()
361
362     for i in ["Help", "No-Action", "Maximum" ]:
363         if not cnf.has_key("Clean-Suites::Options::%s" % (i)):
364             cnf["Clean-Suites::Options::%s" % (i)] = ""
365
366     Arguments = [('h',"help","Clean-Suites::Options::Help"),
367                  ('n',"no-action","Clean-Suites::Options::No-Action"),
368                  ('m',"maximum","Clean-Suites::Options::Maximum", "HasArg")]
369
370     apt_pkg.ParseCommandLine(cnf.Cnf, Arguments, sys.argv)
371     Options = cnf.SubTree("Clean-Suites::Options")
372
373     if cnf["Clean-Suites::Options::Maximum"] != "":
374         try:
375             # Only use Maximum if it's an integer
376             max_delete = int(cnf["Clean-Suites::Options::Maximum"])
377             if max_delete < 1:
378                 utils.fubar("If given, Maximum must be at least 1")
379         except ValueError, e:
380             utils.fubar("If given, Maximum must be an integer")
381     else:
382         max_delete = None
383
384     if Options["Help"]:
385         usage()
386
387     session = DBConn().session()
388
389     now_date = datetime.now()
390     delete_date = now_date - timedelta(seconds=int(cnf['Clean-Suites::StayOfExecution']))
391
392     check_binaries(now_date, delete_date, max_delete, session)
393     clean_binaries(now_date, delete_date, max_delete, session)
394     check_sources(now_date, delete_date, max_delete, session)
395     check_files(now_date, delete_date, max_delete, session)
396     clean(now_date, delete_date, max_delete, session)
397     clean_maintainers(now_date, delete_date, max_delete, session)
398     clean_fingerprints(now_date, delete_date, max_delete, session)
399     clean_queue_build(now_date, delete_date, max_delete, session)
400
401 ################################################################################
402
403 if __name__ == '__main__':
404     main()