photon
6/12/2018 - 11:47 AM

Purge redundant Ubuntu kernel files by rules defined in the script, such as the number of kernels to be preserved

Purge redundant Ubuntu kernel files by rules defined in the script, such as the number of kernels to be preserved

#!/bin/bash

# What if we could adjust the clock, 
#     making a day of a virtual AI world much shorter than ours?
# What if a virtual AI being could spend its whole long life 
#     in a blink of our eyes?

#############################################################
# Purpose: 
# Purge redundant Ubuntu kernel files by rules defined in the script
#     such as the number of kernels to be preserved

# Compatibility:
# Ubuntu 16, 18; Debian 8, 9 amd64
# And probably some other debian distributions

# Usage: 
# 1. Default mode, purge the redundant
#     /path/to/purge_kernel_by_rules.bash
# 2. Manual arguments mode, selectively purge. 
#     It's also affected by default values *KERNELS_TO_PRESERVE.
# 2.1 One argument, no need for other kernel packages such as linux-header-
#     /path/to/purge_kernel_by_rules.bash linux-image-333.16.0-6-amd64
#     /path/to/purge_kernel_by_rules.bash "linux-image-333.16.0-6-amd64"
# 2.2 Multiple arguments
#     ./purge_kernel_by_rules.bash linux-image-333.16.0-6-amd64 linux-image-444.4.0-28-generic
#     ./purge_kernel_by_rules.bash "linux-image-444.16.0-20-generic linux-image-444.16.0-20-generic"
# 3. Pipe mode
# 3.1 Simple pipe
#     echo linux-image-333.16.0-6-amd64 linux-image-444.4.0-28-generic | ./purge_kernel_by_rules.bash
# 3.2 Pipe mixed with arguments input
#     echo linux-image-333.16.0-6-amd64 | ./purge_kernel_by_rules.bash linux-image-444.4.0-28-generic
#     The arguments input are listed and purged before the pipe.

# You can also change the number of oldest kernels or newest kernels  
#     that you'd like to preserve 
#     by adjusting the values of NUMBER_OF_OLDEST_KERNELS_TO_PRESERVE
#     and NUMBER_OF_NEWEST_KERNELS_TO_PRESERVE
# By default 1 oldest and 2 newest along with the ruuning kernel
#    are reserved when purge the kernels

# For the running kernel version:
#     If it's in the range of oldest/newest kernels
#         then the final reserve list will be generated by
#         user-defined defalut values
#     If it's not in the range of oldest/newest kernels
#         then the final reserve list will be generated by
#         (user-defined defalut values + running kernel version)

# The number of remaining kernels after purge 
#    shouldn't be less than the number of total kernels to preserve

#### WARNINGS: 
#### This script could seriously damage your system or computer
####    if you use it improperly or carelessly
#### So, use it with cautions and at your own risks
#### A test environment is preferred 

# ATTENTION: 
# You'd have at least 1 kernel 
#     for your system to run

# If the numbers here are all 0
#     then only the running kernel is reserved
#     and all the rest kernels are purged
SET_DEFAULT_VALUES_FOR_RESERVATION(){
    NUMBER_OF_OLDEST_KERNELS_TO_PRESERVE=1
    NUMBER_OF_NEWEST_KERNELS_TO_PRESERVE=2
    number_of_kernels_to_preserve=$((NUMBER_OF_OLDEST_KERNELS_TO_PRESERVE + NUMBER_OF_NEWEST_KERNELS_TO_PRESERVE))
}

print_with_tail_newline(){
    printf "$1 \n\n"
}

notice_to_check(){
    echo
    echo "NOTICE: Check your default values and reset them properly."
    print_with_tail_newline "NOTICE: No removing kernels. Exit." 
}

check_minimum_values_for_reservation(){
    if [ 0 -ge $number_of_kernels_to_preserve ]
        then
        echo
        echo "** CAUTION: The number of non-running kernels to preserve is 0. "
    fi
}

handle_default_values_for_reservation(){
    SET_DEFAULT_VALUES_FOR_RESERVATION    
}

get_running_kernel_info(){
    sys_running=`uname -s`
    ver_running=`uname -r`
    # ver_running="4.4.0-134"
}

