#!/usr/bin/env python

# print all installed kernel/headers/etc BUT the current and the latest;
# suitable to be used with apt-get remove.
# requires aptitude to be installed. Works on Ubuntu and probably Debian
#
# Originally from:
# http://mostlyunixish.franzoni.eu/blog/2012/06/25/purging-outdated-kernels-on-systems-with-enabled-autoupdate/
#
# With updates from Ewen McNeill <ewen@naos.co.nz>, 2015-03-02
# to fix kernel version matching, sorting order, and Python 2.6 compatibilty
#
# Aims to protect:
# - currently running kernel version
# - latest kernel version
# - at least two kernel versions
# at all times.
#
#---------------------------------------------------------------------------

import subprocess
import re

# Use subprocess.check_output if available (Python 2.7+), or reimplemente
if 'check_output' in dir(subprocess):
    def check_output(*popenargs, **kwargs):
        return subprocess.check_output(*popenargs, **kwargs)
else:
    # Taken from https://gist.github.com/edufelipe/1027906
    # Itself taken from Python 2.7 source code
    # See also: http://stackoverflow.com/questions/4814970/subprocess-check-output-doesnt-seem-to-exist-python-2-6-5
    #
   def check_output(*popenargs, **kwargs):
       r"""Run command with arguments and return its output as a byte string.

        Backported from Python 2.7 as it's implemented as pure python on stdlib.

        >>> check_output(['/usr/bin/python', '--version'])
        Python 2.6.2
       """
       process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
       output, unused_err = process.communicate()
       retcode = process.poll()
       if retcode:
           cmd = kwargs.get("args")
           if cmd is None:
               cmd = popenargs[0]
           error = subprocess.CalledProcessError(retcode, cmd)
           error.output = output
           raise error
       return output    

def runcmd(s):
    return check_output(s.split())

# should work for anything until kernel 9.100
kernel_version_pattern = re.compile("\d\.\d{1,2}\.\d{1,2}-\d{1,3}")
def get_kernel_version(kernelstring):
    return kernel_version_pattern.search(kernelstring).group(0)

# Pad a.b.c-d to be 00a.00b.00c-00d, in order to make it robustly sortable
def get_sortable_kernel_version(kernelstring):
    version_string = get_kernel_version(kernelstring)
    return ".".join(["%03d" % int(part) 
                              for part in re.split(r'[.-]', version_string)])

def get_providing_packages(pkg):
    return filter(lambda x: x != "",
        map(str.strip,
            runcmd('aptitude search ~i~P%s -F%%p' % pkg).split("\n")
            )
        )

def package_if_present(pkg):
    return str.strip(runcmd('aptitude search ~i%s -F%%p' % pkg))

def get_nonmatching_packages(package_list, excluding):
    return [k for k in package_list if not get_kernel_version(k) in
        excluding]


all_kernels = get_providing_packages("linux-image")
all_kernels.sort(key=get_sortable_kernel_version) # lexicographical

kernel_versions = map(get_kernel_version, all_kernels)
latest_version = kernel_versions[-1]
second_latest_version = kernel_versions[-2]
current_version = get_kernel_version(runcmd("uname -r"))

protected_versions = None
if current_version == latest_version:
    protected_versions = (latest_version, second_latest_version)
else:
    protected_versions = (current_version, latest_version)

kernels_to_remove = get_nonmatching_packages(all_kernels, protected_versions)

impl_headers = get_providing_packages("linux-headers")
headers_to_remove = get_nonmatching_packages(impl_headers, protected_versions)

# Also search for extra-modules packages for the kernel packages that
# we are going to remove.  Sadly, these do _not_ have a "Provides:" 
# header so we cannot search for the same way as the 
# linux-image/linux-headers packages.
#
extra_modules = get_providing_packages("linux-image-extra")
extras_to_remove = []
for kernel_package in kernels_to_remove:
    extra_package = \
          package_if_present("linux-image-extra-%s-generic" % 
                              get_kernel_version(kernel_package))
    if extra_package and len(extra_package):
        extras_to_remove.append(extra_package)

# This seems to be needed in Ubuntu >= 13.04
base_headers_to_remove = [header.replace("-generic", "") for header in headers_to_remove]
headers_to_remove += base_headers_to_remove

print " ".join(kernels_to_remove + extras_to_remove + headers_to_remove)
