Skip to content

Commit 25ff949

Browse files
committed
Add Unattended-Upgrade::Remove-New-Unused-Dependencies
The new Unattended-Upgrade::Remove-New-Unused-Dependencies will remove anything that is no longer required after an unattended-upgrade run. It defaults to "true" to ensure that e.g. old kernels are removed and /boot will not fill up. Closes: LP: #1357093
1 parent d4f5543 commit 25ff949

8 files changed

Lines changed: 129 additions & 39 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ Supported Options Reference
200200

201201
Remove all unused dependencies after the upgrade finished.
202202

203+
* `Unattended-Upgrade::Remove-New-Unused-Dependencies` - boolean (default:True)
204+
205+
Remove any new unused dependencies after the upgrade finished.
206+
203207
* `Unattended-Upgrade::Automatic-Reboot` - boolean (default:False)
204208

205209
Automatically reboot *WITHOUT CONFIRMATION* if the file
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1+
12
Unattended-Upgrade::Allowed-Origins {
2-
"Ubuntu:lucid-security";
3+
"Ubuntu:lucid-security";
34
};
4-
Unattended-Upgrade::Remove-Unused-Dependencies "true";
5+
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";

test/root.unused-deps/usr/bin/dpkg

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/python
2+
3+
import os
4+
import sys
5+
import subprocess
6+
7+
if __name__ == "__main__":
8+
if "--unpack" in sys.argv:
9+
dpkg_status = os.path.join(
10+
os.path.dirname(sys.argv[0]), "..", "..",
11+
"var", "lib", "dpkg", "status")
12+
# pretend a new version without the previous dependency got installed
13+
subprocess.check_call(
14+
["sed", "-i", "/Depends:\ test-package-dependency/d", dpkg_status])
15+
subprocess.check_call(
16+
["sed", "-i", "s/1.0.test.pkg/2.0.test.pkg/", dpkg_status])

test/root.unused-deps/var/lib/apt/lists/archive.ubuntu.com_ubuntu_dists_lucid-security_main_binary-amd64_Packages

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Package: test-package
22
Architecture: all
3-
Version: 2.0
3+
Version: 2.0.test.pkg
44
Description-en: test package
55
Filename: test-package_2.0_all.deb
66
SHA256: 01704da7de63ca12109abff5c2c480ad9c757e669c4428424de23ec97bf24248

test/root.unused-deps/var/lib/dpkg/status

Lines changed: 0 additions & 14 deletions
This file was deleted.

test/root.unused-deps/var/run/.silly-git

Whitespace-only changes.

test/test_remove_unused.py

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/python
22

33
import os
4+
import subprocess
45
import unittest
56

67
import apt
@@ -13,7 +14,7 @@
1314
class MockOptions(object):
1415
debug = False
1516
verbose = False
16-
dry_run = True
17+
dry_run = False
1718
apt_debug = False
1819
minimal_upgrade_steps = False
1920

@@ -24,23 +25,96 @@ def setUp(self):
2425
self.rootdir = os.path.abspath("./root.unused-deps")
2526
dpkg_status = os.path.abspath(
2627
os.path.join(self.rootdir, "var", "lib", "dpkg", "status"))
28+
# fake dpkg status
29+
with open(dpkg_status, "w") as fp:
30+
fp.write("""Package: test-package
31+
Status: install ok installed
32+
Architecture: all
33+
Version: 1.0.test.pkg
34+
Depends: test-package-dependency
35+
36+
Package: test-package-dependency
37+
Status: install ok installed
38+
Architecture: all
39+
Version: 1.0
40+
41+
Package: z-package
42+
Status: install ok installed
43+
Architecture: all
44+
Version: 1.0
45+
46+
Package: old-unused-dependency
47+
Status: install ok installed
48+
Architecture: all
49+
Version: 1.0
50+
""")
2751
apt.apt_pkg.config.set("Dir::State::status", dpkg_status)
2852
apt.apt_pkg.config.clear("DPkg::Pre-Invoke")
2953
apt.apt_pkg.config.clear("DPkg::Post-Invoke")
54+
apt.apt_pkg.config.set("Debug::NoLocking", "true")
55+
# we don't really run dpkg
56+
apt.apt_pkg.config.set(
57+
"Dir::Bin::Dpkg", os.path.join(self.rootdir, "bin", "dpkg"))
3058
# pretend test-package-dependency is auto-installed
3159
extended_states = os.path.join(
3260
self.rootdir, "var", "lib", "apt", "extended_states")
3361
with open(extended_states, "w") as f:
34-
f.write("Package: test-package-dependency\nAuto-Installed: 1\n")
62+
f.write("""
63+
Package: old-unused-dependency
64+
Architecture: all
65+
Auto-Installed: 1
66+
67+
Package: test-package-dependency
68+
Architecture: all
69+
Auto-Installed: 1
70+
""")
71+
# clean log
72+
self.log = os.path.join(
73+
self.rootdir, "var", "log", "unattended-upgrades",
74+
"unattended-upgrades.log")
75+
if not os.path.exists(os.path.dirname(self.log)):
76+
os.makedirs(os.path.dirname(self.log))
77+
with open(self.log, "w"):
78+
pass
79+
# clean cache
80+
subprocess.check_call(
81+
["rm", "-f",
82+
os.path.join(self.rootdir, "var", "cache", "apt", "*.bin")])
3583

