Last active
January 24, 2023 04:16
-
-
Save jwodder/19317d3e4b9a58f2355e7643040d483a to your computer and use it in GitHub Desktop.
Display a table of available APT package updates
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python3 | |
""" | |
This script lists all APT package updates currently available for your system | |
along with the version numbers of the old & new packages. It is derived from | |
``/usr/lib/update-notifier/apt-check`` in the ``update-notifier-common`` | |
package on Ubuntu 14.04 (Trusty Tahr) and is made available under the same | |
license (the GNU GPL v2). | |
This script is know to work on Ubuntu Trusty and Xenial, and it should work on | |
any recent version of Ubuntu with the ``python3-apt`` package installed. | |
Options: | |
- ``--csv`` - output table as CSV | |
- ``--plain`` - output a plain table (default) | |
- ``--pretty`` - output a pretty table (requires the ``python3-prettytable`` | |
package) | |
Output columns: | |
- package name | |
- installed version | |
- update candidate version | |
- 'S' if the update is a security update, otherwise '-' (or empty in CSV) | |
- 'P' if the update is a `phased update | |
<https://wiki.ubuntu.com/PhasedUpdates>`_, otherwise '-' (or empty in CSV) | |
""" | |
__author__ = 'John Thorvald Wodder II' | |
__author_email__ = '[email protected]' | |
import argparse | |
from collections import namedtuple | |
import csv | |
import re | |
import sys | |
import apt | |
import apt_pkg | |
class Upgrade(namedtuple('Upgrade', 'package installed candidate security phased')): | |
@property | |
def flags(self): | |
return ('S' if self.security else '-') + ('P' if self.phased else '-') | |
class AptChecker: | |
def __init__(self): | |
self.distro = None | |
with open('/etc/os-release') as fp: | |
for line in fp: | |
m = re.fullmatch(r'UBUNTU_CODENAME=(\w+)', line.strip()) | |
if m: | |
self.distro = m.group(1) | |
if self.distro is None: | |
raise RuntimeError('Could not determine Ubuntu version codename') | |
self.security_pockets = [ | |
("Ubuntu", self.distro + "-security"), | |
("gNewSense", self.distro + "-security"), | |
("Debian", self.distro + "-updates"), | |
] | |
def isSecurityUpgrade(self, ver): | |
""" Check if the given version is a security update (or masks one) """ | |
return any((f.origin, f.archive) in self.security_pockets | |
for f,_ in ver.file_list) | |
def apt_check(self): | |
apt_pkg.init() | |
cache = apt_pkg.Cache(apt.progress.base.OpProgress()) | |
depcache = apt_pkg.DepCache(cache) | |
if depcache.broken_count > 0: | |
raise SystemExit("Error: BrokenCount > 0") | |
try: | |
from UpdateManager.Core.UpdateList import UpdateList | |
ul = UpdateList(None) | |
except ImportError: | |
ul = None | |
# This mimics an upgrade but will never remove anything | |
depcache.upgrade(True) | |
if depcache.del_count > 0: | |
# Unmark (clean) all changes from the given depcache | |
depcache.init() | |
depcache.upgrade() | |
with apt.Cache() as aptcache: | |
for pkg in cache.packages: | |
if not depcache.marked_install(pkg) and \ | |
not depcache.marked_upgrade(pkg): | |
continue | |
inst_ver = pkg.current_ver | |
cand_ver = depcache.get_candidate_ver(pkg) | |
if cand_ver == inst_ver: | |
continue | |
security = False | |
phased = False | |
if self.isSecurityUpgrade(cand_ver): | |
security = True | |
elif inst_ver: | |
# Check for security updates that are masked by a candidate | |
# version from another repo (-proposed or -updates) | |
for ver in pkg.version_list: | |
if apt_pkg.version_compare( | |
ver.ver_str, inst_ver.ver_str | |
) > 0 and self.isSecurityUpgrade(ver): | |
security = True | |
break | |
if ul is not None and \ | |
ul._is_ignored_phased_update(aptcache[pkg.name]): | |
phased = True | |
yield Upgrade( | |
package=pkg.name, | |
installed=inst_ver.ver_str if inst_ver else None, | |
candidate=cand_ver.ver_str, | |
security=security, | |
phased=phased, | |
) | |
def main(): | |
parser = argparse.ArgumentParser( | |
description='List available APT package updates' | |
) | |
parser.add_argument( | |
'--csv', | |
action = 'store_const', | |
dest = 'format', | |
const = 'csv', | |
help = 'Output table as comma-separated values', | |
) | |
parser.add_argument( | |
'--plain', | |
action = 'store_const', | |
dest = 'format', | |
const = 'plain', | |
help = 'Output a plain table (default)', | |
) | |
parser.add_argument( | |
'--pretty', | |
action = 'store_const', | |
dest = 'format', | |
const = 'pretty', | |
help = 'Output a pretty table', | |
) | |
args = parser.parse_args() | |
updates = AptChecker().apt_check() | |
if args.format is None or args.format == 'plain': | |
for upd in updates: | |
print('{0.package:20} {0.installed!s:20} {0.candidate:20} {0.flags}' | |
.format(upd)) | |
elif args.format == 'pretty': | |
try: | |
from prettytable import PrettyTable | |
except ImportError: | |
print('--pretty format requires prettytable', file=sys.stderr) | |
print('Install with `sudo apt-get install python3-prettytable', | |
file=sys.stderr) | |
sys.exit(1) | |
tbl = PrettyTable(['Package', 'Installed', 'Candidate', 'SP']) | |
tbl.align = 'l' | |
for upd in updates: | |
tbl.add_row([upd.package, upd.installed, upd.candidate, upd.flags]) | |
print(tbl.get_string(sortby='Package')) | |
elif args.format == 'csv': | |
out = csv.writer(sys.stdout) | |
out.writerow(['package','installed','candidate','security','phased']) | |
for upd in updates: | |
out.writerow([ | |
upd.package, | |
upd.installed, | |
upd.candidate, | |
'S' if upd.security else '', | |
'P' if upd.phased else '', | |
]) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment