Skip to content

Instantly share code, notes, and snippets.

@lpar
Created June 20, 2011 17:13
Show Gist options
  • Save lpar/1036034 to your computer and use it in GitHub Desktop.
Save lpar/1036034 to your computer and use it in GitHub Desktop.
jay - a utility for removing excess kernel files from Ubuntu systems
#!/usr/bin/env ruby
# encoding: UTF-8
'di '
'ig00 '
# This is both a Ruby script and a man page; you can symlink it into your
# man directory as /usr/local/man/man8/jay.1 or run man -l on this file.
# The package which contains the kernel image itself
KERNEL_PACKAGE = 'linux-image'
# Packages associated with the kernel that should be cleaned up to match
PACKAGES = ['linux-headers', 'linux-restricted-modules',
'linux-backports-modules']
# Implements a class to make it easy to handle version strings.
# Assumes that 2.6 == 2.6.0, and hence that 2.6 < 2.6.1.
# Lets you do stuff like:
# a = [ Version.new('2.6.3'),
# Version.new('2.6.12'),
# Version.new('2.12.1'),
# Version.new('2.4'),
# Version.new('2.13'),
# Version.new('1.22.22')]
# puts a.sort
# and get the right answer.
class Version
include Comparable
def initialize(s)
@string = s
@numbers = s.split(/[.-]+/).map {|x| Integer(x)}
end
def Version.extract(s)
vs = s.match(/(\d[\d\.-]+\d)/)
if vs
return Version.new(vs[1])
else
return nil
end
end
def [](n)
return @numbers[n]
end
def length
return @numbers.length
end
def append(x) @numbers << x end
def <=>(other)
# For ease of comparison, we make sure both versions have the same number
# of punctuation-separated elements, by adding as many .0s as necessary
# to whichever is shorter. This means that eventually your Version objects
# will all grow to have as many elements as the longest, but that isn't
# an issue for our use here as the elements are only used for sorting; we
# only ever output the original string.
while @numbers.length < other.length
@numbers << 0
end
while @numbers.length > other.length
other.append(0)
end
n = 0
result = 0
while result == 0 and n < @numbers.length
result = self[n] <=> other[n]
n += 1
end
return result
end
def to_s
return @string
end
end
# Now we understand versions, work out what we're running right now
RUNNING_KERNEL = Version.extract(`uname -r`)
# Class to represent a single installed version of a single package
# i.e. the contents of one .deb file.
class Deb
include Comparable
attr_accessor :version
attr_accessor :name
# Initialize from a Version and a package name
# e.g. Version.new(2.6.5), 'linux-image'
def initialize(version, name)
@version = version
@name = name
end
def <=>(other)
return @version <=> other.version
end
def to_s
return @name
end
end
# Class to represent all installed versions of a package,
# i.e. the package as a whole.
class Package
attr_accessor :keep # List of Debs to keep
attr_accessor :remove # List of Debs to delete
# Initialize from a package name (with no appended versions)
# e.g. 'linux-image'
def initialize(pkgname)
@name = pkgname
# Ask dpkg for all the installed versions
dpkgoutput = `dpkg --get-selections #{pkgname}*`
@versions = Array.new
dpkgoutput.each_line do |line|
if line.match(/\sinstall$/)
pkgname = line.strip.sub!(/\s.*$/, '')
v = Version.extract(pkgname)
if v
@versions << Deb.new(v, pkgname)
end
end
end
@versions.sort! {|a,b| b <=> a}
end
# Return the list of installed versions, as a list of Versions
def installed(package)
return @versions.map {|x| x.version}
end
# Return the two most recent versions as Version objects; plus the running
# kernel version, if that's not one of the two most recent.
def recent
result = @versions[0,2]
if result[0] != RUNNING_KERNEL and result[1] != RUNNING_KERNEL
result << RUNNING_KERNEL
end
return result
end
# Filter the installed packages according to the argument, an array
# of Versions to keep.
# After calling this method, @keep and @delete are available, and contain
# lists of full package names with version numbers to keep and to delete.
def filter!(verstokeep)
@keep = Array.new
@remove = Array.new
for v in @versions
if verstokeep.member?(v)
@keep << v
else
@remove << v
end
end
end
end
# Now the actual cleanup task...
# Find out the most recent 2 kernel versions (and running version)
kernel = Package.new(KERNEL_PACKAGE)
keepers = kernel.recent
kernel.filter!(keepers)
# Start assembling lists of packages we'll keep or delete
grand_keep_list = kernel.keep
grand_remove_list = kernel.remove
# Run through kernel-related packages and work out what versions we have.
# Keep versions which go with the kernels we intend to keep; delete all the
# others.
for package in (PACKAGES)
pkg = Package.new(package)
pkg.filter!(keepers)
grand_keep_list.concat(pkg.keep)
grand_remove_list.concat(pkg.remove)
end
# Prompt the user, then call APT to do the work.
puts "Packages to keep:"
grand_keep_list.each {|x| puts " #{x}"}
puts "\nPackages to remove:"
grand_remove_list.each {|x| puts " #{x}"}
puts "\nDo you want me to remove the packages listed for removal above?"
puts "If you are absolutely sure, type 'yes' (without the quotes) and hit enter:"
print "> "
line = STDIN.gets
if line.strip != 'yes'
puts "Leaving packages untouched."
exit 0
end
cmd = ['/usr/bin/apt-get', 'purge']
cmd.concat(grand_remove_list.map {|x| x.to_s })
exec(*cmd)
__END__
.00
'di
.TH jay 8 "20 Jun 2011"
.SH NAME
jay \- Remove excess kernels
.SH SYNOPSIS
.B jay
.SH DESCRIPTION
.PP
This utility scans a Ubuntu system and removes all but the two most
recent kernels, plus whichever kernel is running. The running kernel will
typically be one of the two most recent, but might not be.
.PP
It should work on any Ubuntu variant. It should also be fairly easy to make
it work on Debian by adjusting the constants at the top of the code.
.PP
While old kernel cleanup is performed automatically in recent releases of
Ubuntu, this script is still useful for older LTS releases which are still
supported as of 2011.
.PP
Before doing anything, the program prompts the user with a full list of
packages to be kept and packages to be removed, and asks for confirmation
that it has worked out the lists appropriately. It then calls apt\-get, which
will also ask for a final confirmation before purging anything. So if you
hose your system, please do not blame anyone but yourself, because you will
have been asked twice to confirm that the changes look sensible.
.PP
Note that apt\-get removes kernel packages one at a time, and rebuilds the
boot menu after each removal, so if you have multiple kernels to remove it
can take a long time.
.SH BUGS
You may get scary looking warnings that say "Status: Before uninstall, this
module version was ACTIVE on this kernel." The kernel the error message refers
to the one it is uninstalling, not the one you are running, so don't worry.
.SH AUTHOR
mathew <meta@pobox.com>
.SH COPYRIGHT
Copyright (c) 2010. Licensed under the GNU Public Licenses version 3.0.
.PP
Two important clauses from that license are excerpted here:
.PP
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER
EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO
THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM
PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
CORRECTION.
.PP
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY
COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM
AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL,
SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR
DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR
A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH
HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment