Skip to content
Snippets Groups Projects
ubuntu-kernel-cleanup.py 6.08 KiB
#!/usr/bin/python3
#
"""
  Remove intermediates and unused kernels from Ubuntu
  Please check /etc/ubuntu-kernel-cleanup.ini for tweaking & information

Usage:
  remove-ubuntu-kernels.py (--real-run | --dry-run)
  remove-ubuntu-kernels.py -h | --help

Options:
  -h --help       Show this screen
  --real-run      Real execution
  --dry-run       Dry execution
"""
import re
import os
import subprocess as sp
import configparser
from distutils.version import LooseVersion
import platform
import apt
try:
    from docopt import docopt
except ModuleNotFoundError as err:
    print("please install \'docopt\' module for python3")

try:
    from packaging.version import parse
except ModuleNotFoundError as err:
    print("please install \'packaging\' module for python3")


def get_packages_list(pkg_match):
    """
      run a shell command like this:
      dpkg --list linux-image-[1-9]* | awk '/^ii /&&/linux-image-/{print $2}'
      and create a sorted list of installed kernels.
      Or if we need the version only:
      dpkg-query -f '${Status} ${Version}\n' -W linux-image-[1-9]* | awk '/ok installed/{print $NF}'
    """
    installed = sp.Popen(
        "dpkg --list %s[1-9]* | awk \'/^ii /&&/%s/{print $2}\'" % (
            pkg_match, pkg_match),
        stdout=sp.PIPE, stderr=sp.PIPE, shell=True
    )
    installed_out, _ = installed.communicate()
    installed_list = installed_out.decode('utf-8').split('\n')
    # delete empty items
    kernels = [item for item in installed_list if item != '']
    kernel_versions = [item.replace(pkg_match, '') for item in kernels]
    # sort by version and unique
    return sorted(list(set(kernel_versions)), key=LooseVersion)


def remove_pkg(version_number, prefix, execution_type):
    """ remove given package """
    for suffix in KERNEL_SUFFIXES:
        pkg = "{}-{}{}".format(prefix, version_number, suffix)
        if execution_type == 'real':
            CACHE.update()
            CACHE.open()
        try:
            CACHE[pkg].is_installed
        except KeyError:
            pass
        else:
            if execution_type == 'real':
                CACHE[pkg].mark_delete(True, True)
                try:
                    CACHE.commit()
                except Exception as err:  # pylint: disable=W0703
                    print("Package removal failed [{err}]".format(
                        err=str(err)))
                finally:
                    CACHE.close()
            else:
                print('Would have removed {} (noop)'.format(pkg))


def get_version(full_pkg_name):
    """
      get version number from the full package name
      for instance linux-generic-5.8.0-34-generic returns 5.8.0-34
    """
    return re.sub(r"(^\D+-|-\D+)", r"", full_pkg_name)


if __name__ == '__main__':

    if os.geteuid() != 0:
        print("You need to have root privileges to run this script")
        print("Please try again, this time using 'sudo'. Exiting.")
        os.sys.exit()

    CFG_ONE = os.path.join(
        os.path.expanduser('~'), ".ubuntu-kernel-cleanup.ini")
    CFG_TWO = '/etc/ubuntu-kernel-cleanup.ini'
    if not os.path.isfile(CFG_ONE) and not os.path.isfile(CFG_TWO):
        print("you need to create either {} or {}.\nGiving up & waiting for better days to come")
        os.sys.exit()

    CONFIG = configparser.RawConfigParser()
    CONFIG.read([CFG_ONE, CFG_TWO])
    try:
        KERNEL_PREFIXES = CONFIG.get(
            'ubuntu-kernel-cleanup', 'kernel_prefixes').replace(' ', '').split(',')
    except configparser.NoOptionError:
        print('could not find an array option for "kernel_prefixes". Exiting')
        os.sys.exit()

    try:
        _KERNEL_SUFFIXES = CONFIG.get(
            'ubuntu-kernel-cleanup', 'kernel_suffixes').replace(' ', '').split(',')
    except configparser.NoOptionError:
        print('could not find an array option for "kernel_suffixes". Exiting')
        os.sys.exit()

    KERNEL_SUFFIXES = [''] + ['-{}'.format(x) for x in _KERNEL_SUFFIXES]

    try:
        COUNT = CONFIG.getint('ubuntu-kernel-cleanup', 'count')
    except configparser.NoOptionError:
        print('could not find an option for "count". Defaulting to 2')
        COUNT = 2

    if COUNT < 1:
        print("--count must greater or equal to 1")
        os.sys.exit()

    EXECUTION = 'dry'
    ARGS = docopt(__doc__)
    if ARGS.get('--real-run'):
        EXECUTION = 'real'

    running_kernel = get_version(platform.uname().release)

    # load packages cache
    CACHE = apt.cache.Cache()

    kern_list = get_packages_list('linux-image-')

    for kern_count in list(range(COUNT)):
        # do we still have kernels in the list?
        if len(kern_list) > 1:
            latest_kernel = kern_list[-1]
            if parse(running_kernel) < parse(latest_kernel):
                # keep only items not containing the latest version
                sanitized_kernel_list = [
                    item for item in kern_list if latest_kernel != item]
            else:
                # we are running the latest kernel
                sanitized_kernel_list = kern_list
                del kern_list[-1]

    try:
        sanitized_kernel_list
    except NameError:
        pass
    else:
        # delete running kernel package from list
        purged_list = [
            item for item in sanitized_kernel_list if running_kernel not in item]

        # uninstall packages from list
        for kernel_pkg in KERNEL_PREFIXES:
            for kernel_version in purged_list:
                remove_pkg(kernel_version, kernel_pkg, EXECUTION)

    CACHE.close()  # it returns always true... keeps closing :)

    # purge empty packages
    if EXECUTION == 'real':
        CMD = "dpkg-query -f \'${Package} ${Status}\\n\' -W linux-* | awk  \'/deinstall ok/{print $1}\' | xargs apt-get purge -y"  # pylint: disable=C0301
    else:
        CMD = "dpkg-query -f \'${Package} ${Status}\\n\' -W linux-* | awk  \'/deinstall ok/{print $1}\' | xargs apt-get purge --assume-no"  # pylint: disable=C0301

    purge = sp.Popen(CMD, stdout=sp.PIPE, stderr=sp.PIPE, shell=True)
    purge_out, _ = purge.communicate()
    print("\ntrying to purge empty kernel packages:\n{}".format(
        purge_out.decode('utf-8')))