Skip to content

Instantly share code, notes, and snippets.

@jwodder
Last active January 24, 2023 04:16
Show Gist options
  • Save jwodder/19317d3e4b9a58f2355e7643040d483a to your computer and use it in GitHub Desktop.
Save jwodder/19317d3e4b9a58f2355e7643040d483a to your computer and use it in GitHub Desktop.
Display a table of available APT package updates
#!/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__ = 'apt-check@varonathe.org'
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