]> git.decadent.org.uk Git - dak.git/blob - dak/control_suite.py
dak/control_suite.py: update for multi-archive changes
[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     else:
80         q = session.query(DBBinary).filter_by(package=package, version=version) \
81             .join(DBBinary.architecture).filter(Architecture.arch_string.in_([architecture, 'all']))
82
83     pkg = q.first()
84     if pkg is None:
85         utils.warn("Could not find {0}_{1}_{2}.".format(package, version, architecture))
86     return pkg
87
88 #######################################################################################
89
90 def britney_changelog(packages, suite, session):
91
92     old = {}
93     current = {}
94     Cnf = utils.get_conf()
95
96     try:
97         q = session.execute("SELECT changelog FROM suite WHERE id = :suiteid", \
98                             {'suiteid': suite.suite_id})
99         brit_file = q.fetchone()[0]
100     except:
101         brit_file = None
102
103     if brit_file:
104         brit_file = os.path.join(Cnf['Dir::Root'], brit_file)
105     else:
106         return
107
108     q = session.execute("""SELECT s.source, s.version, sa.id
109                              FROM source s, src_associations sa
110                             WHERE sa.suite = :suiteid
111                               AND sa.source = s.id""", {'suiteid': suite.suite_id})
112
113     for p in q.fetchall():
114         current[p[0]] = p[1]
115     for p in packages.keys():
116         if p[2] == "source":
117             old[p[0]] = p[1]
118
119     new = {}
120     for p in current.keys():
121         if p in old.keys():
122             if apt_pkg.version_compare(current[p], old[p]) > 0:
123                 new[p] = [current[p], old[p]]
124         else:
125             new[p] = [current[p], 0]
126
127     query =  "SELECT source, changelog FROM changelogs WHERE"
128     for p in new.keys():
129         query += " source = '%s' AND version > '%s' AND version <= '%s'" \
130                  % (p, new[p][1], new[p][0])
131         query += " AND architecture LIKE '%source%' AND distribution in \
132                   ('unstable', 'experimental', 'testing-proposed-updates') OR"
133     query += " False ORDER BY source, version DESC"
134     q = session.execute(query)
135
136     pu = None
137     brit = utils.open_file(brit_file, 'w')
138
139     for u in q:
140         if pu and pu != u[0]:
141             brit.write("\n")
142         brit.write("%s\n" % u[1])
143         pu = u[0]
144     if q.rowcount: brit.write("\n\n\n")
145
146     for p in list(set(old.keys()).difference(current.keys())):
147         brit.write("REMOVED: %s %s\n" % (p, old[p]))
148
149     brit.flush()
150     brit.close()
151
152 #######################################################################################
153
154 def version_checks(package, architecture, target_suite, new_version, session, force = False):
155     if architecture == "source":
156         suite_version_list = get_suite_version_by_source(package, session)
157     else:
158         suite_version_list = get_suite_version_by_package(package, architecture, session)
159
160     must_be_newer_than = [ vc.reference.suite_name for vc in get_version_checks(target_suite, "MustBeNewerThan") ]
161     must_be_older_than = [ vc.reference.suite_name for vc in get_version_checks(target_suite, "MustBeOlderThan") ]
162
163     # Must be newer than an existing version in target_suite
164     if target_suite not in must_be_newer_than:
165         must_be_newer_than.append(target_suite)
166
167     violations = False
168
169     for suite, version in suite_version_list:
170         cmp = apt_pkg.version_compare(new_version, version)
171         if suite in must_be_newer_than and cmp < 1:
172             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))
173             violations = True
174         if suite in must_be_older_than and cmp > 1:
175             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))
176             violations = True
177
178     if violations:
179         if force:
180             utils.warn("Continuing anyway (forced)...")
181         else:
182             utils.fubar("Aborting. Version checks violated and not forced.")
183
184 #######################################################################################
185
186 def cmp_package_version(a, b):
187     """
188     comparison function for tuples of the form (package-name, version, arch, ...)
189     """
190     res = 0
191     if a[2] == 'source' and b[2] != 'source':
192         res = -1
193     elif a[2] != 'source' and b[2] == 'source':
194         res = 1
195     if res == 0:
196         res = cmp(a[0], b[0])
197     if res == 0:
198         res = apt_pkg.version_compare(a[1], b[1])
199     return res
200
201 #######################################################################################
202
203 def set_suite(file, suite, transaction, britney=False, force=False):
204     session = transaction.session
205     suite_id = suite.suite_id
206     lines = file.readlines()
207
208     # Our session is already in a transaction
209
210     # Build up a dictionary of what is currently in the suite
211     current = {}
212     q = session.execute("""SELECT b.package, b.version, a.arch_string, ba.id
213                              FROM binaries b, bin_associations ba, architecture a
214                             WHERE ba.suite = :suiteid
215                               AND ba.bin = b.id AND b.architecture = a.id""", {'suiteid': suite_id})
216     for i in q:
217         key = i[:3]
218         current[key] = i[3]
219
220     q = session.execute("""SELECT s.source, s.version, 'source', sa.id
221                              FROM source s, src_associations sa
222                             WHERE sa.suite = :suiteid
223                               AND sa.source = s.id""", {'suiteid': suite_id})
224     for i in q:
225         key = i[:3]
226         current[key] = i[3]
227
228     # Build up a dictionary of what should be in the suite
229     desired = set()
230     for line in lines:
231         split_line = line.strip().split()
232         if len(split_line) != 3:
233             utils.warn("'%s' does not break into 'package version architecture'." % (line[:-1]))
234             continue
235         desired.add(tuple(split_line))
236
237     # Check to see which packages need added and add them
238     for key in sorted(desired, cmp=cmp_package_version):
239         if key not in current:
240             (package, version, architecture) = key
241             version_checks(package, architecture, suite.suite_name, version, session, force)
242             pkg = get_pkg(package, version, architecture, session)
243             if pkg is None:
244                 continue
245
246             component = pkg.poolfile.component
247             if architecture == "source":
248                 transaction.copy_source(pkg, suite, component)
249             else:
250                 transaction.copy_binary(pkg, suite, component)
251
252             Logger.log(["added", " ".join(key)])
253
254     # Check to see which packages need removed and remove them
255     for key, pkid in current.iteritems():
256         if key not in desired:
257             (package, version, architecture) = key
258             if architecture == "source":
259                 session.execute("""DELETE FROM src_associations WHERE id = :pkid""", {'pkid': pkid})
260             else:
261                 session.execute("""DELETE FROM bin_associations WHERE id = :pkid""", {'pkid': pkid})
262             Logger.log(["removed", " ".join(key), pkid])
263
264     session.commit()
265
266     if britney:
267         britney_changelog(current, suite, session)
268
269 #######################################################################################
270
271 def process_file(file, suite, action, transaction, britney=False, force=False):
272     session = transaction.session
273
274     if action == "set":
275         set_suite(file, suite, transaction, britney, force)
276         return
277
278     suite_id = suite.suite_id
279
280     request = []
281
282     # Our session is already in a transaction
283     for line in file:
284         split_line = line.strip().split()
285         if len(split_line) != 3:
286             utils.warn("'%s' does not break into 'package version architecture'." % (line[:-1]))
287             continue
288         request.append(split_line)
289
290     request.sort(cmp=cmp_package_version)
291
292     for package, version, architecture in request:
293         pkg = get_pkg(package, version, architecture, session)
294         if pkg is None:
295             continue
296         if architecture == 'source':
297             pkid = pkg.source_id
298         else:
299             pkid = pkg.binary_id
300
301         component = pkg.poolfile.component
302
303         # Do version checks when adding packages
304         if action == "add":
305             version_checks(package, architecture, suite.suite_name, version, session, force)
306
307         if architecture == "source":
308             # Find the existing association ID, if any
309             q = session.execute("""SELECT id FROM src_associations
310                                     WHERE suite = :suiteid and source = :pkid""",
311                                     {'suiteid': suite_id, 'pkid': pkid})
312             ql = q.fetchall()
313             if len(ql) < 1:
314                 association_id = None
315             else:
316                 association_id = ql[0][0]
317
318             # Take action
319             if action == "add":
320                 if association_id:
321                     utils.warn("'%s_%s_%s' already exists in suite %s." % (package, version, architecture, suite))
322                     continue
323                 else:
324                     transaction.copy_source(pkg, suite, component)
325                     Logger.log(["added", package, version, architecture, suite.suite_name, pkid])
326
327             elif action == "remove":
328                 if association_id == None:
329                     utils.warn("'%s_%s_%s' doesn't exist in suite %s." % (package, version, architecture, suite))
330                     continue
331                 else:
332                     session.execute("""DELETE FROM src_associations WHERE id = :pkid""", {'pkid': association_id})
333                     Logger.log(["removed", package, version, architecture, suite.suite_name, pkid])
334         else:
335             # Find the existing associations ID, if any
336             q = session.execute("""SELECT id FROM bin_associations
337                                     WHERE suite = :suiteid and bin = :pkid""",
338                                     {'suiteid': suite_id, 'pkid': pkid})
339             ql = q.fetchall()
340             if len(ql) < 1:
341                 association_id = None
342             else:
343                 association_id = ql[0][0]
344
345             # Take action
346             if action == "add":
347                 if association_id:
348                     utils.warn("'%s_%s_%s' already exists in suite %s." % (package, version, architecture, suite))
349                     continue
350                 else:
351                     transaction.copy_binary(pkg, suite, component)
352                     Logger.log(["added", package, version, architecture, suite.suite_name, pkid])
353             elif action == "remove":
354                 if association_id == None:
355                     utils.warn("'%s_%s_%s' doesn't exist in suite %s." % (package, version, architecture, suite))
356                     continue
357                 else:
358                     session.execute("""DELETE FROM bin_associations WHERE id = :pkid""", {'pkid': association_id})
359                     Logger.log(["removed", package, version, architecture, suite.suite_name, pkid])
360
361     session.commit()
362
363 #######################################################################################
364
365 def get_list(suite, session):
366     suite_id = suite.suite_id
367     # List binaries
368     q = session.execute("""SELECT b.package, b.version, a.arch_string
369                              FROM binaries b, bin_associations ba, architecture a
370                             WHERE ba.suite = :suiteid
371                               AND ba.bin = b.id AND b.architecture = a.id""", {'suiteid': suite_id})
372     for i in q.fetchall():
373         print " ".join(i)
374
375     # List source
376     q = session.execute("""SELECT s.source, s.version
377                              FROM source s, src_associations sa
378                             WHERE sa.suite = :suiteid
379                               AND sa.source = s.id""", {'suiteid': suite_id})
380     for i in q.fetchall():
381         print " ".join(i) + " source"
382
383 #######################################################################################
384
385 def main ():
386     global Logger
387
388     cnf = Config()
389
390     Arguments = [('a',"add","Control-Suite::Options::Add", "HasArg"),
391                  ('b',"britney","Control-Suite::Options::Britney"),
392                  ('f','force','Control-Suite::Options::Force'),
393                  ('h',"help","Control-Suite::Options::Help"),
394                  ('l',"list","Control-Suite::Options::List","HasArg"),
395                  ('r',"remove", "Control-Suite::Options::Remove", "HasArg"),
396                  ('s',"set", "Control-Suite::Options::Set", "HasArg")]
397
398     for i in ["add", "britney", "help", "list", "remove", "set", "version" ]:
399         if not cnf.has_key("Control-Suite::Options::%s" % (i)):
400             cnf["Control-Suite::Options::%s" % (i)] = ""
401
402     try:
403         file_list = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv);
404     except SystemError as e:
405         print "%s\n" % e
406         usage(1)
407     Options = cnf.subtree("Control-Suite::Options")
408
409     if Options["Help"]:
410         usage()
411
412     force = Options.has_key("Force") and Options["Force"]
413
414     action = None
415
416     for i in ("add", "list", "remove", "set"):
417         if cnf["Control-Suite::Options::%s" % (i)] != "":
418             suite_name = cnf["Control-Suite::Options::%s" % (i)]
419
420             if action:
421                 utils.fubar("Can only perform one action at a time.")
422
423             action = i
424
425     # Need an action...
426     if action is None:
427         utils.fubar("No action specified.")
428
429     britney = False
430     if action == "set" and cnf["Control-Suite::Options::Britney"]:
431         britney = True
432
433     if action == "list":
434         session = DBConn().session()
435         suite = session.query(Suite).filter_by(suite_name=suite_name).one()
436         get_list(suite, session)
437     else:
438         Logger = daklog.Logger("control-suite")
439
440         with ArchiveTransaction() as transaction:
441             session = transaction.session
442             suite = session.query(Suite).filter_by(suite_name=suite_name).one()
443
444             if action == "set" and not suite.allowcsset:
445                 if force:
446                     utils.warn("Would not normally allow setting suite {0} (allowsetcs is FALSE), but --force used".format(suite_name))
447                 else:
448                     utils.fubar("Will not reset suite {0} due to its database configuration (allowsetcs is FALSE)".format(suite_name))
449
450             if file_list:
451                 for f in file_list:
452                     process_file(utils.open_file(f), suite, action, transaction, britney, force)
453             else:
454                 process_file(sys.stdin, suite, action, transaction, britney, force)
455
456         Logger.close()
457
458 #######################################################################################
459
460 if __name__ == '__main__':
461     main()