]> git.decadent.org.uk Git - dak.git/blob - dak/control_suite.py
5392259c6002ac20e15597c8123ef4eeb8ce63c3
[dak.git] / dak / control_suite.py
1 #!/usr/bin/env python
2
3 """ Manipulate suite tags """
4 # Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 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 # 8to6Guy: "Wow, Bob, You look rough!"
23 # BTAF: "Mbblpmn..."
24 # BTAF <.oO>: "You moron! This is what you get for staying up all night drinking vodka and salad dressing!"
25 # BTAF <.oO>: "This coffee I.V. drip is barely even keeping me awake! I need something with more kick! But what?"
26 # BTAF: "OMIGOD! I OVERDOSED ON HEROIN"
27 # CoWorker#n: "Give him air!!"
28 # CoWorker#n+1: "We need a syringe full of adrenaline!"
29 # CoWorker#n+2: "Stab him in the heart!"
30 # BTAF: "*YES!*"
31 # CoWorker#n+3: "Bob's been overdosing quite a bit lately..."
32 # CoWorker#n+4: "Third time this week."
33
34 # -- http://www.angryflower.com/8to6.gif
35
36 #######################################################################################
37
38 # Adds or removes packages from a suite.  Takes the list of files
39 # either from stdin or as a command line argument.  Special action
40 # "set", will reset the suite (!) and add all packages from scratch.
41
42 #######################################################################################
43
44 import sys
45 import apt_pkg
46 import os
47
48 from daklib.archive import ArchiveTransaction
49 from daklib.config import Config
50 from daklib.dbconn import *
51 from daklib import daklog
52 from daklib import utils
53 from daklib.queue import get_suite_version_by_package, get_suite_version_by_source
54
55 #######################################################################################
56
57 Logger = None
58
59 ################################################################################
60
61 def usage (exit_code=0):
62     print """Usage: dak control-suite [OPTIONS] [FILE]
63 Display or alter the contents of a suite using FILE(s), or stdin.
64
65   -a, --add=SUITE            add to SUITE
66   -h, --help                 show this help and exit
67   -l, --list=SUITE           list the contents of SUITE
68   -r, --remove=SUITE         remove from SUITE
69   -s, --set=SUITE            set SUITE
70   -b, --britney              generate changelog entry for britney runs"""
71
72     sys.exit(exit_code)
73
74 #######################################################################################
75
76 def get_pkg(package, version, architecture, session):
77     if architecture == 'source':
78         q = session.query(DBSource).filter_by(source=package, version=version) \
79             .join(DBSource.poolfile)
80     else:
81         q = session.query(DBBinary).filter_by(package=package, version=version) \
82             .join(DBBinary.architecture).filter(Architecture.arch_string.in_([architecture, 'all'])) \
83             .join(DBBinary.poolfile)
84
85     pkg = q.first()
86     if pkg is None:
87         utils.warn("Could not find {0}_{1}_{2}.".format(package, version, architecture))
88     return pkg
89
90 #######################################################################################
91
92 def britney_changelog(packages, suite, session):
93
94     old = {}
95     current = {}
96     Cnf = utils.get_conf()
97
98     try:
99         q = session.execute("SELECT changelog FROM suite WHERE id = :suiteid", \
100                             {'suiteid': suite.suite_id})
101         brit_file = q.fetchone()[0]
102     except:
103         brit_file = None
104
105     if brit_file:
106         brit_file = os.path.join(Cnf['Dir::Root'], brit_file)
107     else:
108         return
109
110     q = session.execute("""SELECT s.source, s.version, sa.id
111                              FROM source s, src_associations sa
112                             WHERE sa.suite = :suiteid
113                               AND sa.source = s.id""", {'suiteid': suite.suite_id})
114
115     for p in q.fetchall():
116         current[p[0]] = p[1]
117     for p in packages.keys():
118         if p[2] == "source":
119             old[p[0]] = p[1]
120
121     new = {}
122     for p in current.keys():
123         if p in old.keys():
124             if apt_pkg.version_compare(current[p], old[p]) > 0:
125                 new[p] = [current[p], old[p]]
126         else:
127             new[p] = [current[p], 0]
128
129     query =  "SELECT source, changelog FROM changelogs WHERE"
130     for p in new.keys():
131         query += " source = '%s' AND version > '%s' AND version <= '%s'" \
132                  % (p, new[p][1], new[p][0])
133         query += " AND architecture LIKE '%source%' AND distribution in \
134                   ('unstable', 'experimental', 'testing-proposed-updates') OR"
135     query += " False ORDER BY source, version DESC"
136     q = session.execute(query)
137
138     pu = None
139     brit = utils.open_file(brit_file, 'w')
140
141     for u in q:
142         if pu and pu != u[0]:
143             brit.write("\n")
144         brit.write("%s\n" % u[1])
145         pu = u[0]
146     if q.rowcount: brit.write("\n\n\n")
147
148     for p in list(set(old.keys()).difference(current.keys())):
149         brit.write("REMOVED: %s %s\n" % (p, old[p]))
150
151     brit.flush()
152     brit.close()
153
154 #######################################################################################
155
156 def version_checks(package, architecture, target_suite, new_version, session, force = False):
157     if architecture == "source":
158         suite_version_list = get_suite_version_by_source(package, session)
159     else:
160         suite_version_list = get_suite_version_by_package(package, architecture, session)
161
162     must_be_newer_than = [ vc.reference.suite_name for vc in get_version_checks(target_suite, "MustBeNewerThan") ]
163     must_be_older_than = [ vc.reference.suite_name for vc in get_version_checks(target_suite, "MustBeOlderThan") ]
164
165     # Must be newer than an existing version in target_suite
166     if target_suite not in must_be_newer_than:
167         must_be_newer_than.append(target_suite)
168
169     violations = False
170
171     for suite, version in suite_version_list:
172         cmp = apt_pkg.version_compare(new_version, version)
173         if suite in must_be_newer_than and cmp < 1:
174             utils.warn("%s (%s): version check violated: %s targeted at %s is *not* newer than %s in %s" % (package, architecture, new_version, target_suite, version, suite))
175             violations = True
176         if suite in must_be_older_than and cmp > 1:
177             utils.warn("%s (%s): version check violated: %s targeted at %s is *not* older than %s in %s" % (package, architecture, new_version, target_suite, version, suite))
178             violations = True
179
180     if violations:
181         if force:
182             utils.warn("Continuing anyway (forced)...")
183         else:
184             utils.fubar("Aborting. Version checks violated and not forced.")
185
186 #######################################################################################
187
188 def cmp_package_version(a, b):
189     """
190     comparison function for tuples of the form (package-name, version, arch, ...)
191     """
192     res = 0
193     if a[2] == 'source' and b[2] != 'source':
194         res = -1
195     elif a[2] != 'source' and b[2] == 'source':
196         res = 1
197     if res == 0:
198         res = cmp(a[0], b[0])
199     if res == 0:
200         res = apt_pkg.version_compare(a[1], b[1])
201     return res
202
203 #######################################################################################
204
205 def set_suite(file, suite, transaction, britney=False, force=False):
206     session = transaction.session
207     suite_id = suite.suite_id
208     lines = file.readlines()
209
210     # Our session is already in a transaction
211
212     # Build up a dictionary of what is currently in the suite
213     current = {}
214     q = session.execute("""SELECT b.package, b.version, a.arch_string, ba.id
215                              FROM binaries b, bin_associations ba, architecture a
216                             WHERE ba.suite = :suiteid
217                               AND ba.bin = b.id AND b.architecture = a.id""", {'suiteid': suite_id})
218     for i in q:
219         key = i[:3]
220         current[key] = i[3]
221
222     q = session.execute("""SELECT s.source, s.version, 'source', sa.id
223                              FROM source s, src_associations sa
224                             WHERE sa.suite = :suiteid
225                               AND sa.source = s.id""", {'suiteid': suite_id})
226     for i in q:
227         key = i[:3]
228         current[key] = i[3]
229
230     # Build up a dictionary of what should be in the suite
231     desired = set()
232     for line in lines:
233         split_line = line.strip().split()
234         if len(split_line) != 3:
235             utils.warn("'%s' does not break into 'package version architecture'." % (line[:-1]))
236             continue
237         desired.add(tuple(split_line))
238
239     # Check to see which packages need added and add them
240     for key in sorted(desired, cmp=cmp_package_version):
241         if key not in current:
242             (package, version, architecture) = key
243             version_checks(package, architecture, suite.suite_name, version, session, force)
244             pkg = get_pkg(package, version, architecture, session)
245             if pkg is None:
246                 continue
247
248             component = pkg.poolfile.component
249             if architecture == "source":
250                 transaction.copy_source(pkg, suite, component)
251             else:
252                 transaction.copy_binary(pkg, suite, component)
253
254             Logger.log(["added", " ".join(key)])
255
256     # Check to see which packages need removed and remove them
257     for key, pkid in current.iteritems():
258         if key not in desired:
259             (package, version, architecture) = key
260             if architecture == "source":
261                 session.execute("""DELETE FROM src_associations WHERE id = :pkid""", {'pkid': pkid})
262             else:
263                 session.execute("""DELETE FROM bin_associations WHERE id = :pkid""", {'pkid': pkid})
264             Logger.log(["removed", " ".join(key), pkid])
265
266     session.commit()
267
268     if britney:
269         britney_changelog(current, suite, session)
270
271 #######################################################################################
272
273 def process_file(file, suite, action, transaction, britney=False, force=False):
274     session = transaction.session
275
276     if action == "set":
277         set_suite(file, suite, transaction, britney, force)
278         return
279
280     suite_id = suite.suite_id
281
282     request = []
283
284     # Our session is already in a transaction
285     for line in file:
286         split_line = line.strip().split()
287         if len(split_line) != 3:
288             utils.warn("'%s' does not break into 'package version architecture'." % (line[:-1]))
289             continue
290         request.append(split_line)
291
292     request.sort(cmp=cmp_package_version)
293
294     for package, version, architecture in request:
295         pkg = get_pkg(package, version, architecture, session)
296         if pkg is None:
297             continue
298         if architecture == 'source':
299             pkid = pkg.source_id
300         else:
301             pkid = pkg.binary_id
302
303         component = pkg.poolfile.component
304
305         # Do version checks when adding packages
306         if action == "add":
307             version_checks(package, architecture, suite.suite_name, version, session, force)
308
309         if architecture == "source":
310             # Find the existing association ID, if any
311             q = session.execute("""SELECT id FROM src_associations
312                                     WHERE suite = :suiteid and source = :pkid""",
313                                     {'suiteid': suite_id, 'pkid': pkid})
314             ql = q.fetchall()
315             if len(ql) < 1:
316                 association_id = None
317             else:
318                 association_id = ql[0][0]
319
320             # Take action
321             if action == "add":
322                 if association_id:
323                     utils.warn("'%s_%s_%s' already exists in suite %s." % (package, version, architecture, suite))
324                     continue
325                 else:
326                     transaction.copy_source(pkg, suite, component)
327                     Logger.log(["added", package, version, architecture, suite.suite_name, pkid])
328
329             elif action == "remove":
330                 if association_id == None:
331                     utils.warn("'%s_%s_%s' doesn't exist in suite %s." % (package, version, architecture, suite))
332                     continue
333                 else:
334                     session.execute("""DELETE FROM src_associations WHERE id = :pkid""", {'pkid': association_id})
335                     Logger.log(["removed", package, version, architecture, suite.suite_name, pkid])
336         else:
337             # Find the existing associations ID, if any
338             q = session.execute("""SELECT id FROM bin_associations
339                                     WHERE suite = :suiteid and bin = :pkid""",
340                                     {'suiteid': suite_id, 'pkid': pkid})
341             ql = q.fetchall()
342             if len(ql) < 1:
343                 association_id = None
344             else:
345                 association_id = ql[0][0]
346
347             # Take action
348             if action == "add":
349                 if association_id:
350                     utils.warn("'%s_%s_%s' already exists in suite %s." % (package, version, architecture, suite))
351                     continue
352                 else:
353                     transaction.copy_binary(pkg, suite, component)
354                     Logger.log(["added", package, version, architecture, suite.suite_name, pkid])
355             elif action == "remove":
356                 if association_id == None:
357                     utils.warn("'%s_%s_%s' doesn't exist in suite %s." % (package, version, architecture, suite))
358                     continue
359                 else:
360                     session.execute("""DELETE FROM bin_associations WHERE id = :pkid""", {'pkid': association_id})
361                     Logger.log(["removed", package, version, architecture, suite.suite_name, pkid])
362
363     session.commit()
364
365 #######################################################################################
366
367 def get_list(suite, session):
368     suite_id = suite.suite_id
369     # List binaries
370     q = session.execute("""SELECT b.package, b.version, a.arch_string
371                              FROM binaries b, bin_associations ba, architecture a
372                             WHERE ba.suite = :suiteid
373                               AND ba.bin = b.id AND b.architecture = a.id""", {'suiteid': suite_id})
374     for i in q.fetchall():
375         print " ".join(i)
376
377     # List source
378     q = session.execute("""SELECT s.source, s.version
379                              FROM source s, src_associations sa
380                             WHERE sa.suite = :suiteid
381                               AND sa.source = s.id""", {'suiteid': suite_id})
382     for i in q.fetchall():
383         print " ".join(i) + " source"
384
385 #######################################################################################
386
387 def main ():
388     global Logger
389
390     cnf = Config()
391
392     Arguments = [('a',"add","Control-Suite::Options::Add", "HasArg"),
393                  ('b',"britney","Control-Suite::Options::Britney"),
394                  ('f','force','Control-Suite::Options::Force'),
395                  ('h',"help","Control-Suite::Options::Help"),
396                  ('l',"list","Control-Suite::Options::List","HasArg"),
397                  ('r',"remove", "Control-Suite::Options::Remove", "HasArg"),
398                  ('s',"set", "Control-Suite::Options::Set", "HasArg")]
399
400     for i in ["add", "britney", "help", "list", "remove", "set", "version" ]:
401         if not cnf.has_key("Control-Suite::Options::%s" % (i)):
402             cnf["Control-Suite::Options::%s" % (i)] = ""
403
404     try:
405         file_list = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv);
406     except SystemError as e:
407         print "%s\n" % e
408         usage(1)
409     Options = cnf.subtree("Control-Suite::Options")
410
411     if Options["Help"]:
412         usage()
413
414     force = Options.has_key("Force") and Options["Force"]
415
416     action = None
417
418     for i in ("add", "list", "remove", "set"):
419         if cnf["Control-Suite::Options::%s" % (i)] != "":
420             suite_name = cnf["Control-Suite::Options::%s" % (i)]
421
422             if action:
423                 utils.fubar("Can only perform one action at a time.")
424
425             action = i
426
427     # Need an action...
428     if action is None:
429         utils.fubar("No action specified.")
430
431     britney = False
432     if action == "set" and cnf["Control-Suite::Options::Britney"]:
433         britney = True
434
435     if action == "list":
436         session = DBConn().session()
437         suite = session.query(Suite).filter_by(suite_name=suite_name).one()
438         get_list(suite, session)
439     else:
440         Logger = daklog.Logger("control-suite")
441
442         with ArchiveTransaction() as transaction:
443             session = transaction.session
444             suite = session.query(Suite).filter_by(suite_name=suite_name).one()
445
446             if action == "set" and not suite.allowcsset:
447                 if force:
448                     utils.warn("Would not normally allow setting suite {0} (allowcsset is FALSE), but --force used".format(suite_name))
449                 else:
450                     utils.fubar("Will not reset suite {0} due to its database configuration (allowcsset is FALSE)".format(suite_name))
451
452             if file_list:
453                 for f in file_list:
454                     process_file(utils.open_file(f), suite, action, transaction, britney, force)
455             else:
456                 process_file(sys.stdin, suite, action, transaction, britney, force)
457
458         Logger.close()
459
460 #######################################################################################
461
462 if __name__ == '__main__':
463     main()