3684
def test_remove_unused_dependencies(self):
85+
apt_conf = os.path.join(self.rootdir, "etc", "apt", "apt.conf")
86+
with open(apt_conf, "w") as fp:
87+
fp.write("""
88+
Unattended-Upgrade::Allowed-Origins {
89+
"Ubuntu:lucid-security";
90+
};
91+
Unattended-Upgrade::Remove-Unused-Dependencies "true";
92+
""")
3793
options = MockOptions()
3894
unattended_upgrade.main(
3995
options, rootdir="./root.unused-deps")
40-
log = os.path.join(
41-
self.rootdir, "var", "log", "unattended-upgrades",
42-
"unattended-upgrades.log")
43-
with open(log) as f:
96+
with open(self.log) as f:
97+
# both the new and the old unused dependency are removed
98+
needle = "Packages that are auto removed: "\
99+
"'test-package-dependency old-unused-dependency'"
100+
haystack = f.read()
101+
self.assertTrue(needle in haystack,
102+
"Can not find '%s' in '%s'" % (needle, haystack))
103+
104+
def test_remove_unused_dependencies_new_unused_only(self):
105+
apt_conf = os.path.join(self.rootdir, "etc", "apt", "apt.conf")
106+
with open(apt_conf, "w") as fp:
107+
fp.write("""
108+
Unattended-Upgrade::Allowed-Origins {
109+
"Ubuntu:lucid-security";
110+
};
111+
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
112+
""")
113+
options = MockOptions()
114+
unattended_upgrade.main(
115+
options, rootdir="./root.unused-deps")
116+
with open(self.log) as f:
117+
# ensure its only exactly one package that is removed
44118
needle = "Packages that are auto removed: "\
45119
"'test-package-dependency'"
46120
haystack = f.read()

unattended-upgrade

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,8 +1088,7 @@ def get_auto_removable(cache):
10881088
if pkg.is_auto_removable])
10891089

10901090

1091-
def do_auto_remove(cache, options, logfile_dpkg, verbose=False):
1092-
auto_removable = get_auto_removable(cache)
1091+
def do_auto_remove(cache, auto_removable, logfile_dpkg, verbose=False):
10931092
if not auto_removable:
10941093
return True
10951094

@@ -1300,9 +1299,10 @@ def main(options, rootdir=""):
13001299
logging.debug("dpkg is configured not to cause conffile prompts")
13011300

13021301
# auto-removal
1302+
previous_autoremovals = get_auto_removable(cache)
13031303
if apt_pkg.config.find_b(
13041304
"Unattended-Upgrade::Remove-Unused-Dependencies", False):
1305-
pending_autoremovals = get_auto_removable(cache)
1305+
pending_autoremovals = previous_autoremovals
13061306
else:
13071307
pending_autoremovals = []
13081308

@@ -1342,7 +1342,7 @@ def main(options, rootdir=""):
13421342
logfile_dpkg = os.path.join(_get_logdir(), 'unattended-upgrades-dpkg.log')
13431343

13441344
# only perform install step if we actually have packages to install
1345-
pkg_install_success = None
1345+
pkg_install_success = True
13461346
shutdown_lock = -1
13471347
if len(pkgs_to_upgrade) > 0:
13481348
# lock for the shutdown check - its fine if the system
@@ -1356,21 +1356,30 @@ def main(options, rootdir=""):
13561356
options,
13571357
logfile_dpkg)
13581358

1359-
# remove unneeded packages if there are any
1359+
# now check if any auto-removing needs to be done
1360+
cache = UnattendedUpgradesCache(
1361+
rootdir=rootdir, allowed_origins=allowed_origins)
1362+
if cache._depcache.broken_count > 0:
1363+
print(_("Cache has broken packages, exiting"))
1364+
logging.error(_("Cache has broken packages, exiting"))
1365+
sys.exit(1)
1366+
1367+
# the user wants *all* auto-removals to be removed
13601368
if apt_pkg.config.find_b(
13611369
"Unattended-Upgrade::Remove-Unused-Dependencies", False):
1362-
cache = UnattendedUpgradesCache(rootdir=rootdir,
1363-
allowed_origins=allowed_origins)
1364-
if cache._depcache.broken_count > 0:
1365-
print(_("Cache has broken packages, exiting"))
1366-
logging.error(_("Cache has broken packages, exiting"))
1367-
sys.exit(1)
1368-
1369-
remove_success = do_auto_remove(
1370-
cache, options, logfile_dpkg,
1370+
auto_removals = get_auto_removable(cache)
1371+
pkg_install_success &= do_auto_remove(
1372+
cache, auto_removals, logfile_dpkg,
1373+
options.verbose or options.debug)
1374+
# the user wants *only new* auto-removals to be removed
1375+
elif apt_pkg.config.find_b(
1376+
"Unattended-Upgrade::Remove-New-Unused-Dependencies", True):
1377+
# calculate the new auto-removals
1378+
new_pending_autoremovals = get_auto_removable(cache)
1379+
auto_removals = new_pending_autoremovals - previous_autoremovals
1380+
pkg_install_success &= do_auto_remove(
1381+
cache, auto_removals, logfile_dpkg,
13711382
options.verbose or options.debug)
1372-
if pkg_install_success is not False:
1373-
pkg_install_success = remove_success
13741383

13751384
logging.debug("InstCount=%i DelCount=%i BrokenCount=%i"
13761385
% (cache._depcache.inst_count,

0 commit comments

Comments
 (0)