PDA

View Full Version : [SOLVED] A script to purge Ubuntu kernel files with user reservations



photonxp
June 12th, 2018, 02:47 PM
There are several ways available to purge the redundant Ubuntu kernels , such as by using Synaptic or some one-liner scripts online. But if someone also wants to control how many oldest kernels or newest kernels they'd like to reserve, this is probably the script they need.

Added feature: Selectively purging packages of a single kernel from user input.

Now it also supports Ubuntu 18.04 and debian 8, 9 amd64.

There're built-in variables for users to tweak. By default 1 oldest and 2 newest kernels are reserved when purge the kernels.
Right now it doesn't support some customized distributions of Ubuntu due to kernel-naming rules

Check the syslog (/var/log/syslog) if problems emerge. You can also try changing the code for your situation.

purge_kernel_by_rules.sh


#!/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_L INUX_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_I MAGE_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_LI NUX_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_LINU X_IMAGE[@]:$NUMBER_OF_OLDEST_KERNELS_TO_PRESERVE:$kernel_pur ge_list_length})
arr_raw_purge_list_LINUX_IMAGE_length=${#arr_raw_p urge_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=""

# 4 packages for ubuntu 16
str_purge_list_LINUX_HEADERS=""
str_purge_list_LINUX_HEADERS_GENERIC=""
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_GENERIC[$j]=${debian_header_string//-common}
arr_purge_list_LINUX_HEADERS[$j]=${arr_purge_list_LINUX_HEADERS_GENERIC[$j]//-generic}

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
image_string="${arr_raw_purge_list_LINUX_IMAGE[$i]}"

# skip if the kernel string is in the reserved kernel version list
printf "$reserve_list" | grep -q "$image_string"
if [ 0 -eq $? ]
then
continue
fi

#echo "j: $j"
# header for debian
debian_header_string="${image_string//image/headers}"
arr_purge_list_LINUX_HEADERS[$j]="$debian_header_string"

# 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
}

arr_to_str_purge_list(){
str_purge_list_LINUX_HEADERS="${arr_purge_list_LINUX_HEADERS[@]}"
str_purge_list_LINUX_HEADERS_GENERIC="${arr_purge_list_LINUX_HEADERS_GENERIC[@]}"
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[@]}"
}

format_str_kernel_purge_list(){
# change *_purge_list arr to str here
formatted_kernel_purge_list_str=" \
$str_purge_list_LINUX_IMAGE \
$str_purge_list_LINUX_HEADERS \
$str_purge_list_LINUX_HEADERS_GENERIC \
$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_by_type(){
print_if_no_empty_line "$str_purge_list_LINUX_IMAGE"
echo

print_if_no_empty_line "$str_purge_list_LINUX_HEADERS"
print_if_no_empty_line "$str_purge_list_LINUX_HEADERS_GENERIC"
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
}

show_kernel_purge_list_str_by_type(){
print_with_tail_newline "NOTICE: Generated lists of all kernel packages to be purged:"
print_kernel_purge_list_by_type
}

generate_kernel_purge_list_str(){
init_purge_list_strs
get_linux_distribution_unique_kernels

arr_to_str_purge_list
format_str_kernel_purge_list

show_kernel_purge_list_str_by_type
}

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 $formatted_kernel_purge_list_str"
print_with_tail_newline "$purge_cmd"

# grep command in this script could return err
# so set this arg 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 "$@"


Experienced lab-rats are welcome. ;)

login profile script to check if the boot partition has enough remaining space (https://ubuntuforums.org/showthread.php?t=2393234)

TheFu
June 12th, 2018, 04:37 PM
Perhaps this belongs in the "how-to" subforum?

purge-old-kernels command? OTOH, this sort of script is a good example to practice scripting.

and apt autoremove deals with dependency cleanup.

jan4
June 14th, 2018, 06:56 PM
Well, tried the script and ran into a little hick-up. Have three kernels installed and set variable NUMBER_OF_OLDEST_KERNELS_TO_PRESERVE to 0. This is what I get if I run the script:


sudo ./purge_kernel_by_rules.sh

NOTICE: Existing base kernel images on your system:
linux-image-4.15.0-20-generic got 1th
linux-image-4.15.0-22-generic got 2th
linux-image-4.15.0-23-generic got 3th

NOTICE: Start getting lists of all kernel images to be purged:
linux-image-4.15.0-20-generic
linux-image-extra-4.15.0-20-generic
linux-signed-image-4.15.0-20-generic
linux-headers-4.15.0-20

NOTICE: Check all the kernel versions above before the next step
Press Ctrl+C to interrupt or press Enter to continue:
Continued..


apt-get purge linux-image-4.15.0-20-generic linux-image-extra-4.15.0-20-generic linux-signed-image-4.15.0-20-generic linux-headers-4.15.0-20
Reading package lists... Done
Building dependency tree
Reading state information... Done
E: Unable to locate package linux-image-extra-4.15.0-20-generic
E: Couldn't find any package by glob 'linux-image-extra-4.15.0-20-generic'
E: Couldn't find any package by regex 'linux-image-extra-4.15.0-20-generic'
E: Unable to locate package linux-signed-image-4.15.0-20-generic
E: Couldn't find any package by glob 'linux-signed-image-4.15.0-20-generic'
E: Couldn't find any package by regex 'linux-signed-image-4.15.0-20-generic'


It's probably super simple but can't figure out what's wrong. Do you have a clue as to what I have done wrong? Running Ubuntu 18.04 by the way.

TIA

Greetz.

J@n

photonxp
June 15th, 2018, 08:37 AM
...
and apt autoremove deals with dependency cleanup.

Recently I found that `apt autoremove` would also remove kernel packages on its own will, which is not preferred in this case.

Just checked the code in purge-old-kernels,


# Build our list of kernel packages to purge
CANDIDATES=$(ls -tr /boot/vmlinuz-* | head -n -${KEEP} | grep -v "$(uname -r)$" | cut -d- -f2- | awk '{print "linux-image-" $0 " linux-headers-" $0}' )


It seems purge-old-kernels only check the packages of "linux-image-" and " linux-headers-", probably leaving packages "signed-image-", "image-extra-" and "modules-" untouched.
It might be better if it could also care for those untouched kernel packages.

photonxp
June 15th, 2018, 09:03 AM
...
Do you have a clue as to what I have done wrong? Running Ubuntu 18.04 by the way.


It seems the Ubuntu system couldn't find the packages such as "linux-image-extra-4.15.0-20-generic", etc..
That's probably because the package-naming rules for kernels are different on your system. For now I don't have Ubuntu 18.04 available to check. Is it OK to run the following command so that we can see what the naming rules could be like?

dpkg -l | grep linux | grep 4.15.0-20


If Ubuntu 18.04 does change its naming rules, the script need to be further modified for it to work in this situation. Perhaps there's a way to solve this problem.
If it's not due to the changed naming rules (say, you're using Ubuntu 16), probably the system has been bungled somehow or it's a customized distribution of Ubuntu.

jan4
June 16th, 2018, 07:27 AM
Hi,

Thanks for your support!

This is the outcome of dpkg:



dpkg -l | grep linux | grep 4.15.0-20
rc linux-image-4.15.0-20-generic 4.15.0-20.21 amd64 Signed kernel image generic
rc linux-modules-4.15.0-20-generic 4.15.0-20.21 amd64 Linux kernel extra modules for version 4.15.0 on 64 bit x86 SMP
rc linux-modules-extra-4.15.0-20-generic 4.15.0-20.21 amd64 Linux kernel extra modules for version 4.15.0 on 64 bit x86 SMP


Seems that Ubuntu uses "linux-modules-extra-***" instead of "linux-image-extra-***" and also doesn't use the "signed" part in the kernel image name.

I'd wish I had more knowledge of scripting to modify your script myself ;)

VMC
June 16th, 2018, 03:46 PM
I only keep one. And have for ages. This is the alias I use:

dpkg -l linux-* | awk '/^ii/{ print $2}' | grep -v -e `uname -r | cut -f1,2 -d"-"` | grep -e [0-3]| xargs sudo apt-get -y purge --dry-run
Checking first, then remove dry-run when satisfied.

photonxp
June 17th, 2018, 06:19 PM
...
I'd wish I had more knowledge of scripting to modify your script myself ;)

I'd wish that too, though I've changed the code for the Ubuntu 18+ myself. Just embrace the new script with some boldness.

jan4
June 18th, 2018, 05:26 AM
The modified script works great. Something strange though (not very important but still). When I try to run the script for the second time to check/test the terminal opens and closes "in a blink of my eyes".

Thanks!

Doug S
June 18th, 2018, 03:47 PM
Hi,
Just for your information: There are two excellent selective kernel purging scripts available here (https://askubuntu.com/questions/892076/how-to-selectively-purge-old-kernels-all-at-once) (a desktop and server version) and here (https://launchpad.net/linux-purge). I use the server version from the first link.