├── .gitignore ├── MANIFEST.in ├── kthresher.cron.daily ├── kthresher.systemd.service ├── kthresher.systemd.timer ├── tox.ini ├── kthresher.conf ├── .travis.yml ├── CONTRIBUTING.md ├── kthresher.bash_completion ├── setup.py ├── kthresher.8 ├── CHANGELOG.md ├── LICENSE ├── kthresher.py └── README.rst /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | dist/ 4 | *.egg-info/ 5 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include kthresher.conf 4 | include kthresher.8 5 | -------------------------------------------------------------------------------- /kthresher.cron.daily: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -x /usr/bin/kthresher ] || exit 0 4 | 5 | /usr/bin/kthresher &>/dev/null 6 | -------------------------------------------------------------------------------- /kthresher.systemd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Purge unused kernels 3 | Documentation=man:kthresher(8) 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart=/usr/bin/kthresher 8 | -------------------------------------------------------------------------------- /kthresher.systemd.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Run kthresher daily 3 | 4 | [Timer] 5 | OnCalendar=daily 6 | Persistent=True 7 | 8 | [Install] 9 | WantedBy=timers.target 10 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = True 3 | skip_missing_interpreters = True 4 | envlist = py2, py3, pycodestyle 5 | 6 | [testenv] 7 | sitepackages = True 8 | commands = 9 | {envpython} -V 10 | {envpython} setup.py install 11 | kthresher --version 12 | 13 | [testenv:pycodestyle] 14 | deps = 15 | pycodestyle 16 | basepython = python 17 | commands = 18 | {envpython} -V 19 | pycodestyle --max-line-length=90 kthresher.py 20 | -------------------------------------------------------------------------------- /kthresher.conf: -------------------------------------------------------------------------------- 1 | [main] 2 | include = /etc/kthresher.d/*.conf 3 | 4 | # Is recommended to put the configuration in the included directory instead 5 | # of making changes in here. 6 | # 7 | # The options are: 8 | # headers - (boolean) Include or not the headers, default is false. 9 | # include - [ /path/to/file ] Suports globbing, e.g. /path/to/dir/*.conf 10 | # keep - (int) Number of kernel images to keep (0-9), default is 1. 11 | # purge - (boolean) Commit changes, default is false. 12 | # verbose - (boolean) Be verbose, default is false. 13 | # 14 | # The accepted values for booleans are: 15 | # - yes, on, true 16 | # - no, off, false 17 | # 18 | # NOTES: 19 | # 1) The --dry-run option is only allowed through CLI. 20 | # When purge is defined in the config file and --dry-run is used, the latter has precedence. 21 | # 2) Only the non-boolean options can be overriden from CLI. 22 | 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: generic 3 | dist: xenial 4 | sudo: enabled 5 | branches: 6 | only: 7 | - master 8 | - development 9 | 10 | matrix: 11 | include: 12 | - name: "Ubuntu 14.04 (trusty)" 13 | env: 14 | - DISTRO='ubuntu:14.04' 15 | - name: "Ubuntu 16.04 (xenial)" 16 | env: 17 | - DISTRO='ubuntu:16.04' 18 | - name: "Ubuntu 18.04 (bionic)" 19 | env: 20 | - DISTRO='ubuntu:18.04' 21 | - name: "Debian 8 (jessie)" 22 | env: 23 | - DISTRO='debian:8' 24 | - name: "Debian 9 (stretch)" 25 | env: 26 | - DISTRO='debian:9' 27 | - name: "Debian (buster)" 28 | env: 29 | - DISTRO='debian:buster' 30 | 31 | before_install: 32 | - | 33 | docker run \ 34 | --detach \ 35 | --rm \ 36 | --tty \ 37 | --privileged \ 38 | --network=host \ 39 | --name kthresher_on_${DISTRO/:/_} \ 40 | --mount type=bind,src="$(pwd)",dst=/kthresher \ 41 | ${DISTRO} 42 | - | 43 | docker exec \ 44 | kthresher_on_${DISTRO/:/_} \ 45 | bash -c \ 46 | 'apt-get update; \ 47 | apt-get install -yq --no-install-suggests --no-install-recommends \ 48 | python \ 49 | python3 \ 50 | python-apt \ 51 | python3-apt \ 52 | python-pip \ 53 | python3-pip \ 54 | python-setuptools \ 55 | python3-setuptools' 56 | - | 57 | docker exec \ 58 | kthresher_on_${DISTRO/:/_} \ 59 | bash -c 'pip install tox' 60 | 61 | script: 62 | - > 63 | docker exec \ 64 | kthresher_on_${DISTRO/:/_} \ 65 | bash -c 'cd /kthresher/; tox' 66 | ... 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Workflow 2 | 3 | This project uses gitflow, here some documentation about [how gitflow works](https://datasift.github.io/gitflow/IntroducingGitFlow.html) 4 | 5 | # Issues 6 | 7 | Bugs, RFE or others should be documented in the [issues](https://github.com/rackerlabs/kthresher/issues/). 8 | 9 | ## Guidelines 10 | 1. Before opening an issue, search [open](https://github.com/rackerlabs/kthresher/issues?q=is%3Aopen+is%3Aissue) and [closed](https://github.com/rackerlabs/kthresher/issues?q=is%3Aissue+is%3Aclosed) issues to ensure the issue has not ben reported previously. 11 | 1. Ensure the issue can be reproduced with the latest [`development`](https://github.com/rackerlabs/kthresher/tree/development) version. 12 | 1. The issue should contain detailed instructions and when possible logs, as well as the expected result. 13 | 1. Include details of the environment where it's running, e.g. Debian Jessie, CentOS 7.3, etc. 14 | 1. For RFE explain *Why would this feature be useful?* 15 | 16 | # Pull Requests 17 | 18 | Code contributions are greatly appreciated, please make sure you follow the guidelines below. 19 | 20 | ## Pull requests should be 21 | 1. Made against the `development` branch. 22 | 1. Made from a git feature/fix branch. 23 | 1. Associated to a documented issue. 24 | 25 | ## Pull requests will not be accepted that 26 | 1. Are not made against the `development` branch 27 | 1. Are made by editing files via the GitHub website 28 | 1. Does not have a documented [issue](https://github.com/rackerlabs/kthresher/issues/) for it. 29 | 30 | # Coding Guidelines 31 | 32 | - pep8/pycodestyle 33 | - pyflakes 34 | 35 | # Testing 36 | 37 | - None at the moment :(, would be nice to have some. 38 | -------------------------------------------------------------------------------- /kthresher.bash_completion: -------------------------------------------------------------------------------- 1 | # bash completion for kthresher -*- shell-script -*- 2 | # 3 | # Copyright 2015-2018 Tony Garcia 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | containsElement() 18 | { 19 | # 0 if string in $1 is in the array 20 | # 1 if not 21 | local e 22 | for e in "${@:2}"; do 23 | if [[ "${e}" == "${1}" ]]; then 24 | return 0 25 | fi 26 | done 27 | return 1 28 | } 29 | 30 | genOpts() 31 | { 32 | local OLDIFS 33 | local IFS 34 | local not_used="" 35 | # options in pairs 36 | local opts="--help,-h --config,-c --dry-run,-d --headers,-H --keep,-k --purge,-p " 37 | opts+="--show-autoremoval,-s --verbose,-v --version,-V" 38 | for i in ${opts} ; do 39 | OLDIFS=${IFS} 40 | # set IFS to split our pairs 41 | IFS="," 42 | set -- ${i}`` 43 | # reset IFS to original value 44 | IFS=${OLDIFS} 45 | if ! $( containsElement "${1}" "${COMP_WORDS[@]}" ) && \ 46 | ! $( containsElement "${2}" "${COMP_WORDS[@]}" ); then 47 | not_used="${not_used} ${1}" 48 | not_used="${not_used} ${2}" 49 | fi 50 | done 51 | echo "${not_used}" 52 | } 53 | 54 | _kthresher() 55 | { 56 | local cur=${COMP_WORDS[COMP_CWORD]} 57 | local prev=${COMP_WORDS[COMP_CWORD-1]} 58 | #echo "Genopts $(genOpts) COMP_WORDS ${COMP_WORDS[@]}" 59 | COMPREPLY=( $(compgen -W "$(genOpts)" -- ${cur} ) ) 60 | } 61 | 62 | complete -F _kthresher kthresher 63 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2015 Tony Garcia 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import setuptools 17 | 18 | 19 | def get_version(): 20 | with open("kthresher.py") as f: 21 | for line in f: 22 | if line.startswith("__version__"): 23 | return eval(line.split("=")[-1]) 24 | 25 | 26 | setuptools.setup( 27 | name="kthresher", 28 | version=get_version(), 29 | description=("Purge Unused Kernels."), 30 | long_description=( 31 | "Tool to remove kernel image packages marked as candidates for autoremoval." 32 | ), 33 | url="https://github.com/rackerlabs/kthresher", 34 | author="Tony Garcia", 35 | author_email="tony.garcia@rackspace.com", 36 | license="Apache License, Version 2.0", 37 | entry_points={"console_scripts": ["kthresher=kthresher:main"]}, 38 | py_modules=["kthresher"], 39 | download_url="https://github.com/rackerlabs/kthresher/tarball/{0}".format( 40 | get_version() 41 | ), 42 | classifiers=[ 43 | "Development Status :: 5 - Production/Stable", 44 | "Programming Language :: Python", 45 | "Environment :: Console", 46 | "Operating System :: POSIX :: Linux", 47 | "License :: OSI Approved :: Apache Software License", 48 | "Programming Language :: Python :: 2.7", 49 | "Programming Language :: Python :: 3.4", 50 | "Programming Language :: Python :: 3.5", 51 | "Programming Language :: Python :: 3.6", 52 | "Programming Language :: Python :: 3.7", 53 | ], 54 | ) 55 | -------------------------------------------------------------------------------- /kthresher.8: -------------------------------------------------------------------------------- 1 | .TH kthresher 8 "November 2016" "kthresher" 2 | .SH "NAME" 3 | kthresher \- purge unused kernels 4 | .SH "SYNOPSIS" 5 | .B kthresher [\-h] [\-c 6 | .I FILE 7 | .B ] [\-d] [\-H] [\-k 8 | .I [N] 9 | .B ] [\-p] [\-s] [\-v] [\-V] 10 | .SH "DESCRIPTION" 11 | .I kthresher 12 | is a utility to remove kernel packages that are marked as a candidate for autoremoval, such as those installed via unattended\-upgrades or meta packages. 13 | .PP 14 | By default the running kernel, the latest kernel and manual installations are marked to 'Never Auto Remove'. 15 | .IR "kthresher " "then will not attempt to remove those packages." 16 | .PP 17 | When no option is passed to kthresher the list of available kernels for autoremoval will be printed. 18 | .SH "OPTIONS" 19 | .TP 20 | .B "\-h, \-\-help" 21 | show a help message and exits 22 | .TP 23 | .BI "\-c " "FILE" ", \-\-config " "FILE" 24 | Config file, default is 25 | .I /etc/kthresher.conf 26 | .TP 27 | .B "\-d, \-\-dry\-run" 28 | List what packages would be purged but do not perform the action, by default 29 | does not perform a dry-run. 30 | .TP 31 | .B "\-H, \-\-headers" 32 | Include the search for kernel headers, by default does not include headers. 33 | .TP 34 | .BI "\-k " "N" ", \-\-keep " "N" 35 | Number of autoremovable kernels to keep, default is 1. 36 | .TP 37 | .B "\-p, \-\-purge" 38 | Purge unused kernels, by default does not purge packages. 39 | .TP 40 | .B "\-s, \-\-show\-autoremoval" 41 | Show kernel packages available for autoremoval. When defined, overrides any 42 | other action coming from the configuration file(s). This is the default action when 43 | no other action is defined. 44 | .TP 45 | .B "\-v, \-\-verbose" 46 | Be verbose, by default is not verbose. 47 | .TP 48 | .B "\-V, \-\-version" 49 | Print version 50 | .SH FILES 51 | .TP 52 | .B \fBMain Configuration\fP 53 | /etc/kthresher.conf 54 | .TP 55 | .B \fBOverriding configs\fP 56 | /etc/kthresher.d 57 | .SH "REPORTING BUGS" 58 | Bugs and issues to be submitted via github . 59 | .SH "AUTHOR" 60 | Written by Tony Garcia . 61 | .SH "SEE ALSO" 62 | Additional documentation to be found via github . 63 | .SH "LICENSE" 64 | This software is Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at . 65 | .PP 66 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [Unreleased] 5 | 6 | ## [1.4.1] - 2019-06-26 7 | ### Added 8 | - Only create a syslog handler if /dev/log exists. 9 | - Reference of unattended-updates capability to remove unused kernels. 10 | 11 | ### Changed 12 | - Use package section from `Version` instead of `Package`. 13 | 14 | ## [1.4.0] - 2019-02-13 15 | ### Added 16 | - Add system logs on purge, show and errors. 17 | - Python3 Support. 18 | - Add systemd timer units. 19 | 20 | ### Changed 21 | - Documented default values. 22 | - Make show autoremoval action the default. 23 | - Use `version_compare` from `apt` when `LooseVersion` fails. 24 | 25 | ### Fixed 26 | - Make `keep` truly default to 1. 27 | - Cron job should not fail if the binary does not exist. 28 | 29 | ### Removed 30 | - Setuptools does not include `data_files` with man page nor default config anymore. 31 | 32 | ## [1.3.1] - 2018-04-23 33 | ### Added 34 | - Removed shell from bash completion. 35 | - Added license to bash completion. 36 | 37 | ## [1.3.0] - 2018-01-30 38 | ### Added 39 | - Bash completion 40 | 41 | ### Changed 42 | - Support to most/all kernel/header packages [flavors used in Ubuntu](https://people.canonical.com/~kernel/info/kernel-version-pockets.txt) 43 | - Using logging consistently. 44 | - PEP8 compliant. 45 | 46 | ## [1.2.7] - 2017-03-09 47 | ### Added 48 | - Better error handling when executing as non-root. 49 | - Logos. 50 | 51 | ### Changed 52 | - Regex to include linux-headers ending in -common. 53 | - Improved the testing example on the README. 54 | 55 | ## [1.2.6] - 2017-01-30 56 | ### Added 57 | - Support to amd64 kernels. 58 | 59 | ### Changed 60 | - Consistency between `--show` and `--purge`. 61 | - Sytle improvements(pep8). 62 | - Man page update. 63 | 64 | ## [1.2.5] - 2016-11-15 65 | ### Added 66 | - Support for nested config files through `include` setting. 67 | - README info about how a package is marked for autoremoval. 68 | - README info to be able to perform tests by installing kernels and headers. 69 | 70 | ### Changed 71 | - Default config file to only include a `include` path for `/etc/kthresher.d/*.conf` 72 | 73 | ### Removed 74 | - Debian dir and drone configs, will not live now with the code, @thebwt will maintain that now. 75 | - Config file support for dry-run, this is now only available through command line arguments. 76 | 77 | ## [1.2.4] - 2016-09-02 78 | ### Added 79 | - Drone config. 80 | 81 | ### Changed 82 | - Debian configs for proper building. 83 | 84 | ### Fixed 85 | - Typos on man page. 86 | 87 | ## [1.2.3] - 2016-07-25 88 | ### Added 89 | - Man page. 90 | - Changelog. 91 | - Debian directory for .debs. 92 | 93 | ### Changed 94 | - Flatten directory structure for .deb. 95 | - Cron file to check if script is available prior execution. 96 | 97 | ## [1.2.2] - 2016-04-19 98 | ### Added 99 | - Cron file cron.daily. 100 | 101 | ## [1.2.1] - 2016-04-18 102 | ### Added 103 | - Support for old virtual kernel packages. 104 | 105 | ### Fixed 106 | - Bug when searching for a list of installed kernel images. 107 | 108 | ## [1.2.0] - 2016-04-18 109 | ### Added 110 | - Support to remove headers '-h'. 111 | 112 | ## [1.1.0] - 2016-04-14 113 | ### Added 114 | - Support for config file '-c'. 115 | 116 | ### Change 117 | - Use of '-n' or '--number' changed to '-k' or '--keep' for number of kernels to keep. 118 | 119 | ## [1.0.1] - 2016-04-11 120 | ### Added 121 | - LICENSE. 122 | 123 | ### Change 124 | - README to rst. 125 | 126 | ## [1.0.0] - 2016-04-06 127 | ### Added 128 | - Support to keep a fixed amount of kernels '-n'. 129 | - Support to '--dry-run'. 130 | 131 | ### Changed 132 | - Previously '-v' was used for version, it was changed to '-V'. 133 | - Use of '-v' or '--verbose' was changed to add verbosity. 134 | - Option '-l' or '--list' was changed to '-s' or '--show-autoremoval'. 135 | - The use of disutils.LooseVersion broke support for Python3. 136 | 137 | ### Deprecated 138 | - Use of '-l','-f'. 139 | 140 | ### Fixed 141 | - Typos. 142 | 143 | ## [0.2.3] - 2015-12-14 144 | ### Fixed 145 | - README. 146 | 147 | ## [0.2.2] - 2015-11-09 148 | ### Added 149 | - Licencing. 150 | 151 | ### Fixed 152 | - README. 153 | 154 | ## [0.2.1] - 2015-10-12 155 | ### Added 156 | - Released kthresher. 157 | 158 | --- 159 | 160 | # Contributors 161 | - [delag](https://github.com/delag) 162 | - [disengage00](https://github.com/disengage00) 163 | - [jamrok](https://github.com/jamrok) 164 | - [jkirk](https://github.com/jkirk) 165 | - [Jose R. Gonzalez](https://github.com/Komish) 166 | - [Tony G.](https://github.com/tonyskapunk) 167 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /kthresher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | 4 | 5 | # Copyright 2015 Tony Garcia 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | 19 | """Tool to Purge Unused Kernels 20 | Removes those kernel packages marked as candidate for autoremoval. 21 | Those packages are generally installed via Unattended upgrade or meta-packages 22 | By default the latest kernel and manual installations are marked to Never 23 | Auto Remove. 24 | 25 | Ubuntu has multiple suggestions on how to remove kernels: 26 | https://help.ubuntu.com/community/RemoveOldKernels 27 | 28 | A great recommendation is to make use of unattended-upgrades (u-u). Since 29 | version 1.0 u-u is capable of removing unused kernel packages. 30 | 31 | thresher - A device that first separates the head of a stalk of grain from 32 | the straw, and then further separates the kernel from the rest of the head. 33 | """ 34 | 35 | import re 36 | import sys 37 | import logging 38 | import argparse 39 | from glob import iglob 40 | from os import path 41 | from platform import uname 42 | from logging.handlers import SysLogHandler 43 | from distutils.version import LooseVersion 44 | 45 | try: 46 | import configparser 47 | except ImportError: 48 | import ConfigParser as configparser 49 | 50 | try: 51 | Parser = configparser.ConfigParser 52 | except AttributeError: 53 | Parser = configparser.SafeConfigParser 54 | 55 | try: 56 | import apt 57 | except ImportError: 58 | try: 59 | import distro 60 | except ImportError: 61 | from platform import dist 62 | 63 | DISTRO = dist()[0] 64 | else: 65 | DISTRO = distro.linux_distribution(False)[0] 66 | 67 | if DISTRO == "debian" or DISTRO == "Ubuntu": 68 | print( 69 | "Error: python apt library was not found\n" 70 | "python-apt and/or python3-apt packages provide it.", 71 | file=sys.stderr, 72 | ) 73 | else: 74 | print("Error: {0} distro not supported".format(DISTRO), file=sys.stderr) 75 | sys.exit(1) 76 | 77 | 78 | __version__ = "1.4.1" 79 | 80 | 81 | # Loggers 82 | logger = logging.getLogger("kthresher") 83 | 84 | 85 | def cmp_to_key(mycmp): 86 | """Convert a cmp= function into a key= function 87 | Wrapper to leverage the use of apt_pkg.version_compare. 88 | As documented in: 89 | https://docs.python.org/3/howto/sorting.html#the-old-way-using-the-cmp-parameter 90 | """ 91 | 92 | class K: 93 | def __init__(self, obj, *args): 94 | self.obj = obj 95 | 96 | def __lt__(self, other): 97 | return mycmp(self.obj, other.obj) < 0 98 | 99 | def __gt__(self, other): 100 | return mycmp(self.obj, other.obj) > 0 101 | 102 | def __eq__(self, other): 103 | return mycmp(self.obj, other.obj) == 0 104 | 105 | def __le__(self, other): 106 | return mycmp(self.obj, other.obj) <= 0 107 | 108 | def __ge__(self, other): 109 | return mycmp(self.obj, other.obj) >= 0 110 | 111 | def __ne__(self, other): 112 | return mycmp(self.obj, other.obj) != 0 113 | 114 | return K 115 | 116 | 117 | def get_configs(conf_file, section): 118 | """Obtains the configs from a file. 119 | Config file format: INI 120 | Valid sections: main 121 | Valid options: headers, include, keep, purge, verbose 122 | Example: 123 | [main] 124 | headers=(yes|on|true|no|off|false) 125 | include=/path/to/dir/ 126 | keep=[0-9] 127 | purge=(yes|on|true|no|off|false) 128 | verbose=(yes|on|true|no|off|false) 129 | """ 130 | valid_configs = { 131 | "headers": "boolean", 132 | "include": "str", 133 | "keep": "int", 134 | "purge": "boolean", 135 | "verbose": "boolean", 136 | } 137 | configs = {} 138 | def_conf = Parser() 139 | logger.info("Attempting to read {0}.".format(conf_file)) 140 | try: 141 | def_conf.read(conf_file) 142 | except configparser.ParsingError: 143 | logger.error("Config file contains errors: {0}".format(conf_file)) 144 | sys.exit(1) 145 | if not def_conf.read(conf_file): 146 | logger.info( 147 | "Config file {0} is empty or does not exist, ignoring.".format(conf_file) 148 | ) 149 | return configs 150 | if not def_conf.has_section(section): 151 | logger.info("Unable to find section [{0}].".format(section)) 152 | return configs 153 | if len(def_conf.options(section)) < 1: 154 | logger.info("No options found in section [{0}].".format(section)) 155 | return configs 156 | logger.info("Options found: {0}.".format(def_conf.options(section))) 157 | # Validation of the options found 158 | for option in def_conf.options(section): 159 | if option not in valid_configs.keys(): 160 | logger.info('Invalid setting "{0}", ignoring'.format(option)) 161 | else: 162 | logger.info('Valid setting found "{0}"'.format(option)) 163 | if valid_configs[option] == "int": 164 | try: 165 | configs[option] = def_conf.getint(section, option) 166 | except configparser.NoOptionError: 167 | logger.error('Unable to get value from "{0}".'.format(option)) 168 | sys.exit(1) 169 | except ValueError: 170 | logger.error( 171 | 'Invalid "{0}" value, an integer is required.'.format(option) 172 | ) 173 | sys.exit(1) 174 | if option == "keep": 175 | if configs[option] > 9: 176 | logger.error("keep value should be between 0-9.") 177 | sys.exit(1) 178 | elif valid_configs[option] == "boolean": 179 | try: 180 | configs[option] = def_conf.getboolean(section, option) 181 | except configparser.NoOptionError: 182 | logger.error('Unable to get value from "{0}".'.format(option)) 183 | sys.exit(1) 184 | except ValueError: 185 | logger.error( 186 | 'Invalid "{0}" value, a boolean is required.'.format(option) 187 | ) 188 | sys.exit(1) 189 | elif valid_configs[option] == "str": 190 | try: 191 | configs[option] = def_conf.get(section, option) 192 | except configparser.NoOptionError: 193 | logger.error('Unable to get value from "{0}".'.format(option)) 194 | logger.info("\t{0} = {1}".format(option, configs[option])) 195 | if "include" in configs.keys(): 196 | # Obtain the configs on each nested config file. 197 | for nested_file in sorted(iglob(configs["include"])): 198 | # Aborting if importing the same config file. 199 | # Won't prevent indirect loops. 200 | if nested_file == conf_file: 201 | logger.error("Looping config files, aborting...") 202 | sys.exit(1) 203 | nested_configs = get_configs(nested_file, section) 204 | # Override any option coming from the nested configs. 205 | for nested_config in nested_configs: 206 | configs[nested_config] = nested_configs[nested_config] 207 | return configs 208 | 209 | 210 | def show_autoremovable_pkgs(): 211 | """List all the kernel related packages available for autoremoval. 212 | """ 213 | packages = {} 214 | ver_max_len = 0 215 | try: 216 | apt_cache = apt.Cache() 217 | except SystemError: 218 | logger.error("Unable to obtain the cache!") 219 | sys.exit(1) 220 | for pkg_name in apt_cache.keys(): 221 | pkg = apt_cache[pkg_name] 222 | if (pkg.is_installed and pkg.is_auto_removable) and re.match( 223 | r"^linux-(image|(\w+-)?headers)-.*$", pkg_name 224 | ): 225 | packages[pkg_name] = pkg.installed.version 226 | if ver_max_len < len(pkg.installed.version): 227 | ver_max_len = len(pkg.installed.version) 228 | if packages: 229 | logger.info("List of kernel packages available for autoremoval:") 230 | logger.info( 231 | "{0:>{width}} {1:<{width}}".format( 232 | "Version", "Package", width=ver_max_len + 2 233 | ) 234 | ) 235 | for package in sorted(packages.keys()): 236 | logger.info( 237 | "{0:>{width}} {1:<{width}}".format( 238 | packages[package], package, width=ver_max_len + 2 239 | ) 240 | ) 241 | logger.log( 242 | 42, 243 | "kernel packages available for autoremoval: {0}".format( 244 | sorted(packages.keys()) 245 | ), 246 | ) 247 | else: 248 | logger.log(42, "No kernel packages available for autoremoval.") 249 | 250 | 251 | def kthreshing(purge=None, headers=None, keep=1): 252 | """Purge or list the unused kernels. 253 | By default keeps 1. 254 | The running kernel, the kernels marked as NeverAutoRemove and 255 | Manually installed kernels are nevertouched by kthresher. 256 | """ 257 | kernels = {} 258 | ver_max_len = 0 259 | kernel_image_regex = r"^linux-image-.*$" 260 | kernel_header_regex = r"^linux-(\w+-)?headers-.*$" 261 | try: 262 | apt_cache = apt.Cache() 263 | except SystemError: 264 | logger.error("Unable to obtain the cache!") 265 | sys.exit(1) 266 | current_kernel_ver = uname()[2] 267 | kernel_pkg = apt_cache["linux-image-%s" % current_kernel_ver] 268 | logger.info( 269 | "Running kernel is {0} v[{1}]".format( 270 | kernel_pkg.name, kernel_pkg.installed.version 271 | ) 272 | ) 273 | for pkg_name in apt_cache.keys(): 274 | pkg = apt_cache[pkg_name] 275 | section = pkg.candidate.section or '' 276 | if (pkg.is_installed and pkg.is_auto_removable) and ( 277 | "kernel" in section and re.match(kernel_image_regex, pkg_name) 278 | ): 279 | if ver_max_len < len(pkg.installed.version): 280 | ver_max_len = len(pkg.installed.version) 281 | kernels.setdefault(pkg.installed.version, []).append(pkg.name) 282 | if headers: 283 | for pkg_name in apt_cache.keys(): 284 | pkg = apt_cache[pkg_name] 285 | if (pkg.is_installed and pkg.is_auto_removable) and re.match( 286 | kernel_header_regex, pkg_name 287 | ): 288 | if pkg.installed.version in kernels.keys(): 289 | kernels[pkg.installed.version].append(pkg.name) 290 | else: 291 | kernels[pkg.installed.version] = [pkg.name] 292 | if kernels: 293 | logger.info("Attempting to keep {0} kernel package(s)".format(keep)) 294 | kernel_versions = list(kernels.copy().keys()) 295 | logger.info( 296 | "Found {0} kernel image(s) installed and available for " 297 | "autoremoval".format(len(kernel_versions)) 298 | ) 299 | logger.info("Pre-sorting: {0}".format(kernel_versions)) 300 | try: 301 | # Sadly this is broken in python3, https://bugs.python.org/issue14894 302 | sorted_kernel_list = sorted(kernel_versions, key=LooseVersion) 303 | except TypeError: 304 | # Using apt_pkg.version_compare 305 | # https://github.com/rackerlabs/kthresher/pull/61 306 | sorted_kernel_list = sorted( 307 | kernel_versions, key=cmp_to_key(apt.apt_pkg.version_compare) 308 | ) 309 | 310 | logger.info("Post-sorting: {0}".format(sorted_kernel_list)) 311 | if keep >= len(kernel_versions): 312 | logger.log( 313 | 42, 314 | "Nothing to do, attempting to keep {0} out of {1} " 315 | "kernel images.".format(keep, len(kernel_versions)), 316 | ) 317 | sys.exit(0) 318 | else: 319 | purged_pkgs = [] 320 | for index in range(0, len(sorted_kernel_list) - keep): 321 | kernel_version = sorted_kernel_list[index] 322 | logger.info( 323 | "\tPurging packages from version: {0}".format(kernel_version) 324 | ) 325 | for pkg_name in kernels[kernel_version]: 326 | logger.info("\t\tPurging: {0}".format(pkg_name)) 327 | if purge: 328 | pkg = apt_cache[pkg_name] 329 | pkg.mark_delete(purge=True) 330 | purged_pkgs.append(pkg_name) 331 | if purge: 332 | try: 333 | apt_cache.commit( 334 | fetch_progress=apt.progress.text.AcquireProgress(), 335 | install_progress=apt.progress.base.InstallProgress(), 336 | ) 337 | except apt.cache.LockFailedException as lfe: 338 | logger.error("{}, are you root?".format(lfe)) 339 | sys.exit(1) 340 | except SystemError: 341 | logger.error("Unable to commit the changes") 342 | sys.exit(1) 343 | logger.log( 344 | 42, 345 | "kernel packages purged: {} - {}".format( 346 | len(purged_pkgs), purged_pkgs 347 | ), 348 | ) 349 | else: 350 | logger.info("No packages available for autoremoval.") 351 | 352 | 353 | def main(): 354 | """The main function. 355 | """ 356 | defaults = { 357 | "config": {"file": "/etc/kthresher.conf", "section": "main"}, 358 | "options": { 359 | "dry_run": False, 360 | "headers": False, 361 | "keep": 1, 362 | "purge": False, 363 | "verbose": False, 364 | }, 365 | } 366 | options = defaults["options"].copy() 367 | conf_options = {} 368 | parser = argparse.ArgumentParser( 369 | description="Purge Unused Kernels.", prog="kthresher" 370 | ) 371 | parser.add_argument( 372 | "-c", 373 | "--config", 374 | type=str, 375 | metavar="FILE", 376 | help="Config file, default is /etc/kthresher.conf", 377 | ) 378 | parser.add_argument( 379 | "-d", 380 | "--dry-run", 381 | action="store_true", 382 | help="List unused kernel images available to purge" 383 | " (dry run). Is always verbose.", 384 | ) 385 | parser.add_argument( 386 | "-H", 387 | "--headers", 388 | action="store_true", 389 | help="Include the search for kernel headers.", 390 | ) 391 | parser.add_argument( 392 | "-k", 393 | "--keep", 394 | nargs="?", 395 | type=int, 396 | const=1, 397 | metavar="N", 398 | choices=range(0, 10), 399 | help="Number of kernels to keep, default 1.", 400 | ) 401 | parser.add_argument( 402 | "-p", "--purge", help="Purge Unused Kernels.", action="store_true" 403 | ) 404 | parser.add_argument( 405 | "-s", 406 | "--show-autoremoval", 407 | action="store_true", 408 | help="Show kernel packages available for autoremoval. This is the " 409 | "default action", 410 | ) 411 | parser.add_argument("-v", "--verbose", action="store_true", help="Be verbose.") 412 | parser.add_argument( 413 | "-V", 414 | "--version", 415 | action="version", 416 | version="%(prog)s v{0}".format(__version__), 417 | help="Print version.", 418 | ) 419 | args = parser.parse_args() 420 | 421 | # Default logger 422 | logger.setLevel(logging.ERROR) 423 | 424 | # Configure console handler 425 | ch = logging.StreamHandler() 426 | cf = logging.Formatter("%(levelname)s: %(message)s") 427 | ch.setFormatter(cf) 428 | logger.addHandler(ch) 429 | 430 | # Console logging override 431 | if args.verbose or args.dry_run: 432 | logger.setLevel(logging.INFO) 433 | 434 | # Configure syslog handler 435 | if path.exists("/dev/log"): 436 | sh = SysLogHandler("/dev/log") 437 | sf = logging.Formatter("%(name)s[%(process)d]: %(message)s") 438 | sh.setLevel(logging.ERROR) 439 | sh.setFormatter(sf) 440 | logger.addHandler(sh) 441 | 442 | # Create new logging level to be used on syslog 443 | logging.addLevelName(42, "INFO") 444 | 445 | # Read config files 446 | if args.config: 447 | conf_options = get_configs(args.config, defaults["config"]["section"]) 448 | else: 449 | conf_options = get_configs( 450 | defaults["config"]["file"], defaults["config"]["section"] 451 | ) 452 | # Overriding options as follows: 453 | # defaults -> default config file or custom config file -> included config 454 | # -> cli arguments 455 | # First overriding default configs from a file if available: 456 | if conf_options: 457 | options.update(conf_options) 458 | # Override the verbosity if set through configuration 459 | if options["verbose"]: 460 | logger.setLevel(logging.INFO) 461 | # Now overriding the result options with cli arguments 462 | if args.dry_run: 463 | options["dry_run"] = args.dry_run 464 | if args.headers: 465 | options["headers"] = args.headers 466 | if args.keep is not None: 467 | options["keep"] = args.keep 468 | if args.purge: 469 | options["purge"] = args.purge 470 | if args.verbose: 471 | options["verbose"] = args.verbose 472 | logger.info("Options: {0}".format(options)) 473 | # Show auto-removable, this is only available via explicit argument 474 | # Overrides actions defined in the configuration file(s). 475 | if args.show_autoremoval: 476 | logger.setLevel(logging.INFO) 477 | show_autoremovable_pkgs() 478 | sys.exit(0) 479 | if options["dry_run"]: 480 | logger.info("----- DRY RUN -----") 481 | kthreshing(purge=False, headers=options["headers"], keep=options["keep"]) 482 | sys.exit(0) 483 | if options["purge"]: 484 | kthreshing(purge=True, headers=options["headers"], keep=options["keep"]) 485 | sys.exit(0) 486 | if not sys.stdout.isatty(): 487 | sys.exit(0) 488 | else: 489 | # Show auto-remove is also a default option if no other action is 490 | # defined. 491 | logger.setLevel(logging.INFO) 492 | show_autoremovable_pkgs() 493 | sys.exit(0) 494 | 495 | 496 | if __name__ == "__main__": 497 | main() 498 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |logo0| 2 | 3 | kthresher 4 | ========= 5 | 6 | .. image:: https://img.shields.io/github/release/rackerlabs/kthresher.svg 7 | :target: https://github.com/rackerlabs/kthresher/releases/latest 8 | :alt: Github release 9 | .. image:: https://img.shields.io/travis/rackerlabs/kthresher/master.svg?logo=travis&label=master 10 | :target: https://travis-ci.org/rackerlabs/kthresher 11 | :alt: Build Master Status 12 | .. image:: https://img.shields.io/travis/rackerlabs/kthresher/master.svg?logo=travis&label=development 13 | :target: https://travis-ci.org/rackerlabs/kthresher 14 | :alt: Build Development Status 15 | .. image:: https://img.shields.io/github/license/rackerlabs/kthresher.svg 16 | :target: https://raw.githubusercontent.com/rackerlabs/kthresher/master/LICENSE 17 | :alt: License 18 | .. image:: https://img.shields.io/twitter/url/https/github.com/rackerlabs/kthresher.svg?style=social 19 | :target: https://twitter.com/intent/tweet?text=Check%20this%20out:&url=https%3A%2F%2Fgithub.com%2Frackerlabs%2Fkthresher 20 | :alt: Twitter 21 | 22 | Tool to remove unused kernels that were installed automatically in Debian/Ubuntu. 23 | 24 | This tool removes those kernel packages marked as candidate for autoremoval. Those packages are generally installed via Unattended upgrade or meta-packages. 25 | 26 | By default, on apt 1.0 and below, the booted kernel, the latest-installed kernel and the latest kernel are set to "NeverAutoRemove". Or, for apt 1.2 and above, the booted kernel, the latest-installed kernel, the latest kernel and the second-latest kernel are set to "NeverAutoRemove". 27 | 28 | The official documentation has multiple suggestions on how to remove kernels: 29 | https://help.ubuntu.com/community/RemoveOldKernels 30 | 31 | A great recommendation is to make use of unattended-upgrades `u-u` (`debian `__, `ubuntu `__). Since version **1.0** (`debian `__, `ubuntu `__) *u-u* removes unused kernel packages by default (*Remove-Unused-Kernel-Packages*). 32 | Some distribution versions may not make reference or may comment out that setting in its configuration, but unless it is explicitly disabled, *u-u* will attempt to remove unused kernels. `More info here `__. 33 | 34 | For scenarios where *u-u* is not available or older than **1.0**, `kthresher` is still a good option. 35 | 36 | 37 | *thresher - A device that first separates the head of a stalk of grain from the straw, and then further separates the kernel from the rest of the head.* 38 | 39 | ----- 40 | 41 | |version| |downloads-pypi| |versions| |license| |stars-github| 42 | 43 | ----- 44 | 45 | .. contents:: Table of Contents 46 | :depth: 1 47 | :backlinks: none 48 | 49 | ----- 50 | 51 | How a package is marked for autoremoval? 52 | ---------------------------------------- 53 | 54 | Whenever a package is auto-installed and there is no other dependency for it, the package is marked as a candidate for autoremoval, there is an exception if the *APT* configuration does have the package marked as "NeverAutoRemove". 55 | 56 | 57 | How the kernel image is added into the "APT::NeverAutoRemove::" config? 58 | ----------------------------------------------------------------------- 59 | 60 | When a kernel image is installed the *postinstall* script will issue the *run-parts* on */etc/kernel/postinst.d/* and */etc/kernel/postinst.d/${version}* if any exist. The *run-parts* script will run each one of the scripts located in that directory, e.g. 61 | 62 | .. code-block:: bash 63 | 64 | # ls -1 /etc/kernel/postinst.d/ 65 | apt-auto-removal 66 | initramfs-tools 67 | update-notifier 68 | x-grub-legacy-ec2 69 | zz-update-grub 70 | 71 | All the scripts found by *run-parts* are executed on post install of the kernel package and the output of apt-get install/upgrade/dist-upgrade will show them, e.g. 72 | 73 | .. code-block:: bash 74 | 75 | run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 3.13.0-96-generic /boot/vmlinuz-3.13.0-96-generic 76 | run-parts: executing /etc/kernel/postinst.d/initramfs-tools 3.13.0-96-generic /boot/vmlinuz-3.13.0-96-generic 77 | run-parts: executing /etc/kernel/postinst.d/update-notifier 3.13.0-96-generic /boot/vmlinuz-3.13.0-96-generic 78 | run-parts: executing /etc/kernel/postinst.d/x-grub-legacy-ec2 3.13.0-96-generic /boot/vmlinuz-3.13.0-96-generic 79 | run-parts: executing /etc/kernel/postinst.d/zz-update-grub 3.13.0-96-generic /boot/vmlinuz-3.13.0-96-generic 80 | 81 | The first script *"apt-auto-removal"* takes care of adding a configuration in /etc/apt/apt.conf.d/01autoremove-kernels this script generates that list based on the logic described above, it means that the NeverAutoRemove may have anything between two to three kernels listed. 82 | 83 | Supported Operating Systems 84 | --------------------------- 85 | 86 | * Debian (Tested on Version(s)) 87 | * `8 `__ 88 | * `9 `__ 89 | * Ubuntu (Tested on Version(s)) 90 | * `12.04 `__ 91 | * `14.04 `__ 92 | * `16.04 `__ 93 | * `17.10 `__ 94 | 95 | 96 | Installation 97 | ------------ 98 | script 99 | ~~~~~~ 100 | 101 | .. code-block:: bash 102 | 103 | wget -O kthresher https://raw.githubusercontent.com/rackerlabs/kthresher/master/kthresher.py 104 | chmod u+x kthresher 105 | 106 | pip 107 | ~~~ 108 | 109 | .. code-block:: bash 110 | 111 | pip install kthresher 112 | 113 | or 114 | 115 | .. code-block:: bash 116 | 117 | pip install git+https://github.com/rackerlabs/kthresher.git 118 | 119 | Github 120 | ~~~~~~ 121 | 122 | .. code-block:: bash 123 | 124 | git clone https://github.com/rackerlabs/kthresher.git 125 | cd kthresher && python setup.py install 126 | 127 | 128 | Usage 129 | ----- 130 | 131 | .. code-block:: 132 | 133 | $ kthresher -h 134 | usage: kthresher [-h] [-c FILE] [-d] [-H] [-k [N]] [-p] [-s] [-v] [-V] 135 | 136 | Purge Unused Kernels. 137 | 138 | optional arguments: 139 | -h, --help show this help message and exit 140 | -c FILE, --config FILE 141 | Config file, default is /etc/kthresher.conf 142 | -d, --dry-run List unused kernel images available to purge(dry run). 143 | Is always verbose. 144 | -H, --headers Include the search for kernel headers. 145 | -k [N], --keep [N] Number of kernels to keep, default 1. 146 | -p, --purge Purge Unused Kernels. 147 | -s, --show-autoremoval 148 | Show kernel packages available for autoremoval. 149 | -v, --verbose Be verbose. 150 | -V, --version Print version. 151 | 152 | 153 | Examples 154 | -------- 155 | 156 | List which kernel images and its dependencies would remove(dry run) 157 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 158 | .. code-block:: 159 | 160 | # kthresher -d 161 | INFO: Attempting to read /etc/kthresher.conf. 162 | INFO: Config file /etc/kthresher.conf is empty or does not exist, ignoring. 163 | INFO: Options: {'purge': False, 'verbose': False, 'dry_run': True, 'keep': 1} 164 | INFO: ----- DRY RUN ----- 165 | INFO: Running kernel is linux-image-3.13.0-83-generic v[3.13.0-83.127] 166 | INFO: Attempting to keep 1 kernel package(s) 167 | INFO: Found 4 kernel image(s) installed and available for autoremoval 168 | INFO: Pre-sorting: ['3.16.0-60.80~14.04.1', '3.13.0-77.121', '3.13.0-63.103', '3.16.0-33.44~14.04.1'] 169 | INFO: Post-sorting: ['3.13.0-63.103', '3.13.0-77.121', '3.16.0-33.44~14.04.1', '3.16.0-60.80~14.04.1'] 170 | INFO: Purging packages from version: 3.13.0-63.103 171 | INFO: Purging: linux-image-extra-3.13.0-63-generic 172 | INFO: Purging: linux-image-3.13.0-63-generic 173 | INFO: Purging packages from version: 3.13.0-77.121 174 | INFO: Purging: linux-image-3.13.0-77-generic 175 | INFO: Purging: linux-image-extra-3.13.0-77-generic 176 | INFO: Purging packages from version: 3.16.0-33.44~14.04.1 177 | INFO: Purging: linux-image-3.16.0-33-generic 178 | 179 | 180 | Show all kernel packages available for autoremoval 181 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 182 | .. code-block:: 183 | 184 | # kthresher -s 185 | List of kernel packages available for autoremoval: 186 | Version Package 187 | 3.13.0.83.89 linux-generic 188 | 3.13.0-51.84 linux-headers-3.13.0-51 189 | 3.13.0-51.84 linux-headers-3.13.0-51-generic 190 | 3.13.0-71.114 linux-headers-3.13.0-71 191 | 3.13.0-71.114 linux-headers-3.13.0-71-generic 192 | 3.13.0-77.121 linux-headers-3.13.0-77 193 | 3.13.0-77.121 linux-headers-3.13.0-77-generic 194 | 3.13.0-79.123 linux-headers-3.13.0-79 195 | 3.13.0-79.123 linux-headers-3.13.0-79-generic 196 | 3.13.0-63.103 linux-image-3.13.0-63-generic 197 | 3.13.0-77.121 linux-image-3.13.0-77-generic 198 | 3.16.0-33.44~14.04.1 linux-image-3.16.0-33-generic 199 | 3.16.0-60.80~14.04.1 linux-image-3.16.0-60-generic 200 | 3.13.0-63.103 linux-image-extra-3.13.0-63-generic 201 | 3.13.0-77.121 linux-image-extra-3.13.0-77-generic 202 | 3.13.0.83.89 linux-image-generic 203 | 204 | 205 | Purge Unused Kernels, keep 3 kernels and be verbose 206 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 207 | .. code-block:: 208 | 209 | # kthresher -p -k3 -v 210 | INFO: Attempting to read /etc/kthresher.conf. 211 | INFO: Config file /etc/kthresher.conf is empty or does not exist, ignoring. 212 | INFO: Options: {'purge': True, 'verbose': True, 'dry_run': False, 'keep': 3} 213 | INFO: Running kernel is linux-image-3.13.0-83-generic v[3.13.0-83.127] 214 | INFO: Attempting to keep 3 kernel package(s) 215 | INFO: Found 4 kernel image(s) installed and available for autoremoval 216 | INFO: Pre-sorting: ['3.16.0-60.80~14.04.1', '3.13.0-77.121', '3.13.0-63.103', '3.16.0-33.44~14.04.1'] 217 | INFO: Post-sorting: ['3.13.0-63.103', '3.13.0-77.121', '3.16.0-33.44~14.04.1', '3.16.0-60.80~14.04.1'] 218 | INFO: Purging packages from version: 3.13.0-63.103 219 | INFO: Purging: linux-image-extra-3.13.0-63-generic 220 | INFO: Purging: linux-image-3.13.0-63-generic 221 | Fetched 0 B in 0s (0 B/s) 222 | (Reading database ... 169514 files and directories currently installed.) 223 | Removing linux-image-extra-3.13.0-63-generic (3.13.0-63.103) ... 224 | run-parts: executing /etc/kernel/postinst.d/apt-auto-removal 3.13.0-63-generic /boot/vmlinuz-3.13.0-63-generic 225 | run-parts: executing /etc/kernel/postinst.d/initramfs-tools 3.13.0-63-generic /boot/vmlinuz-3.13.0-63-generic 226 | update-initramfs: Generating /boot/initrd.img-3.13.0-63-generic 227 | run-parts: executing /etc/kernel/postinst.d/zz-update-grub 3.13.0-63-generic /boot/vmlinuz-3.13.0-63-generic 228 | Generating grub configuration file ... 229 | Found linux image: /boot/vmlinuz-3.16.0-60-generic 230 | Found initrd image: /boot/initrd.img-3.16.0-60-generic 231 | Found linux image: /boot/vmlinuz-3.16.0-33-generic 232 | Found initrd image: /boot/initrd.img-3.16.0-33-generic 233 | Found linux image: /boot/vmlinuz-3.13.0-83-generic 234 | Found initrd image: /boot/initrd.img-3.13.0-83-generic 235 | Found linux image: /boot/vmlinuz-3.13.0-77-generic 236 | Found initrd image: /boot/initrd.img-3.13.0-77-generic 237 | Found linux image: /boot/vmlinuz-3.13.0-63-generic 238 | Found initrd image: /boot/initrd.img-3.13.0-63-generic 239 | done 240 | Purging configuration files for linux-image-extra-3.13.0-63-generic (3.13.0-63.103) ... 241 | Removing linux-image-3.13.0-63-generic (3.13.0-63.103) ... 242 | Examining /etc/kernel/postrm.d . 243 | run-parts: executing /etc/kernel/postrm.d/initramfs-tools 3.13.0-63-generic /boot/vmlinuz-3.13.0-63-generic 244 | update-initramfs: Deleting /boot/initrd.img-3.13.0-63-generic 245 | run-parts: executing /etc/kernel/postrm.d/zz-update-grub 3.13.0-63-generic /boot/vmlinuz-3.13.0-63-generic 246 | Generating grub configuration file ... 247 | Found linux image: /boot/vmlinuz-3.16.0-60-generic 248 | Found initrd image: /boot/initrd.img-3.16.0-60-generic 249 | Found linux image: /boot/vmlinuz-3.16.0-33-generic 250 | Found initrd image: /boot/initrd.img-3.16.0-33-generic 251 | Found linux image: /boot/vmlinuz-3.13.0-83-generic 252 | Found initrd image: /boot/initrd.img-3.13.0-83-generic 253 | Found linux image: /boot/vmlinuz-3.13.0-77-generic 254 | Found initrd image: /boot/initrd.img-3.13.0-77-generic 255 | done 256 | Purging configuration files for linux-image-3.13.0-63-generic (3.13.0-63.103) ... 257 | Examining /etc/kernel/postrm.d . 258 | run-parts: executing /etc/kernel/postrm.d/initramfs-tools 3.13.0-63-generic /boot/vmlinuz-3.13.0-63-generic 259 | run-parts: executing /etc/kernel/postrm.d/zz-update-grub 3.13.0-63-generic /boot/vmlinuz-3.13.0-63-generic 260 | 261 | 262 | Verbose run using a non-default config file 263 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 264 | 265 | .. code-block:: 266 | 267 | # kthresher -c myconf.conf 268 | INFO: Attempting to read myconf.conf. 269 | INFO: Options found: ['keep', 'dry_run']. 270 | INFO: Valid setting found "keep" 271 | INFO: keep = 1 272 | INFO: Valid setting found "dry_run" 273 | INFO: dry_run = True 274 | INFO: Options: {'purge': False, 'verbose': True, 'dry_run': True, 'keep': 1} 275 | INFO: ----- DRY RUN ----- 276 | INFO: Running kernel is linux-image-3.13.0-83-generic v[3.13.0-83.127] 277 | INFO: Attempting to keep 1 kernel package(s) 278 | INFO: Found 2 kernel image(s) installed and available for autoremoval 279 | INFO: Pre-sorting: ['3.16.0-60.80~14.04.1', '3.16.0-33.44~14.04.1'] 280 | INFO: Post-sorting: ['3.16.0-33.44~14.04.1', '3.16.0-60.80~14.04.1'] 281 | INFO: Purging packages from version: 3.16.0-33.44~14.04.1 282 | INFO: Purging: linux-image-3.16.0-33-generic 283 | 284 | Content of myconf.conf is: 285 | .. code-block:: 286 | 287 | [main] 288 | keep = 1 289 | dry_run = yes 290 | #purge = yes 291 | 292 | 293 | Dry run including headers 294 | ~~~~~~~~~~~~~~~~~~~~~~~~~ 295 | 296 | .. code-block:: 297 | 298 | # kthresher -v -d -H 299 | INFO: Attempting to read /etc/kthresher.conf. 300 | INFO: Options found: ['keep', 'dry_run', 'purge', 'verbose']. 301 | INFO: Valid setting found "keep" 302 | INFO: keep = 2 303 | INFO: Valid setting found "dry_run" 304 | INFO: dry_run = False 305 | INFO: Valid setting found "purge" 306 | INFO: purge = True 307 | INFO: Valid setting found "verbose" 308 | INFO: verbose = True 309 | INFO: Options: {'verbose': True, 'dry_run': True, 'keep': 2, 'purge': True, 'headers': True} 310 | INFO: ----- DRY RUN ----- 311 | INFO: Running kernel is linux-image-3.13.0-83-generic v[3.13.0-83.127] 312 | INFO: Attempting to keep 2 kernel package(s) 313 | INFO: Found 4 kernel image(s) installed and available for autoremoval 314 | INFO: Pre-sorting: ['3.16.0-60.80~14.04.1', '3.16.0-33.44~14.04.1', '3.13.0-85.129', '3.13.0-79.123'] 315 | INFO: Post-sorting: ['3.13.0-79.123', '3.13.0-85.129', '3.16.0-33.44~14.04.1', '3.16.0-60.80~14.04.1'] 316 | INFO: Purging packages from version: 3.13.0-79.123 317 | INFO: Purging: linux-image-3.13.0-79-generic 318 | INFO: Purging: linux-headers-3.13.0-79-generic 319 | INFO: Purging: linux-headers-3.13.0-79 320 | INFO: Purging packages from version: 3.13.0-85.129 321 | INFO: Purging: linux-image-3.13.0-85-generic 322 | INFO: Purging: linux-headers-3.13.0-85 323 | INFO: Purging: linux-headers-3.13.0-85-generic 324 | 325 | 326 | Testing 327 | ------- 328 | 329 | The below code can be used to install up to a fixed amount of kernels and headers if available of the form "linux-(image|headers)-[0-9].*-(generic|amd64)" at the end it should end up with two or three kernels in the NeverAutoRemove list, including the latest, the prior to latest and the running kernel. 330 | 331 | .. code-block:: python 332 | 333 | #!/usr/bin/env python 334 | '''Installs available linux-image-* and linux-headers-* 335 | And set them for autoremoval, so kthresher can be used for testing. 336 | ''' 337 | 338 | import re 339 | import apt 340 | import sys 341 | from platform import uname 342 | 343 | def autorm_install(pkgs): 344 | '''Install a list of packages and set them autoremovable. 345 | ''' 346 | latest_kernel = '' 347 | ac = apt.Cache() 348 | for pkg in pkgs: 349 | latest_kernel = pkg 350 | k = ac[pkg] 351 | if not k.is_installed: 352 | k.mark_install(from_user=False) 353 | try: 354 | ac.commit(install_progress=None) 355 | except apt.cache.LockFailedException as lfe: 356 | print('{}, are you root?'.format(lfe)) 357 | sys.exit(1) 358 | except SystemError: 359 | print('Something failed') 360 | sys.exit(1) 361 | 362 | def get_pkg(regex): 363 | '''Get a list of packages available that match the regex. 364 | ''' 365 | pkgs = [] 366 | ac = apt.Cache() 367 | ac.update() 368 | for pkg in ac: 369 | if re.match(regex, pkg.name): 370 | # ignore running kernel 371 | if pkg.name == 'linux-image-{0}'.format(uname()[2]): 372 | continue 373 | pkgs.append(pkg.name) 374 | return pkgs 375 | 376 | def main(): 377 | limit = 5 378 | if len(sys.argv) > 1: 379 | try: 380 | limit = int(sys.argv[1]) 381 | except: 382 | print("Use an integer as the limit of pkgs to install.") 383 | sys.exit(1) 384 | print("Installing {} kernels/headers if available...".format(limit)) 385 | kernel_regex = "^linux-image-\d\..*-(generic|amd64)$" 386 | header_regex = "^linux-headers-\d\..*-(generic|amd64)$" 387 | kernels = get_pkg(kernel_regex) 388 | headers = get_pkg(header_regex) 389 | pkgs = kernels[0:limit] + headers[0:limit] 390 | print("Installing {} packages total\n\tkernels: {}\n\theaders: {}" 391 | .format(len(pkgs), kernels[0:limit], headers[0:limit])) 392 | autorm_install(pkgs) 393 | 394 | if __name__ == "__main__": 395 | main() 396 | 397 | 398 | 399 | Bugs 400 | ---- 401 | 402 | Submit Bug reports, feature requests via `issues `__. 403 | 404 | Logos 405 | ----- 406 | 407 | The art was created by `Carlos Garcia `__ and released under CC BY-SA 4.0 408 | 409 | +---------+---------+ 410 | | |logo0| | |logo1| | 411 | +---------+---------+ 412 | | |logo2| | |logo3| | 413 | +---------+---------+ 414 | 415 | .. image:: https://i.creativecommons.org/l/by-sa/4.0/88x31.png 416 | :target: http://creativecommons.org/licenses/by-sa/4.0/ 417 | :alt: Creative Commons License 418 | 419 | ----- 420 | 421 | .. |version| image:: https://img.shields.io/pypi/v/kthresher.svg 422 | :target: https://github.com/rackerlabs/kthresher/releases/latest 423 | :alt: Latest Version 424 | .. |downloads-pypi| image:: https://img.shields.io/pypi/dm/kthresher.svg 425 | :target: https://pypi.python.org/pypi/kthresher 426 | :alt: PyPi Downloads 427 | .. |stars-github| image:: https://img.shields.io/github/stars/rackerlabs/kthresher.svg 428 | :target: https://github.com/rackerlabs/kthresher 429 | :alt: Github Stars 430 | .. |versions| image:: https://img.shields.io/pypi/pyversions/kthresher.svg 431 | :target: https://github.com/rackerlabs/kthresher/releases 432 | :alt: Versions 433 | .. |license| image:: https://img.shields.io/pypi/l/kthresher.svg 434 | :target: https://github.com/rackerlabs/kthresher/blob/master/LICENSE 435 | :alt: License 436 | 437 | .. |logo0| image:: https://github.com/rackerlabs/kthresher/wiki/img/kthresher.png 438 | 439 | .. |logo1| image:: https://github.com/rackerlabs/kthresher/wiki/img/kthresher_horiz.png 440 | 441 | .. |logo2| image:: https://github.com/rackerlabs/kthresher/wiki/img/kthresher_circ.png 442 | 443 | .. |logo3| image:: https://github.com/rackerlabs/kthresher/wiki/img/kthresher_half.png 444 | --------------------------------------------------------------------------------