#! /usr/bin/env python3
""" Manipulate suite tags """
# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org>
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#######################################################################################
# 8to6Guy: "Wow, Bob, You look rough!"
# BTAF: "Mbblpmn..."
# BTAF <.oO>: "You moron! This is what you get for staying up all night drinking vodka and salad dressing!"
# BTAF <.oO>: "This coffee I.V. drip is barely even keeping me awake! I need something with more kick! But what?"
# BTAF: "OMIGOD! I OVERDOSED ON HEROIN"
# CoWorker#n: "Give him air!!"
# CoWorker#n+1: "We need a syringe full of adrenaline!"
# CoWorker#n+2: "Stab him in the heart!"
# BTAF: "*YES!*"
# CoWorker#n+3: "Bob's been overdosing quite a bit lately..."
# CoWorker#n+4: "Third time this week."
# -- http://www.angryflower.com/8to6.gif
#######################################################################################
# Adds or removes packages from a suite. Takes the list of files
# either from stdin or as a command line argument. Special action
# "set", will reset the suite (!) and add all packages from scratch.
#######################################################################################
import sys
import apt_pkg
import functools
import os
from daklib.archive import ArchiveTransaction
from daklib.config import Config
from daklib.dbconn import *
from daklib import daklog
from daklib import utils
from daklib.queue import get_suite_version_by_package, get_suite_version_by_source
#######################################################################################
Logger = None
################################################################################
[docs]def usage(exit_code=0):
print("""Usage: dak control-suite [OPTIONS] [FILE]
Display or alter the contents of a suite using FILE(s), or stdin.
-a, --add=SUITE add to SUITE
-h, --help show this help and exit
-l, --list=SUITE list the contents of SUITE
-r, --remove=SUITE remove from SUITE
-s, --set=SUITE set SUITE
-b, --britney generate changelog entry for britney runs""")
sys.exit(exit_code)
#######################################################################################
[docs]def get_pkg(package, version, architecture, session):
if architecture == 'source':
q = session.query(DBSource).filter_by(source=package, version=version) \
.join(DBSource.poolfile)
else:
q = session.query(DBBinary).filter_by(package=package, version=version) \
.join(DBBinary.architecture).filter(Architecture.arch_string.in_([architecture, 'all'])) \
.join(DBBinary.poolfile)
pkg = q.first()
if pkg is None:
utils.warn("Could not find {0}_{1}_{2}.".format(package, version, architecture))
return pkg
#######################################################################################
[docs]def britney_changelog(packages, suite, session):
old = {}
current = {}
Cnf = utils.get_conf()
try:
q = session.execute("SELECT changelog FROM suite WHERE id = :suiteid",
{'suiteid': suite.suite_id})
brit_file = q.fetchone()[0]
except:
brit_file = None
if brit_file:
brit_file = os.path.join(Cnf['Dir::Root'], brit_file)
else:
return
q = session.execute("""SELECT s.source, s.version, sa.id
FROM source s, src_associations sa
WHERE sa.suite = :suiteid
AND sa.source = s.id""", {'suiteid': suite.suite_id})
for p in q.fetchall():
current[p[0]] = p[1]
for p in packages.keys():
if p[2] == "source":
old[p[0]] = p[1]
new = {}
for p in current.keys():
if p in old:
if apt_pkg.version_compare(current[p], old[p]) > 0:
new[p] = [current[p], old[p]]
else:
new[p] = [current[p], None]
params = {}
query = "SELECT source, changelog FROM changelogs WHERE"
for n, p in enumerate(new.keys()):
query += f" source = :source_{n} AND (:version1_{n} IS NULL OR version > :version1_{n}) AND version <= :version2_{n}"
query += " AND architecture LIKE '%source%' AND distribution in \
('unstable', 'experimental', 'testing-proposed-updates') OR"
params[f'source_{n}'] = p
params[f'version1_{n}'] = new[p][1]
params[f'version2_{n}'] = new[p][0]
query += " False ORDER BY source, version DESC"
q = session.execute(query, params)
pu = None
with open(brit_file, 'w') as brit:
for u in q:
if pu and pu != u[0]:
brit.write("\n")
brit.write("%s\n" % u[1])
pu = u[0]
if q.rowcount:
brit.write("\n\n\n")
for p in list(set(old.keys()).difference(current.keys())):
brit.write("REMOVED: %s %s\n" % (p, old[p]))
brit.flush()
#######################################################################################
[docs]class VersionCheck:
def __init__(self, target_suite: str, force: bool, session):
self.target_suite = target_suite
self.force = force
self.session = session
self.must_be_newer_than = [vc.reference.suite_name for vc in get_version_checks(target_suite, "MustBeNewerThan", session)]
self.must_be_older_than = [vc.reference.suite_name for vc in get_version_checks(target_suite, "MustBeOlderThan", session)]
# Must be newer than an existing version in target_suite
if target_suite not in self.must_be_newer_than:
self.must_be_newer_than.append(target_suite)
def __call__(self, package: str, architecture: str, new_version: str):
if architecture == "source":
suite_version_list = get_suite_version_by_source(package, self.session)
else:
suite_version_list = get_suite_version_by_package(package, architecture, self.session)
violations = False
for suite, version in suite_version_list:
cmp = apt_pkg.version_compare(new_version, version)
# for control-suite we allow equal version (for uploads, we don't)
if suite in self.must_be_newer_than and cmp < 0:
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))
violations = True
if suite in self.must_be_older_than and cmp > 0:
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))
violations = True
if violations:
if self.force:
utils.warn("Continuing anyway (forced)...")
else:
utils.fubar("Aborting. Version checks violated and not forced.")
#######################################################################################
[docs]def cmp_package_version(a, b):
"""
comparison function for tuples of the form (package-name, version, arch, ...)
"""
res = 0
if a[2] == 'source' and b[2] != 'source':
res = -1
elif a[2] != 'source' and b[2] == 'source':
res = 1
if res == 0:
res = (a[0] > b[0]) - (a[0] < b[0])
if res == 0:
res = apt_pkg.version_compare(a[1], b[1])
return res
#######################################################################################
[docs]def copy_to_suites(transaction, pkg, suites):
component = pkg.poolfile.component
if pkg.arch_string == "source":
for s in suites:
transaction.copy_source(pkg, s, component)
else:
for s in suites:
transaction.copy_binary(pkg, s, component)
[docs]def check_propups(pkg, psuites_current, propups):
key = (pkg.name, pkg.arch_string)
for suite_id in psuites_current:
if key in psuites_current[suite_id]:
old_version = psuites_current[suite_id][key]
if apt_pkg.version_compare(pkg.version, old_version) > 0:
propups[suite_id].add(pkg)
if pkg.arch_string != "source":
source = pkg.source
propups[suite_id].add(source)
[docs]def get_propup_suites(suite, session):
propup_suites = []
for rule in Config().value_list("SuiteMappings"):
fields = rule.split()
if fields[0] == "propup-version" and fields[1] == suite.suite_name:
propup_suites.append(session.query(Suite).filter_by(suite_name=fields[2]).one())
return propup_suites
[docs]def set_suite(file, suite, transaction, britney=False, force=False):
session = transaction.session
suite_id = suite.suite_id
lines = file.readlines()
suites = [suite] + [q.suite for q in suite.copy_queues]
propup_suites = get_propup_suites(suite, session)
# Our session is already in a transaction
def get_binary_q(suite_id):
return session.execute("""SELECT b.package, b.version, a.arch_string, ba.id
FROM binaries b, bin_associations ba, architecture a
WHERE ba.suite = :suiteid
AND ba.bin = b.id AND b.architecture = a.id
ORDER BY b.version ASC""", {'suiteid': suite_id})
def get_source_q(suite_id):
return session.execute("""SELECT s.source, s.version, 'source', sa.id
FROM source s, src_associations sa
WHERE sa.suite = :suiteid
AND sa.source = s.id
ORDER BY s.version ASC""", {'suiteid': suite_id})
# Build up a dictionary of what is currently in the suite
current = {}
q = get_binary_q(suite_id)
for i in q:
key = i[:3]
current[key] = i[3]
q = get_source_q(suite_id)
for i in q:
key = i[:3]
current[key] = i[3]
# Build a dictionary of what's currently in the propup suites
psuites_current = {}
propups_needed = {}
for p_s in propup_suites:
propups_needed[p_s.suite_id] = set()
psuites_current[p_s.suite_id] = {}
q = get_binary_q(p_s.suite_id)
for i in q:
key = (i[0], i[2])
# the query is sorted, so we only keep the newest version
psuites_current[p_s.suite_id][key] = i[1]
q = get_source_q(p_s.suite_id)
for i in q:
key = (i[0], i[2])
# the query is sorted, so we only keep the newest version
psuites_current[p_s.suite_id][key] = i[1]
# Build up a dictionary of what should be in the suite
desired = set()
for line in lines:
split_line = line.strip().split()
if len(split_line) != 3:
utils.warn("'%s' does not break into 'package version architecture'." % (line[:-1]))
continue
desired.add(tuple(split_line))
version_check = VersionCheck(suite.suite_name, force, session)
# Check to see which packages need added and add them
for key in sorted(desired, key=functools.cmp_to_key(cmp_package_version)):
if key not in current:
(package, version, architecture) = key
version_check(package, architecture, version)
pkg = get_pkg(package, version, architecture, session)
if pkg is None:
continue
copy_to_suites(transaction, pkg, suites)
Logger.log(["added", suite.suite_name, " ".join(key)])
check_propups(pkg, psuites_current, propups_needed)
# Check to see which packages need removed and remove them
for key, pkid in current.items():
if key not in desired:
(package, version, architecture) = key
if architecture == "source":
session.execute("""DELETE FROM src_associations WHERE id = :pkid""", {'pkid': pkid})
else:
session.execute("""DELETE FROM bin_associations WHERE id = :pkid""", {'pkid': pkid})
Logger.log(["removed", suite.suite_name, " ".join(key), pkid])
for p_s in propup_suites:
for pkg in propups_needed[p_s.suite_id]:
copy_to_suites(transaction, pkg, [p_s])
info = (pkg.name, pkg.version, pkg.arch_string)
Logger.log(["propup", p_s.suite_name, " ".join(info)])
session.commit()
if britney:
britney_changelog(current, suite, session)
#######################################################################################
[docs]def process_file(file, suite, action, transaction, britney=False, force=False):
session = transaction.session
if action == "set":
set_suite(file, suite, transaction, britney, force)
return
suite_id = suite.suite_id
suites = [suite] + [q.suite for q in suite.copy_queues]
extra_archives = [suite.archive]
request = []
# Our session is already in a transaction
for line in file:
split_line = line.strip().split()
if len(split_line) != 3:
utils.warn("'%s' does not break into 'package version architecture'." % (line[:-1]))
continue
request.append(split_line)
request.sort(key=functools.cmp_to_key(cmp_package_version))
version_check = VersionCheck(suite.suite_name, force, session)
for package, version, architecture in request:
pkg = get_pkg(package, version, architecture, session)
if pkg is None:
continue
if architecture == 'source':
pkid = pkg.source_id
else:
pkid = pkg.binary_id
component = pkg.poolfile.component
# Do version checks when adding packages
if action == "add":
version_check(package, architecture, version)
if architecture == "source":
# Find the existing association ID, if any
q = session.execute("""SELECT id FROM src_associations
WHERE suite = :suiteid and source = :pkid""",
{'suiteid': suite_id, 'pkid': pkid})
ql = q.fetchall()
if len(ql) < 1:
association_id = None
else:
association_id = ql[0][0]
# Take action
if action == "add":
if association_id:
utils.warn("'%s_%s_%s' already exists in suite %s." % (package, version, architecture, suite.suite_name))
continue
else:
for s in suites:
transaction.copy_source(pkg, s, component)
Logger.log(["added", package, version, architecture, suite.suite_name, pkid])
elif action == "remove":
if association_id is None:
utils.warn("'%s_%s_%s' doesn't exist in suite %s." % (package, version, architecture, suite))
continue
else:
session.execute("""DELETE FROM src_associations WHERE id = :pkid""", {'pkid': association_id})
Logger.log(["removed", package, version, architecture, suite.suite_name, pkid])
else:
# Find the existing associations ID, if any
q = session.execute("""SELECT id FROM bin_associations
WHERE suite = :suiteid and bin = :pkid""",
{'suiteid': suite_id, 'pkid': pkid})
ql = q.fetchall()
if len(ql) < 1:
association_id = None
else:
association_id = ql[0][0]
# Take action
if action == "add":
if association_id:
utils.warn("'%s_%s_%s' already exists in suite %s." % (package, version, architecture, suite))
continue
else:
for s in suites:
transaction.copy_binary(pkg, s, component, extra_archives=extra_archives)
Logger.log(["added", package, version, architecture, suite.suite_name, pkid])
elif action == "remove":
if association_id is None:
utils.warn("'%s_%s_%s' doesn't exist in suite %s." % (package, version, architecture, suite))
continue
else:
session.execute("""DELETE FROM bin_associations WHERE id = :pkid""", {'pkid': association_id})
Logger.log(["removed", package, version, architecture, suite.suite_name, pkid])
session.commit()
#######################################################################################
[docs]def get_list(suite, session):
suite_id = suite.suite_id
# List binaries
q = session.execute("""SELECT b.package, b.version, a.arch_string
FROM binaries b, bin_associations ba, architecture a
WHERE ba.suite = :suiteid
AND ba.bin = b.id AND b.architecture = a.id""", {'suiteid': suite_id})
for i in q.fetchall():
print(" ".join(i))
# List source
q = session.execute("""SELECT s.source, s.version
FROM source s, src_associations sa
WHERE sa.suite = :suiteid
AND sa.source = s.id""", {'suiteid': suite_id})
for i in q.fetchall():
print(" ".join(i) + " source")
#######################################################################################
[docs]def main():
global Logger
cnf = Config()
Arguments = [('a', "add", "Control-Suite::Options::Add", "HasArg"),
('b', "britney", "Control-Suite::Options::Britney"),
('f', 'force', 'Control-Suite::Options::Force'),
('h', "help", "Control-Suite::Options::Help"),
('l', "list", "Control-Suite::Options::List", "HasArg"),
('r', "remove", "Control-Suite::Options::Remove", "HasArg"),
('s', "set", "Control-Suite::Options::Set", "HasArg")]
for i in ["add", "britney", "help", "list", "remove", "set", "version"]:
key = "Control-Suite::Options::%s" % i
if key not in cnf:
cnf[key] = ""
try:
file_list = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv)
except SystemError as e:
print("%s\n" % e)
usage(1)
Options = cnf.subtree("Control-Suite::Options")
if Options["Help"]:
usage()
force = "Force" in Options and Options["Force"]
action = None
for i in ("add", "list", "remove", "set"):
if cnf["Control-Suite::Options::%s" % (i)] != "":
suite_name = cnf["Control-Suite::Options::%s" % (i)]
if action:
utils.fubar("Can only perform one action at a time.")
action = i
# Need an action...
if action is None:
utils.fubar("No action specified.")
britney = False
if action == "set" and cnf["Control-Suite::Options::Britney"]:
britney = True
if action == "list":
session = DBConn().session()
suite = get_suite(suite_name, session)
get_list(suite, session)
else:
Logger = daklog.Logger("control-suite")
with ArchiveTransaction() as transaction:
session = transaction.session
suite = get_suite(suite_name, session)
if action == "set" and not suite.allowcsset:
if force:
utils.warn("Would not normally allow setting suite {0} (allowcsset is FALSE), but --force used".format(suite_name))
else:
utils.fubar("Will not reset suite {0} due to its database configuration (allowcsset is FALSE)".format(suite_name))
if file_list:
for f in file_list:
process_file(open(f), suite, action, transaction, britney, force)
else:
process_file(sys.stdin, suite, action, transaction, britney, force)
Logger.close()
#######################################################################################
if __name__ == '__main__':
main()