]> git.decadent.org.uk Git - dak.git/blob - dak/clean_suites.py
Merge commit 'mhy/sqlalchemy' 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 and the files should not be auto-deleted.
151     # XXX: In that case, remove the stupid return to later and actually
152     #      *TELL* us rather than silently ignore it - mhy
153
154     print "Checking for unused files..."
155     q = session.execute("""
156 SELECT id, filename FROM files f
157   WHERE NOT EXISTS (SELECT 1 FROM binaries b WHERE b.file = f.id)
158     AND NOT EXISTS (SELECT 1 FROM dsc_files df WHERE df.file = f.id)
159     ORDER BY filename""")
160
161     ql = q.fetchall()
162     if len(ql) > 0:
163         print "WARNING: check_files found something it shouldn't"
164         for x in ql:
165             print x
166
167     # NOW return, the code below is left as an example of what was
168     # evidently done at some point in the past
169     return
170
171 #    for i in q.fetchall():
172 #        file_id = i[0]
173 #        session.execute("UPDATE files SET last_used = :lastused WHERE id = :fileid",
174 #                        {'lastused': now_date, 'fileid': file_id})
175 #
176 #    session.commit()
177
178 def clean_binaries(now_date, delete_date, max_delete, session):
179     # We do this here so that the binaries we remove will have their
180     # source also removed (if possible).
181
182     # XXX: why doesn't this remove the files here as well? I don't think it
183     #      buys anything keeping this separate
184     print "Cleaning binaries from the DB..."
185     if not Options["No-Action"]:
186         print "Deleting from binaries table... "
187         session.execute("""DELETE FROM binaries WHERE EXISTS
188                               (SELECT 1 FROM files WHERE binaries.file = files.id
189                                          AND files.last_used <= :deldate)""",
190                            {'deldate': delete_date})
191
192 ########################################
193
194 def clean(now_date, delete_date, max_delete, session):
195     cnf = Config()
196
197     count = 0
198     size = 0
199
200     print "Cleaning out packages..."
201
202     cur_date = now_date.strftime("%Y-%m-%d")
203     dest = os.path.join(cnf["Dir::Morgue"], cnf["Clean-Suites::MorgueSubDir"], cur_date)
204     if not os.path.exists(dest):
205         os.mkdir(dest)
206
207     # Delete from source
208     if not Options["No-Action"]:
209         print "Deleting from source table... "
210         session.execute("""DELETE FROM dsc_files
211                             WHERE EXISTS
212                                (SELECT 1 FROM source s, files f, dsc_files df
213                                  WHERE f.last_used <= :deletedate
214                                    AND s.file = f.id AND s.id = df.source
215                                    AND df.id = dsc_files.id)""", {'deletedate': delete_date})
216         session.execute("""DELETE FROM source
217                             WHERE EXISTS
218                                (SELECT 1 FROM files
219                                  WHERE source.file = files.id
220                                    AND files.last_used <= :deletedate)""", {'deletedate': delete_date})
221
222         session.commit()
223
224     # Delete files from the pool
225     query = """SELECT l.path, f.filename FROM location l, files f
226               WHERE f.last_used <= :deletedate AND l.id = f.location"""
227     if max_delete is not None:
228         query += " LIMIT %d" % max_delete
229         print "Limiting removals to %d" % max_delete
230
231     q = session.execute(query, {'deletedate': delete_date})
232     for i in q.fetchall():
233         filename = i[0] + i[1]
234         if not os.path.exists(filename):
235             utils.warn("can not find '%s'." % (filename))
236             continue
237         if os.path.isfile(filename):
238             if os.path.islink(filename):
239                 count += 1
240                 if Options["No-Action"]:
241                     print "Removing symlink %s..." % (filename)
242                 else:
243                     os.unlink(filename)
244             else:
245                 size += os.stat(filename)[stat.ST_SIZE]
246                 count += 1
247
248                 dest_filename = dest + '/' + os.path.basename(filename)
249                 # If the destination file exists; try to find another filename to use
250                 if os.path.exists(dest_filename):
251                     dest_filename = utils.find_next_free(dest_filename)
252
253                 if Options["No-Action"]:
254                     print "Cleaning %s -> %s ..." % (filename, dest_filename)
255                 else:
256                     utils.move(filename, dest_filename)
257         else:
258             utils.fubar("%s is neither symlink nor file?!" % (filename))
259
260     # Delete from the 'files' table
261     # XXX: I've a horrible feeling that the max_delete stuff breaks here - mhy
262     # TODO: Change it so we do the DELETEs as we go; it'll be slower but
263     #       more reliable
264     if not Options["No-Action"]:
265         print "Deleting from files table... "
266         session.execute("DELETE FROM files WHERE last_used <= :deletedate", {'deletedate': delete_date})
267         session.commit()
268
269     if count > 0:
270         print "Cleaned %d files, %s." % (count, utils.size_type(size))
271
272 ################################################################################
273
274 def clean_maintainers(now_date, delete_date, max_delete, session):
275     print "Cleaning out unused Maintainer entries..."
276
277     # TODO Replace this whole thing with one SQL statement
278     q = session.execute("""
279 SELECT m.id FROM maintainer m
280   WHERE NOT EXISTS (SELECT 1 FROM binaries b WHERE b.maintainer = m.id)
281     AND NOT EXISTS (SELECT 1 FROM source s WHERE s.maintainer = m.id OR s.changedby = m.id)
282     AND NOT EXISTS (SELECT 1 FROM src_uploaders u WHERE u.maintainer = m.id)""")
283
284     count = 0
285
286     for i in q.fetchall():
287         maintainer_id = i[0]
288         if not Options["No-Action"]:
289             session.execute("DELETE FROM maintainer WHERE id = :maint", {'maint': maintainer_id})
290             count += 1
291
292     if not Options["No-Action"]:
293         session.commit()
294
295     if count > 0:
296         print "Cleared out %d maintainer entries." % (count)
297
298 ################################################################################
299
300 def clean_fingerprints(now_date, delete_date, max_delete, session):
301     print "Cleaning out unused fingerprint entries..."
302
303     # TODO Replace this whole thing with one SQL statement
304     q = session.execute("""
305 SELECT f.id FROM fingerprint f
306   WHERE f.keyring IS NULL
307     AND NOT EXISTS (SELECT 1 FROM binaries b WHERE b.sig_fpr = f.id)
308     AND NOT EXISTS (SELECT 1 FROM source s WHERE s.sig_fpr = f.id)""")
309
310     count = 0
311
312     for i in q.fetchall():
313         fingerprint_id = i[0]
314         if not Options["No-Action"]:
315             session.execute("DELETE FROM fingerprint WHERE id = :fpr", {'fpr': fingerprint_id})
316             count += 1
317
318     if not Options["No-Action"]:
319         session.commit()
320
321     if count > 0:
322         print "Cleared out %d fingerprint entries." % (count)
323
324 ################################################################################
325
326 def clean_queue_build(now_date, delete_date, max_delete, session):
327
328     cnf = Config()
329
330     if not cnf.ValueList("Dinstall::QueueBuildSuites") or Options["No-Action"]:
331         return
332
333     print "Cleaning out queue build symlinks..."
334
335     our_delete_date = now_date - timedelta(seconds = int(cnf["Clean-Suites::QueueBuildStayOfExecution"]))
336     count = 0
337
338     q = session.execute("SELECT filename FROM queue_build WHERE last_used <= :deletedate",
339                         {'deletedate': our_delete_date})
340     for i in q.fetchall():
341         filename = i[0]
342         if not os.path.exists(filename):
343             utils.warn("%s (from queue_build) doesn't exist." % (filename))
344             continue
345
346         if not cnf.FindB("Dinstall::SecurityQueueBuild") and not os.path.islink(filename):
347             utils.fubar("%s (from queue_build) should be a symlink but isn't." % (filename))
348
349         os.unlink(filename)
350         count += 1
351
352     session.execute("DELETE FROM queue_build WHERE last_used <= :deletedate",
353                     {'deletedate': our_delete_date})
354
355     session.commit()
356
357     if count:
358         print "Cleaned %d queue_build files." % (count)
359
360 ################################################################################
361
362 def main():
363     global Options
364
365     cnf = Config()
366
367     for i in ["Help", "No-Action", "Maximum" ]:
368         if not cnf.has_key("Clean-Suites::Options::%s" % (i)):
369             cnf["Clean-Suites::Options::%s" % (i)] = ""
370
371     Arguments = [('h',"help","Clean-Suites::Options::Help"),
372                  ('n',"no-action","Clean-Suites::Options::No-Action"),
373                  ('m',"maximum","Clean-Suites::Options::Maximum", "HasArg")]
374
375     apt_pkg.ParseCommandLine(cnf.Cnf, Arguments, sys.argv)
376     Options = cnf.SubTree("Clean-Suites::Options")
377
378     if cnf["Clean-Suites::Options::Maximum"] != "":
379         try:
380             # Only use Maximum if it's an integer
381             max_delete = int(cnf["Clean-Suites::Options::Maximum"])
382             if max_delete < 1:
383                 utils.fubar("If given, Maximum must be at least 1")
384         except ValueError, e:
385             utils.fubar("If given, Maximum must be an integer")
386     else:
387         max_delete = None
388
389     if Options["Help"]:
390         usage()
391
392     session = DBConn().session()
393
394     now_date = datetime.now()
395     delete_date = now_date - timedelta(seconds=int(cnf['Clean-Suites::StayOfExecution']))
396
397     check_binaries(now_date, delete_date, max_delete, session)
398     clean_binaries(now_date, delete_date, max_delete, session)
399     check_sources(now_date, delete_date, max_delete, session)
400     check_files(now_date, delete_date, max_delete, session)
401     clean(now_date, delete_date, max_delete, session)
402     clean_maintainers(now_date, delete_date, max_delete, session)
403     clean_fingerprints(now_date, delete_date, max_delete, session)
404     clean_queue_build(now_date, delete_date, max_delete, session)
405
406 ################################################################################
407
408 if __name__ == '__main__':
409     main()