list_kernel_candidates(){
    print_with_tail_newline "NOTICE: Existing candidate kernel packages on your system:"
    dpkg -l | grep ' linux-\(image\|headers\|image-extra\|signed-image\|modules\|modules-extra\)'
    print_with_tail_newline "========================================"
    
    issue_info=$(cat /etc/issue)
    echo "Currently running:"
    echo "    $issue_info"
    echo "$(echo '    '$sys_running $ver_running)" 
}

get_script_args(){
    str_list_arg_input_LINUX_IMAGE="$@"
}

generate_OS_LINUX_IMAGE_list(){
    # all the linux-image-[ver] pkges on your system
    str_list_OS_LINUX_IMAGE=$(dpkg --list | grep linux-image-[0-9] | awk '{ print $2 }' | sort -V | xargs echo)
    
    arr_list_OS_LINUX_IMAGE=($str_list_OS_LINUX_IMAGE)   
    length_of_arr_list_OS_LINUX_IMAGE=${#arr_list_OS_LINUX_IMAGE[@]}
    #print_with_tail_newline $length_of_arr_list_OS_LINUX_IMAGE
    #print_with_tail_newline ${arr_list_OS_LINUX_IMAGE[0]}
}

check_OS_LINUX_IMAGE_list_length(){
    if [ $length_of_arr_list_OS_LINUX_IMAGE -le $number_of_kernels_to_preserve ] 
        then
        echo "NOTICE: The number of found linux-image kernel versions"
        echo "NOTICE:     were less or equal to $number_of_kernels_to_preserve ."
        notice_to_check
        exit 1
    fi
}

check_empty_space_input(){
    # invalid input like "    "    
    str=$(echo $str_input_list_LINUX_IMAGE)   
    if [ "y" == "y$str" ]
        then
        print_with_tail_newline "NOTICE: Invalid empty input::Empty kernel string. Exit."
        exit 1
    fi
}

check_OS_LINUX_IMAGE_list_length_for_input(){    
    INPUT_LINUX_IMAGE_list_arr=($str_input_list_LINUX_IMAGE)
    INPUT_LINUX_IMAGE_list_arr_length=${#INPUT_LINUX_IMAGE_list_arr[@]}

    number_of_kernels_to_remain=$((length_of_arr_list_OS_LINUX_IMAGE - INPUT_LINUX_IMAGE_list_arr_length))
    
    if [ $number_of_kernels_to_preserve -gt $number_of_kernels_to_remain ] 
        then        
        echo "NOTICE: The number of remaining versions of linux-image kernel"
        echo "NOTICE:     should be GREATER than $number_of_kernels_to_preserve"
        echo "NOTICE:     after purging the input."
        notice_to_check
        exit 1
    fi   
}

get_running_image_str(){
    for image_str in ${arr_list_OS_LINUX_IMAGE[@]}
        do
        result=$(printf "$image_str" | grep -n "$ver_running")
        if [ 0 -eq $? ]
            then
            # remove the leading "num:" part in the result
            image_str_running="${result#*:}"
            break
        fi
    done

    #echo "arr_list_OS_LINUX_IMAGE: ${arr_list_OS_LINUX_IMAGE[@]}"
    #echo "image_str_running: $image_str_running" 
}

make_raw_reserve_list(){
    # make list for the oldest kernels to reserve
    arr_reserve_list_oldest_LINUX_IMAGE=(${arr_list_OS_LINUX_IMAGE[@]:0:$NUMBER_OF_OLDEST_KERNELS_TO_PRESERVE})
    str_reserve_list_oldest_LINUX_IMAGE="${arr_reserve_list_oldest_LINUX_IMAGE[@]}"

    # make list for the newest kernels to reserve
    length=${#arr_list_OS_LINUX_IMAGE[@]}
    start_idx_arr_reserve_list_newest_LINUX_IMAGE=$(($length - $NUMBER_OF_NEWEST_KERNELS_TO_PRESERVE))
    arr_reserve_list_newest_LINUX_IMAGE=(${arr_list_OS_LINUX_IMAGE[@]:$start_idx_arr_reserve_list_newest_LINUX_IMAGE})
    str_reserve_list_newest_LINUX_IMAGE="${arr_reserve_list_newest_LINUX_IMAGE[@]}"
    
    reserve_list_raw="$str_reserve_list_oldest_LINUX_IMAGE $str_reserve_list_newest_LINUX_IMAGE"
}

make_reserve_list(){
    # add the running kernel version to the raw_reserve_list #

    check_minimum_values_for_reservation
    get_running_image_str
    make_raw_reserve_list

    printf "$reserve_list_raw" | grep -q "$image_str_running"
    if [ 0 -ne $? ] 
        then
        reserve_list="$image_str_running $reserve_list_raw"
    else
        reserve_list="$reserve_list_raw"
    fi
    
    print_with_tail_newline "Reserved kernel list: $reserve_list" 
}

do_input(){
    # arg input, pipe input or pipe with arg input
    str_input_list_LINUX_IMAGE=$1
    
    check_empty_space_input
    check_OS_LINUX_IMAGE_list_length_for_input
    arr_raw_purge_list_LINUX_IMAGE=($str_input_list_LINUX_IMAGE)
    
    make_reserve_list
}

make_reserve_list_for_default(){
    # only show reserve list for terminal users
    # not really used by the default mode
    make_reserve_list
    
    # This is the real reserve_list used by the default mode
    get_running_image_str
    reserve_list="$image_str_running"
}

do_default(){
    echo "NOTICE: Purge with default mode."

    kernel_purge_list_length=$(($length_of_arr_list_OS_LINUX_IMAGE - $number_of_kernels_to_preserve))
    arr_raw_purge_list_LINUX_IMAGE=(${arr_list_OS_LINUX_IMAGE[@]:$NUMBER_OF_OLDEST_KERNELS_TO_PRESERVE:$kernel_purge_list_length})
    arr_raw_purge_list_LINUX_IMAGE_length=${#arr_raw_purge_list_LINUX_IMAGE[@]}
    
    make_reserve_list_for_default
}

do_nopipe(){
    # manual argument mode, purge from user arguments input
    if [ -n "$str_list_arg_input_LINUX_IMAGE" ]
        then
        echo "NOTICE: Purge with user arguments input mode."
        do_input "$str_list_arg_input_LINUX_IMAGE"
    fi
    
    # default mode, no user-input arg
    if [ -z "$str_list_arg_input_LINUX_IMAGE" ]
        then
        do_default
    fi
}

do_pipe(){
    echo "NOTICE: Purge with user pipe (or pipe-mixed) input mode."

    # cat from default stdin, which receive data from the pipe
    str_pipe_lines_LINUX_IMAGE=$(cat | tr -s '\n' ' ')    
    str_list_pipe_input_LINUX_IMAGE="$str_list_arg_input_LINUX_IMAGE $str_pipe_lines_LINUX_IMAGE"
    do_input "$str_list_pipe_input_LINUX_IMAGE"
}

how_to_do(){
    generate_OS_LINUX_IMAGE_list
    check_OS_LINUX_IMAGE_list_length
    
    # check pipe status
    if [ -t 0 ]
        then
        do_nopipe
    else
        do_pipe
    fi
}

init_purge_list_strs(){
    # 1 package for ubuntu and debian 8 amd64
    str_purge_list_LINUX_IMAGE=""
    
    # 3 packages for ubuntu 16
    str_purge_list_LINUX_HEADERS=""
    str_purge_list_LINUX_IMAGE_EXTRA=""
    str_purge_list_LINUX_SIGNED_IMAGE=""
    
    # 2 packages for ubuntu 18
    str_purge_list_LINUX_MODULES=""
    str_purge_list_LINUX_MODULES_EXTRA=""
    
    # all packages for both ubuntu and debian 
    kernel_purge_list_str=""
}

get_version_numbers_by_image_string(){
    image_ver=$(echo $image_string | cut -d - -f 3-4)
    ver_n1=${image_ver%%.*}
    ver_n1_n2=${image_ver%.*}
    ver_n2=${ver_n1_n2#*.}
}

get_ubuntu_unique_kernels(){
    # change header kernel format from debian to ubuntu
    arr_purge_list_LINUX_HEADERS[$j]=${debian_header//-common}

    get_version_numbers_by_image_string         
    # ubuntu 18, kernel 4.15.0
    if [[ ("$ver_n1" -eq 4 && "$ver_n2" -ge 15) || "$ver_n1" -ge 5 ]]
        then
        arr_purge_list_LINUX_MODULES[$j]="${image_string/image/modules}"
        arr_purge_list_LINUX_MODULES_EXTRA[$j]="${image_string/image/modules-extra}"
    else
        arr_purge_list_LINUX_IMAGE_EXTRA[$j]="${image_string//image/image-extra}"
        arr_purge_list_LINUX_SIGNED_IMAGE[$j]="${image_string//image/signed-image}"
    fi
}

get_linux_distribution_unique_kernels(){
    provider_id=${issue_info%% *}
    #for test
    #provider_id="Debian"

    length=${#arr_raw_purge_list_LINUX_IMAGE[@]}
    j=0
    for ((i=0;$i<$length;i++))
        do
        # skip if the kernel string is in the reserved kernel version list
        image_string="${arr_raw_purge_list_LINUX_IMAGE[$i]}"
 
        printf "$reserve_list" | grep -q "$image_string"
        if [ 0 -eq $? ] 
            then
            continue
        fi
        
        #echo "j: $j"
        # header for debian
        debian_header="${image_string//image/headers}"
        arr_purge_list_LINUX_HEADERS[$j]="$debian_header"
        
        # image for debian, ubuntu
        arr_purge_list_LINUX_IMAGE[$j]="$image_string"
        
        # ubuntu 16, 18
        if [ "Ubuntu" == "$provider_id" ]
            then
            get_ubuntu_unique_kernels  
        fi
        j=$((j+1))
    done   
}

format_str_kernel_purge_list(){
    # change *_purge_list arr to str here
    kernel_purge_list_str=" \
        $str_purge_list_LINUX_IMAGE \
        $str_purge_list_LINUX_HEADERS \
        $str_purge_list_LINUX_IMAGE_EXTRA \
        $str_purge_list_LINUX_SIGNED_IMAGE \
        $str_purge_list_LINUX_MODULES \
        $str_purge_list_LINUX_MODULES_EXTRA"
}

print_if_no_empty_line(){
    str_purge_list_no_side_spaces="$(echo $1)"
        if [ y"" != y"$str_purge_list_no_side_spaces" ]
            then 
            echo "$str_purge_list_no_side_spaces"
        fi
}

print_kernel_purge_list(){
    print_if_no_empty_line "$str_purge_list_LINUX_IMAGE" 
    print_if_no_empty_line "$str_purge_list_LINUX_HEADERS"
    echo

    print_if_no_empty_line "$str_purge_list_LINUX_IMAGE_EXTRA"
    print_if_no_empty_line "$str_purge_list_LINUX_SIGNED_IMAGE"
    echo

    print_if_no_empty_line "$str_purge_list_LINUX_MODULES"
    print_if_no_empty_line "$str_purge_list_LINUX_MODULES_EXTRA"
    echo
}

generate_kernel_purge_list_str(){
    print_with_tail_newline "NOTICE: Generated lists of all kernel packages to be purged:"
    init_purge_list_strs   
    get_linux_distribution_unique_kernels       

    str_purge_list_LINUX_HEADERS="${arr_purge_list_LINUX_HEADERS[@]}"
    str_purge_list_LINUX_IMAGE="${arr_purge_list_LINUX_IMAGE[@]}"
    str_purge_list_LINUX_IMAGE_EXTRA="${arr_purge_list_LINUX_IMAGE_EXTRA[@]}"
    str_purge_list_LINUX_SIGNED_IMAGE="${arr_purge_list_LINUX_SIGNED_IMAGE[@]}"
    str_purge_list_LINUX_MODULES="${arr_purge_list_LINUX_MODULES[@]}"
    str_purge_list_LINUX_MODULES_EXTRA="${arr_purge_list_LINUX_MODULES_EXTRA[@]}"    
    print_kernel_purge_list
    format_str_kernel_purge_list
}

vital_check(){
    while true
        do
        echo "NOTICE: Check all the kernel versions above before the next step."
        read -p "Press Ctrl+C to interrupt or press Enter to continue: " input
        if [ -z "$input" ]
            then
            break
        fi
    done

    print_with_tail_newline "Continued.. "
}

purge_kernels_by_purge_list_str(){
    purge_cmd="apt-get purge $kernel_purge_list_str"
    print_with_tail_newline "$purge_cmd"
    
    # grep command in this script could return err
    # so set this in the later part of the script
    set -e
    
    $purge_cmd
    update-grub2
}

main(){
    handle_default_values_for_reservation
    get_running_kernel_info
    list_kernel_candidates

    get_script_args "$@"
    how_to_do
    generate_kernel_purge_list_str

    vital_check
    purge_kernels_by_purge_list_str
}

##################################
main "$@"