1#! /usr/bin/env python3
3""" Manipulate suite tags """
4# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org>
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.
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.
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
20#######################################################################################
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."
34# -- http://www.angryflower.com/8to6.gif
36#######################################################################################
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.
42#######################################################################################
44import sys
45import apt_pkg
46import functools
47import os
49from daklib.archive import ArchiveTransaction
50from daklib.config import Config
51from daklib.dbconn import *
52from daklib import daklog
53from daklib import utils
54from daklib.queue import get_suite_version_by_package, get_suite_version_by_source
56#######################################################################################
58Logger = None
60################################################################################
63def usage(exit_code=0):
64 print("""Usage: dak control-suite [OPTIONS] [FILE]
65Display or alter the contents of a suite using FILE(s), or stdin.
67 -a, --add=SUITE add to SUITE
68 -h, --help show this help and exit
69 -l, --list=SUITE list the contents of SUITE
70 -r, --remove=SUITE remove from SUITE
71 -s, --set=SUITE set SUITE
72 -b, --britney generate changelog entry for britney runs""")
74 sys.exit(exit_code)
76#######################################################################################
79def get_pkg(package, version, architecture, session):
80 if architecture == 'source':
81 q = session.query(DBSource).filter_by(source=package, version=version) \
82 .join(DBSource.poolfile)
83 else:
84 q = session.query(DBBinary).filter_by(package=package, version=version) \
85 .join(DBBinary.architecture).filter(Architecture.arch_string.in_([architecture, 'all'])) \
86 .join(DBBinary.poolfile)
88 pkg = q.first()
89 if pkg is None: 89 ↛ 90line 89 didn't jump to line 90, because the condition on line 89 was never true
90 utils.warn("Could not find {0}_{1}_{2}.".format(package, version, architecture))
91 return pkg
93#######################################################################################
96def britney_changelog(packages, suite, session):
98 old = {}
99 current = {}
100 Cnf = utils.get_conf()
102 try:
103 q = session.execute("SELECT changelog FROM suite WHERE id = :suiteid",
104 {'suiteid': suite.suite_id})
105 brit_file = q.fetchone()[0]
106 except:
107 brit_file = None
109 if brit_file: 109 ↛ 112line 109 didn't jump to line 112, because the condition on line 109 was never false
110 brit_file = os.path.join(Cnf['Dir::Root'], brit_file)
111 else:
112 return
114 q = session.execute("""SELECT s.source, s.version, sa.id
115 FROM source s, src_associations sa
116 WHERE sa.suite = :suiteid
117 AND sa.source = s.id""", {'suiteid': suite.suite_id})
119 for p in q.fetchall():
120 current[p[0]] = p[1]
121 for p in packages.keys(): 121 ↛ 122line 121 didn't jump to line 122, because the loop on line 121 never started
122 if p[2] == "source":
123 old[p[0]] = p[1]
125 new = {}
126 for p in current.keys():
127 if p in old: 127 ↛ 128line 127 didn't jump to line 128, because the condition on line 127 was never true
128 if apt_pkg.version_compare(current[p], old[p]) > 0:
129 new[p] = [current[p], old[p]]
130 else:
131 new[p] = [current[p], None]
133 params = {}
134 query = "SELECT source, changelog FROM changelogs WHERE"
135 for n, p in enumerate(new.keys()):
136 query += f" source = :source_{n} AND (:version1_{n} IS NULL OR version > :version1_{n}) AND version <= :version2_{n}"
137 query += " AND architecture LIKE '%source%' AND distribution in \
138 ('unstable', 'experimental', 'testing-proposed-updates') OR"
139 params[f'source_{n}'] = p
140 params[f'version1_{n}'] = new[p][1]
141 params[f'version2_{n}'] = new[p][0]
142 query += " False ORDER BY source, version DESC"
143 q = session.execute(query, params)
145 pu = None
146 with open(brit_file, 'w') as brit:
148 for u in q:
149 if pu and pu != u[0]: 149 ↛ 150line 149 didn't jump to line 150, because the condition on line 149 was never true
150 brit.write("\n")
151 brit.write("%s\n" % u[1])
152 pu = u[0]
153 if q.rowcount: 153 ↛ 156line 153 didn't jump to line 156, because the condition on line 153 was never false
154 brit.write("\n\n\n")
156 for p in list(set(old.keys()).difference(current.keys())): 156 ↛ 157line 156 didn't jump to line 157, because the loop on line 156 never started
157 brit.write("REMOVED: %s %s\n" % (p, old[p]))
159 brit.flush()
162#######################################################################################
165class VersionCheck:
166 def __init__(self, target_suite: str, force: bool, session):
167 self.target_suite = target_suite
168 self.force = force
169 self.session = session
171 self.must_be_newer_than = [vc.reference.suite_name for vc in get_version_checks(target_suite, "MustBeNewerThan", session)]
172 self.must_be_older_than = [vc.reference.suite_name for vc in get_version_checks(target_suite, "MustBeOlderThan", session)]
174 # Must be newer than an existing version in target_suite
175 if target_suite not in self.must_be_newer_than: 175 ↛ exitline 175 didn't return from function '__init__', because the condition on line 175 was never false
176 self.must_be_newer_than.append(target_suite)
178 def __call__(self, package: str, architecture: str, new_version: str):
179 if architecture == "source":
180 suite_version_list = get_suite_version_by_source(package, self.session)
181 else:
182 suite_version_list = get_suite_version_by_package(package, architecture, self.session)
184 violations = False
186 for suite, version in suite_version_list:
187 cmp = apt_pkg.version_compare(new_version, version)
188 # for control-suite we allow equal version (for uploads, we don't)
189 if suite in self.must_be_newer_than and cmp < 0:
190 utils.warn("%s (%s): version check violated: %s targeted at %s is *not* newer than %s in %s" % (package, architecture, new_version, self.target_suite, version, suite))
191 violations = True
192 if suite in self.must_be_older_than and cmp > 0: 192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true
193 utils.warn("%s (%s): version check violated: %s targeted at %s is *not* older than %s in %s" % (package, architecture, new_version, self.target_suite, version, suite))
194 violations = True
196 if violations:
197 if self.force:
198 utils.warn("Continuing anyway (forced)...")
199 else:
200 utils.fubar("Aborting. Version checks violated and not forced.")
202#######################################################################################
205def cmp_package_version(a, b):
206 """
207 comparison function for tuples of the form (package-name, version, arch, ...)
208 """
209 res = 0
210 if a[2] == 'source' and b[2] != 'source':
211 res = -1
212 elif a[2] != 'source' and b[2] == 'source':
213 res = 1
214 if res == 0:
215 res = (a[0] > b[0]) - (a[0] < b[0])
216 if res == 0:
217 res = apt_pkg.version_compare(a[1], b[1])
218 return res
220#######################################################################################
223def copy_to_suites(transaction, pkg, suites):
224 component = pkg.poolfile.component
225 if pkg.arch_string == "source":
226 for s in suites:
227 transaction.copy_source(pkg, s, component)
228 else:
229 for s in suites:
230 transaction.copy_binary(pkg, s, component)
233def check_propups(pkg, psuites_current, propups):
234 key = (pkg.name, pkg.arch_string)
235 for suite_id in psuites_current:
236 if key in psuites_current[suite_id]:
237 old_version = psuites_current[suite_id][key]
238 if apt_pkg.version_compare(pkg.version, old_version) > 0:
239 propups[suite_id].add(pkg)
240 if pkg.arch_string != "source":
241 source = pkg.source
242 propups[suite_id].add(source)
245def get_propup_suites(suite, session):
246 propup_suites = []
247 for rule in Config().value_list("SuiteMappings"):
248 fields = rule.split()
249 if fields[0] == "propup-version" and fields[1] == suite.suite_name:
250 propup_suites.append(session.query(Suite).filter_by(suite_name=fields[2]).one())
251 return propup_suites
254def set_suite(file, suite, transaction, britney=False, force=False):
255 session = transaction.session
256 suite_id = suite.suite_id
257 lines = file.readlines()
258 suites = [suite] + [q.suite for q in suite.copy_queues]
259 propup_suites = get_propup_suites(suite, session)
261 # Our session is already in a transaction
263 def get_binary_q(suite_id):
264 return session.execute("""SELECT b.package, b.version, a.arch_string, ba.id
265 FROM binaries b, bin_associations ba, architecture a
266 WHERE ba.suite = :suiteid
267 AND ba.bin = b.id AND b.architecture = a.id
268 ORDER BY b.version ASC""", {'suiteid': suite_id})
270 def get_source_q(suite_id):
271 return session.execute("""SELECT s.source, s.version, 'source', sa.id
272 FROM source s, src_associations sa
273 WHERE sa.suite = :suiteid
274 AND sa.source = s.id
275 ORDER BY s.version ASC""", {'suiteid': suite_id})
277 # Build up a dictionary of what is currently in the suite
278 current = {}
280 q = get_binary_q(suite_id)
281 for i in q:
282 key = i[:3]
283 current[key] = i[3]
285 q = get_source_q(suite_id)
286 for i in q:
287 key = i[:3]
288 current[key] = i[3]
290 # Build a dictionary of what's currently in the propup suites
291 psuites_current = {}
292 propups_needed = {}
293 for p_s in propup_suites:
294 propups_needed[p_s.suite_id] = set()
295 psuites_current[p_s.suite_id] = {}
296 q = get_binary_q(p_s.suite_id)
297 for i in q:
298 key = (i[0], i[2])
299 # the query is sorted, so we only keep the newest version
300 psuites_current[p_s.suite_id][key] = i[1]
302 q = get_source_q(p_s.suite_id)
303 for i in q:
304 key = (i[0], i[2])
305 # the query is sorted, so we only keep the newest version
306 psuites_current[p_s.suite_id][key] = i[1]
308 # Build up a dictionary of what should be in the suite
309 desired = set()
310 for line in lines:
311 split_line = line.strip().split()
312 if len(split_line) != 3: 312 ↛ 313line 312 didn't jump to line 313, because the condition on line 312 was never true
313 utils.warn("'%s' does not break into 'package version architecture'." % (line[:-1]))
314 continue
315 desired.add(tuple(split_line))
317 version_check = VersionCheck(suite.suite_name, force, session)
319 # Check to see which packages need added and add them
320 for key in sorted(desired, key=functools.cmp_to_key(cmp_package_version)):
321 if key not in current:
322 (package, version, architecture) = key
323 version_check(package, architecture, version)
324 pkg = get_pkg(package, version, architecture, session)
325 if pkg is None: 325 ↛ 326line 325 didn't jump to line 326, because the condition on line 325 was never true
326 continue
328 copy_to_suites(transaction, pkg, suites)
329 Logger.log(["added", suite.suite_name, " ".join(key)])
331 check_propups(pkg, psuites_current, propups_needed)
333 # Check to see which packages need removed and remove them
334 for key, pkid in current.items():
335 if key not in desired:
336 (package, version, architecture) = key
337 if architecture == "source":
338 session.execute("""DELETE FROM src_associations WHERE id = :pkid""", {'pkid': pkid})
339 else:
340 session.execute("""DELETE FROM bin_associations WHERE id = :pkid""", {'pkid': pkid})
341 Logger.log(["removed", suite.suite_name, " ".join(key), pkid])
343 for p_s in propup_suites:
344 for pkg in propups_needed[p_s.suite_id]:
345 copy_to_suites(transaction, pkg, [p_s])
346 info = (pkg.name, pkg.version, pkg.arch_string)
347 Logger.log(["propup", p_s.suite_name, " ".join(info)])
349 session.commit()
351 if britney:
352 britney_changelog(current, suite, session)
354#######################################################################################
357def process_file(file, suite, action, transaction, britney=False, force=False):
358 session = transaction.session
360 if action == "set":
361 set_suite(file, suite, transaction, britney, force)
362 return
364 suite_id = suite.suite_id
365 suites = [suite] + [q.suite for q in suite.copy_queues]
366 extra_archives = [suite.archive]
368 request = []
370 # Our session is already in a transaction
371 for line in file:
372 split_line = line.strip().split()
373 if len(split_line) != 3: 373 ↛ 374line 373 didn't jump to line 374, because the condition on line 373 was never true
374 utils.warn("'%s' does not break into 'package version architecture'." % (line[:-1]))
375 continue
376 request.append(split_line)
378 request.sort(key=functools.cmp_to_key(cmp_package_version))
380 version_check = VersionCheck(suite.suite_name, force, session)
382 for package, version, architecture in request:
383 pkg = get_pkg(package, version, architecture, session)
384 if pkg is None: 384 ↛ 385line 384 didn't jump to line 385, because the condition on line 384 was never true
385 continue
386 if architecture == 'source':
387 pkid = pkg.source_id
388 else:
389 pkid = pkg.binary_id
391 component = pkg.poolfile.component
393 # Do version checks when adding packages
394 if action == "add":
395 version_check(package, architecture, version)
397 if architecture == "source":
398 # Find the existing association ID, if any
399 q = session.execute("""SELECT id FROM src_associations
400 WHERE suite = :suiteid and source = :pkid""",
401 {'suiteid': suite_id, 'pkid': pkid})
402 ql = q.fetchall()
403 if len(ql) < 1:
404 association_id = None
405 else:
406 association_id = ql[0][0]
408 # Take action
409 if action == "add":
410 if association_id: 410 ↛ 411line 410 didn't jump to line 411, because the condition on line 410 was never true
411 utils.warn("'%s_%s_%s' already exists in suite %s." % (package, version, architecture, suite.suite_name))
412 continue
413 else:
414 for s in suites:
415 transaction.copy_source(pkg, s, component)
416 Logger.log(["added", package, version, architecture, suite.suite_name, pkid])
418 elif action == "remove": 418 ↛ 382line 418 didn't jump to line 382, because the condition on line 418 was never false
419 if association_id is None: 419 ↛ 420line 419 didn't jump to line 420, because the condition on line 419 was never true
420 utils.warn("'%s_%s_%s' doesn't exist in suite %s." % (package, version, architecture, suite))
421 continue
422 else:
423 session.execute("""DELETE FROM src_associations WHERE id = :pkid""", {'pkid': association_id})
424 Logger.log(["removed", package, version, architecture, suite.suite_name, pkid])
425 else:
426 # Find the existing associations ID, if any
427 q = session.execute("""SELECT id FROM bin_associations
428 WHERE suite = :suiteid and bin = :pkid""",
429 {'suiteid': suite_id, 'pkid': pkid})
430 ql = q.fetchall()
431 if len(ql) < 1:
432 association_id = None
433 else:
434 association_id = ql[0][0]
436 # Take action
437 if action == "add":
438 if association_id: 438 ↛ 439line 438 didn't jump to line 439, because the condition on line 438 was never true
439 utils.warn("'%s_%s_%s' already exists in suite %s." % (package, version, architecture, suite))
440 continue
441 else:
442 for s in suites:
443 transaction.copy_binary(pkg, s, component, extra_archives=extra_archives)
444 Logger.log(["added", package, version, architecture, suite.suite_name, pkid])
445 elif action == "remove": 445 ↛ 382line 445 didn't jump to line 382, because the condition on line 445 was never false
446 if association_id is None: 446 ↛ 447line 446 didn't jump to line 447, because the condition on line 446 was never true
447 utils.warn("'%s_%s_%s' doesn't exist in suite %s." % (package, version, architecture, suite))
448 continue
449 else:
450 session.execute("""DELETE FROM bin_associations WHERE id = :pkid""", {'pkid': association_id})
451 Logger.log(["removed", package, version, architecture, suite.suite_name, pkid])
453 session.commit()
455#######################################################################################
458def get_list(suite, session):
459 suite_id = suite.suite_id
460 # List binaries
461 q = session.execute("""SELECT b.package, b.version, a.arch_string
462 FROM binaries b, bin_associations ba, architecture a
463 WHERE ba.suite = :suiteid
464 AND ba.bin = b.id AND b.architecture = a.id""", {'suiteid': suite_id})
465 for i in q.fetchall():
466 print(" ".join(i))
468 # List source
469 q = session.execute("""SELECT s.source, s.version
470 FROM source s, src_associations sa
471 WHERE sa.suite = :suiteid
472 AND sa.source = s.id""", {'suiteid': suite_id})
473 for i in q.fetchall():
474 print(" ".join(i) + " source")
476#######################################################################################
479def main():
480 global Logger
482 cnf = Config()
484 Arguments = [('a', "add", "Control-Suite::Options::Add", "HasArg"),
485 ('b', "britney", "Control-Suite::Options::Britney"),
486 ('f', 'force', 'Control-Suite::Options::Force'),
487 ('h', "help", "Control-Suite::Options::Help"),
488 ('l', "list", "Control-Suite::Options::List", "HasArg"),
489 ('r', "remove", "Control-Suite::Options::Remove", "HasArg"),
490 ('s', "set", "Control-Suite::Options::Set", "HasArg")]
492 for i in ["add", "britney", "help", "list", "remove", "set", "version"]:
493 key = "Control-Suite::Options::%s" % i
494 if key not in cnf: 494 ↛ 492line 494 didn't jump to line 492, because the condition on line 494 was never false
495 cnf[key] = ""
497 try:
498 file_list = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv)
499 except SystemError as e:
500 print("%s\n" % e)
501 usage(1)
502 Options = cnf.subtree("Control-Suite::Options")
504 if Options["Help"]:
505 usage()
507 force = "Force" in Options and Options["Force"]
509 action = None
511 for i in ("add", "list", "remove", "set"):
512 if cnf["Control-Suite::Options::%s" % (i)] != "":
513 suite_name = cnf["Control-Suite::Options::%s" % (i)]
515 if action: 515 ↛ 516line 515 didn't jump to line 516, because the condition on line 515 was never true
516 utils.fubar("Can only perform one action at a time.")
518 action = i
520 # Need an action...
521 if action is None: 521 ↛ 522line 521 didn't jump to line 522, because the condition on line 521 was never true
522 utils.fubar("No action specified.")
524 britney = False
525 if action == "set" and cnf["Control-Suite::Options::Britney"]:
526 britney = True
528 if action == "list":
529 session = DBConn().session()
530 suite = get_suite(suite_name, session)
531 get_list(suite, session)
532 else:
533 Logger = daklog.Logger("control-suite")
535 with ArchiveTransaction() as transaction:
536 session = transaction.session
537 suite = get_suite(suite_name, session)
539 if action == "set" and not suite.allowcsset:
540 if force: 540 ↛ 543line 540 didn't jump to line 543, because the condition on line 540 was never false
541 utils.warn("Would not normally allow setting suite {0} (allowcsset is FALSE), but --force used".format(suite_name))
542 else:
543 utils.fubar("Will not reset suite {0} due to its database configuration (allowcsset is FALSE)".format(suite_name))
545 if file_list: 545 ↛ 546line 545 didn't jump to line 546, because the condition on line 545 was never true
546 for f in file_list:
547 process_file(open(f), suite, action, transaction, britney, force)
548 else:
549 process_file(sys.stdin, suite, action, transaction, britney, force)
551 Logger.close()
553#######################################################################################
556if __name__ == '__main__':
557 main()