3
# Copyright 2009 Canonical Ltd. This software is licensed under the
4
# GNU Affero General Public License version 3 (see the file LICENSE).
6
# General purpose package removal tool for ftpmaster
8
################################################################################
22
from zope.component import getUtility
24
from canonical.config import config
25
from canonical.database.constants import UTC_NOW
26
from canonical.launchpad.scripts import (
27
execute_zcml_for_scripts,
31
from canonical.lp import initZopeless
32
from lp.registry.interfaces.distribution import IDistributionSet
33
from lp.soyuz.enums import PackagePublishingStatus
34
from lp.soyuz.model.publishing import (
35
BinaryPackagePublishingHistory,
36
SourcePackagePublishingHistory,
39
from contrib.glock import GlobalLock
41
################################################################################
43
re_strip_source_version = re.compile (r'\s+.*$')
44
re_build_dep_arch = re.compile(r"\[[^]]+\]")
46
################################################################################
53
################################################################################
56
answer = dak_utils.our_raw_input("Continue (y/N)? ").lower()
61
################################################################################
63
# def reverse_depends_check(removals, suites):
64
# print "Checking reverse dependencies..."
65
# components = Cnf.ValueList("Suite::%s::Components" % suites[0])
68
# for architecture in Cnf.ValueList("Suite::%s::Architectures" % suites[0]):
69
# if architecture in ["source", "all"]:
72
# virtual_packages = {}
73
# for component in components:
74
# filename = "%s/dists/%s/%s/binary-%s/Packages.gz" \
75
# % (Cnf["Dir::Root"], suites[0], component,
77
# # apt_pkg.ParseTagFile needs a real file handle and can't
78
# # handle a GzipFile instance...
79
# temp_filename = dak_utils.temp_filename()
80
# (result, output) = commands.getstatusoutput("gunzip -c %s > %s" \
81
# % (filename, temp_filename))
83
# dak_utils.fubar("Gunzip invocation failed!\n%s\n" \
85
# packages = open(temp_filename)
86
# Packages = apt_pkg.ParseTagFile(packages)
87
# while Packages.Step():
88
# package = Packages.Section.Find("Package")
89
# depends = Packages.Section.Find("Depends")
91
# deps[package] = depends
92
# provides = Packages.Section.Find("Provides")
93
# # Maintain a counter for each virtual package. If a
94
# # Provides: exists, set the counter to 0 and count all
95
# # provides by a package not in the list for removal.
96
# # If the counter stays 0 at the end, we know that only
97
# # the to-be-removed packages provided this virtual
100
# for virtual_pkg in provides.split(","):
101
# virtual_pkg = virtual_pkg.strip()
102
# if virtual_pkg == package: continue
103
# if not virtual_packages.has_key(virtual_pkg):
104
# virtual_packages[virtual_pkg] = 0
105
# if package not in removals:
106
# virtual_packages[virtual_pkg] += 1
107
# p2c[package] = component
109
# os.unlink(temp_filename)
111
# # If a virtual package is only provided by the to-be-removed
112
# # packages, treat the virtual package as to-be-removed too.
113
# for virtual_pkg in virtual_packages.keys():
114
# if virtual_packages[virtual_pkg] == 0:
115
# removals.append(virtual_pkg)
117
# # Check binary dependencies (Depends)
118
# for package in deps.keys():
119
# if package in removals: continue
122
# parsed_dep += apt_pkg.ParseDepends(deps[package])
123
# except ValueError, e:
124
# print "Error for package %s: %s" % (package, e)
125
# for dep in parsed_dep:
126
# # Check for partial breakage. If a package has a ORed
127
# # dependency, there is only a dependency problem if all
128
# # packages in the ORed depends will be removed.
130
# for dep_package, _, _ in dep:
131
# if dep_package in removals:
133
# if unsat == len(dep):
134
# component = p2c[package]
135
# if component != "main":
136
# what = "%s/%s" % (package, component)
138
# what = "** %s" % (package)
139
# print "%s has an unsatisfied dependency on %s: %s" \
140
# % (what, architecture, dak_utils.pp_deps(dep))
143
# # Check source dependencies (Build-Depends and Build-Depends-Indep)
144
# for component in components:
145
# filename = "%s/dists/%s/%s/source/Sources.gz" \
146
# % (Cnf["Dir::Root"], suites[0], component)
147
# # apt_pkg.ParseTagFile needs a real file handle and can't
148
# # handle a GzipFile instance...
149
# temp_filename = dak_utils.temp_filename()
150
# result, output = commands.getstatusoutput("gunzip -c %s > %s" \
151
# % (filename, temp_filename))
153
# sys.stderr.write("Gunzip invocation failed!\n%s\n" \
156
# sources = open(temp_filename)
157
# Sources = apt_pkg.ParseTagFile(sources)
158
# while Sources.Step():
159
# source = Sources.Section.Find("Package")
160
# if source in removals: continue
162
# for build_dep_type in ["Build-Depends", "Build-Depends-Indep"]:
163
# build_dep = Sources.Section.get(build_dep_type)
165
# # Remove [arch] information since we want to see
166
# # breakage on all arches
167
# build_dep = re_build_dep_arch.sub("", build_dep)
169
# parsed_dep += apt_pkg.ParseDepends(build_dep)
170
# except ValueError, e:
171
# print "Error for source %s: %s" % (source, e)
172
# for dep in parsed_dep:
174
# for dep_package, _, _ in dep:
175
# if dep_package in removals:
177
# if unsat == len(dep):
178
# if component != "main":
179
# source = "%s/%s" % (source, component)
181
# source = "** %s" % (source)
182
# print "%s has an unsatisfied build-dependency: %s" \
183
# % (source, dak_utils.pp_deps(dep))
186
# os.unlink(temp_filename)
189
# print "Dependency problem found."
193
# print "No dependency problem found."
196
################################################################################
201
parser = optparse.OptionParser()
202
logger_options(parser)
203
parser.add_option("-a", "--architecture", dest="architecture",
204
help="only act on ARCHITECTURE")
205
parser.add_option("-b", "--binary", dest="binaryonly",
206
default=False, action="store_true",
207
help="remove binaries only")
208
parser.add_option("-c", "--component", dest="component",
209
help="only act on COMPONENT")
210
parser.add_option("-d", "--distro", dest="distro",
211
help="remove from DISTRO")
212
parser.add_option("-m", "--reason", dest="reason",
213
help="reason for removal")
214
parser.add_option("-n", "--no-action", dest="action",
215
default=True, action="store_false",
216
help="don't do anything")
217
parser.add_option("-R", "--rdep-check", dest="rdepcheck",
218
default=False, action="store_true",
219
help="check reverse dependencies")
220
parser.add_option("-s", "--suite", dest="suite",
221
help="only act on SUITE")
222
parser.add_option("-S", "--source-only", dest="sourceonly",
223
default=False, action="store_true",
224
help="remove source only")
226
(Options, arguments) = parser.parse_args()
228
# Sanity check options
230
dak_utils.fubar("need at least one package name as an argument.")
231
if Options.architecture and Options.sourceonly:
232
dak_utils.fubar("can't use -a/--architecutre and -S/"
233
"--source-only options simultaneously.")
234
if Options.binaryonly and Options.sourceonly:
235
dak_utils.fubar("can't use -b/--binary-only and -S/"
236
"--source-only options simultaneously.")
238
if not Options.reason:
241
# XXX malcc 2006-08-03: 'dak rm' used to check here whether or not we're
242
# removing from anything other than << unstable. This never got ported
243
# to ubuntu anyway, but it might be nice someday.
245
# Additional architecture checks
246
# XXX James Troup 2006-01-30: parse_args.
247
if Options.architecture and 0:
248
dak_utils.warn("'source' in -a/--argument makes no sense and is ignored.")
252
################################################################################
254
global Lock, Log, ztm
258
arguments = options_init()
260
Log = logger(Options, "remove-package")
262
Log.debug("Acquiring lock")
263
Lock = GlobalLock('/var/lock/launchpad-remove-package.lock')
264
Lock.acquire(blocking=True)
266
Log.debug("Initializing connection.")
267
execute_zcml_for_scripts()
268
ztm = initZopeless(dbuser=config.archivepublisher.dbuser)
270
if not Options.distro:
271
Options.distro = "ubuntu"
272
Options.distro = getUtility(IDistributionSet)[Options.distro]
274
if not Options.suite:
275
Options.suite = Options.distro.currentseries.name
277
Options.architecture = dak_utils.split_args(Options.architecture)
278
Options.component = dak_utils.split_args(Options.component)
279
Options.suite = dak_utils.split_args(Options.suite)
283
################################################################################
285
def summary_to_remove(to_remove):
286
# Generate the summary of what's to be removed
288
for removal in to_remove:
289
package = removal["package"]
290
version = removal["version"]
291
architecture = removal["architecture"]
292
if not d.has_key(package):
294
if not d[package].has_key(version):
295
d[package][version] = []
296
if architecture not in d[package][version]:
297
d[package][version].append(architecture)
302
for package in removals:
303
versions = d[package].keys()
304
versions.sort(apt_pkg.VersionCompare)
305
for version in versions:
306
d[package][version].sort(dak_utils.arch_compare_sw)
307
summary += "%10s | %10s | %s\n" % (package, version,
308
", ".join(d[package][version]))
310
suites_list = dak_utils.join_with_commas_and(Options.suite);
311
print "Will remove the following packages from %s:" % (suites_list)
315
print "------------------- Reason -------------------"
317
print "----------------------------------------------"
322
################################################################################
324
def what_to_remove(packages):
327
# We have 3 modes of package selection: binary-only, source-only
328
# and source+binary. The first two are trivial and obvious; the
329
# latter is a nasty mess, but very nice from a UI perspective so
330
# we try to support it.
332
for removal in packages:
333
for suite in Options.suite:
334
distro_series = Options.distro.getSeries(suite)
336
if Options.sourceonly:
339
if Options.binaryonly:
340
bpp_list = distro_series.getBinaryPackagePublishing(removal)
342
bpp_list = distro_series.getBinaryPackagePublishing(
346
package=bpp.binarypackagerelease.binarypackagename.name
347
version=bpp.binarypackagerelease.version
348
architecture=bpp.distroarchseries.architecturetag
349
if (Options.architecture and
350
architecture not in Options.architecture):
352
if (Options.component and
353
bpp.component.name not in Options.component):
356
type="binary", publishing=bpp, package=package,
357
version=version, architecture=architecture)
360
if not Options.binaryonly:
361
for spp in distro_series.getPublishedSources(removal):
362
package = spp.sourcepackagerelease.sourcepackagename.name
363
version = spp.sourcepackagerelease.version
364
if (Options.component and
365
spp.component.name not in Options.component):
368
type="source",publishing=spp, package=package,
369
version=version, architecture="source")
374
################################################################################
376
def do_removal(removal):
377
"""Perform published package removal.
379
Mark provided publishing record as SUPERSEDED, such that the Domination
380
procedure will sort out its eventual removal appropriately; obeying the
381
rules for archive consistency.
383
current = removal["publishing"]
384
if removal["type"] == "binary":
385
real_current = BinaryPackagePublishingHistory.get(current.id)
387
real_current = SourcePackagePublishingHistory.get(current.id)
388
real_current.status = PackagePublishingStatus.SUPERSEDED
389
real_current.datesuperseded = UTC_NOW
391
################################################################################
398
to_remove = what_to_remove(packages)
402
print "Nothing to do."
405
# If we don't have a reason; spawn an editor so the user can add one
406
# Write the rejection email out as the <foo>.reason file
407
if not Options.reason and Options.action:
408
temp_filename = dak_utils.temp_filename()
409
editor = os.environ.get("EDITOR","vi")
410
result = os.system("%s %s" % (editor, temp_filename))
412
dak_utils.fubar ("vi invocation failed for `%s'!" % (temp_filename),
414
temp_file = open(temp_filename)
415
for line in temp_file.readlines():
416
Options.reason += line
418
os.unlink(temp_filename)
420
summary = summary_to_remove(to_remove)
422
if Options.rdepcheck:
423
dak_utils.fubar("Unimplemented, sucks to be you.")
424
#reverse_depends_check(removals, suites)
426
# If -n/--no-action, drop out here
427
if not Options.action:
430
print "Going to remove the packages now."
433
whoami = dak_utils.whoami()
434
date = commands.getoutput('date -R')
435
suites_list = dak_utils.join_with_commas_and(Options.suite);
437
# Log first; if it all falls apart I want a record that we at least tried.
438
# XXX malcc 2006-08-03: de-hardcode me harder
439
logfile = open("/srv/launchpad.net/dak/removals.txt", 'a')
440
logfile.write("==================================="
441
"======================================\n")
442
logfile.write("[Date: %s] [ftpmaster: %s]\n" % (date, whoami))
443
logfile.write("Removed the following packages from %s:\n\n%s"
444
% (suites_list, summary))
445
logfile.write("\n------------------- Reason -------------------\n%s\n"
447
logfile.write("----------------------------------------------\n")
450
# Do the actual deletion
453
for removal in to_remove:
458
logfile.write("==================================="
459
"======================================\n")
462
################################################################################
464
if __name__ == '__main__':