11
from UpdateManager.Core.utils import get_dist
13
from datetime import datetime
14
from textwrap import wrap
15
from urllib.error import URLError, HTTPError
16
from urllib.request import urlopen
18
# TODO make DEBUG an environmental variable
23
"""Tracks overall patch status
25
The relationship between archives enabled and whether a patch is eligible
26
for receiving updates is non-trivial. We track here all the important
27
buckets a package can be in:
29
- Whether it is set to expire with no ESM coverage
30
- Whether it is in an archive covered by ESM
31
- Whether it received LTS patches
32
- whether it received ESM patches
34
We also track the total packages covered and uncovered, and for the
35
uncovered packages, we track where they originate from.
37
The Ubuntu main archive receives patches for 5 years.
38
Canonical-owned archives (excluding partner) receive patches for 10 years.
42
# TODO no-update FIPS is never patched
43
self.pkgs_uncovered_fips = set()
45
# list of package names available in ESM
46
self.pkgs_updated_in_esmi = set()
47
self.pkgs_updated_in_esma = set()
51
self.pkgs_unavailable = set()
52
self.pkgs_thirdparty = set()
54
self.pkgs_uncategorized = set()
62
def whats_in_esm(url):
64
# return a set of package names in an esm archive
66
response = urlopen(url)
67
except (URLError, HTTPError):
68
print_debug('failed to load: %s' % url)
71
content = response.read().decode('utf-8')
73
print('failed to read data at: %s' % url)
75
for line in content.split('\n'):
76
if not line.startswith('Package:'):
79
pkg = line.split(': ')[1]
84
def livepatch_is_enabled():
85
""" Check to see if livepatch is enabled on the system"""
87
c_livepatch = subprocess.run(["/snap/bin/canonical-livepatch",
89
stdout=subprocess.PIPE,
90
stderr=subprocess.PIPE)
91
# it can't be enabled if it isn't installed
92
except FileNotFoundError:
94
if c_livepatch.returncode == 0:
96
elif c_livepatch.returncode == 1:
100
def esm_is_enabled():
101
""" Check to see if esm is an available source"""
102
acp = subprocess.Popen(["apt-cache", "policy"],
103
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
104
grep = subprocess.run(["grep", "-F", "-q", "https://%s" % esm_site],
105
stdin=acp.stdout, stdout=subprocess.PIPE)
106
if grep.returncode == 0:
108
elif grep.returncode == -1:
112
def trim_archive(archive):
113
return archive.split("-")[-1]
117
# *.ec2.archive.ubuntu.com -> archive.ubuntu.com
118
if host.endswith("archive.ubuntu.com"):
119
return "archive.ubuntu.com"
124
m_file = '/usr/share/ubuntu-release-upgrader/mirrors.cfg'
125
if not os.path.exists(m_file):
126
print("Official mirror list not found.")
127
with open(m_file) as f:
128
items = [x.strip() for x in f]
129
mirrors = [s.split('//')[1].split('/')[0] for s in items
130
if not s.startswith("#") and not s == ""]
131
# ddebs.ubuntu.com isn't in mirrors.cfg for every release
132
mirrors.append('ddebs.ubuntu.com')
136
def origins_for(ver: apt.package.Version) -> str:
138
for origin in ver.origins:
140
# When the package is installed, site is empty, archive/component
143
site = trim_site(origin.site)
144
s.append("%s %s/%s" % (site, origin.archive, origin.component))
148
def print_wrapped(str):
149
print("\n".join(wrap(str, break_on_hyphens=False)))
152
def print_thirdparty_count():
153
print(gettext.dngettext("update-manager",
154
"%s package is from a third party",
155
"%s packages are from third parties",
156
len(pkgstats.pkgs_thirdparty)) %
157
"{:>{width}}".format(len(pkgstats.pkgs_thirdparty), width=width))
160
def print_unavailable_count():
161
print(gettext.dngettext("update-manager",
162
"%s package is no longer available for "
164
"%s packages are no longer available for "
166
len(pkgstats.pkgs_unavailable)) %
167
"{:>{width}}".format(len(pkgstats.pkgs_unavailable), width=width))
171
'''Parse command line arguments.
175
parser = argparse.ArgumentParser(
176
description='Return information about security support for packages')
177
parser.add_argument('--thirdparty', action='store_true')
178
parser.add_argument('--unavailable', action='store_true')
182
if __name__ == "__main__":
184
APP = "update-manager"
185
DIR = "/usr/share/locale"
186
gettext.bindtextdomain(APP, DIR)
187
gettext.textdomain(APP)
189
parser = parse_options()
190
args = parser.parse_args()
192
esm_site = "esm.ubuntu.com"
195
dpkg = subprocess.check_output(['dpkg', '--print-architecture'])
196
arch = dpkg.decode().strip()
197
except subprocess.CalledProcessError:
198
print("failed getting dpkg architecture")
202
pkgstats = PatchStats()
203
codename = get_dist()
204
di = distro_info.UbuntuDistroInfo()
205
lts = di.is_lts(codename)
206
release_expired = True
207
if codename in di.supported():
208
release_expired = False
209
# distro-info-data in Ubuntu 16.04 LTS does not have eol-esm data
210
if codename != 'xenial':
211
eol_data = [(r.eol, r.eol_esm)
212
for r in di._releases if r.series == codename][0]
213
elif codename == 'xenial':
214
eol_data = (datetime.strptime('2021-04-21', '%Y-%m-%d'),
215
datetime.strptime('2024-04-21', '%Y-%m-%d'))
217
eol_esm = eol_data[1]
220
origins_by_package = {}
221
official_mirrors = mirror_list()
223
# N.B. only the security pocket is checked because this tool displays
224
# information about security updates
226
'https://%s/%s/ubuntu/dists/%s-%s-%s/main/binary-%s/Packages'
227
pkgs_in_esma = whats_in_esm(esm_url %
228
(esm_site, 'apps', codename, 'apps',
230
pkgs_in_esmi = whats_in_esm(esm_url %
231
(esm_site, 'infra', codename, 'infra',
238
if not pkg.is_installed:
240
if not pkg.candidate or not pkg.candidate.downloadable:
243
origins_by_package[pkgname] = set()
245
for ver in pkg.versions:
246
# Loop through origins and store all of them. The idea here is that
247
# we don't care where the installed package comes from, provided
248
# there is at least one repository we identify as being
249
# security-assured under either LTS or ESM.
250
for origin in ver.origins:
251
# TODO: in order to handle FIPS and other archives which have
252
# root-level path names, we'll need to loop over ver.uris
256
site = trim_site(origin.site)
257
archive = origin.archive
258
component = origin.component
259
origin = origin.origin
260
official_mirror = False
262
# thirdparty providers like dl.google.com don't set "Origin"
263
if origin != "Ubuntu":
265
if site in official_mirrors:
266
site = "official_mirror"
267
if "MY_MIRROR" in os.environ:
268
if site in os.environ["MY_MIRROR"]:
269
site = "official_mirror"
270
t = (site, archive, component, thirdparty)
274
origins_by_package[pkgname].add(t)
277
pkg_sites.append("%s %s/%s" %
278
(site, archive, component))
280
print_debug("available versions for %s" % pkgname)
281
print_debug(",".join(pkg_sites))
283
# This tracks suites we care about. Sadly, it appears that the way apt
284
# stores origins truncates away the path that comes after the
285
# domainname in the site portion, or maybe I am just clueless, but
286
# there's no way to tell FIPS apart from ESM, for instance.
287
# See 00REPOS.txt for examples
289
# 2020-03-18 ver.filename has the path so why is that no good?
291
# TODO Need to handle:
292
# MAAS, lxd, juju PPAs
296
# TODO handle partner.c.c
298
# main and restricted from release, -updates, -proposed, or -security
300
suite_main = ("official_mirror", codename, "main", True)
301
suite_main_updates = ("official_mirror", codename + "-updates",
303
suite_main_security = ("official_mirror", codename + "-security",
305
suite_main_proposed = ("official_mirror", codename + "-proposed",
308
suite_restricted = ("official_mirror", codename, "restricted",
310
suite_restricted_updates = ("official_mirror",
311
codename + "-updates",
313
suite_restricted_security = ("official_mirror",
314
codename + "-security",
316
suite_restricted_proposed = ("official_mirror",
317
codename + "-proposed",
320
# universe and multiverse from release, -updates, -proposed, or -security
322
suite_universe = ("official_mirror", codename, "universe", True)
323
suite_universe_updates = ("official_mirror", codename + "-updates",
325
suite_universe_security = ("official_mirror",
326
codename + "-security",
328
suite_universe_proposed = ("official_mirror",
329
codename + "-proposed",
332
suite_multiverse = ("official_mirror", codename, "multiverse",
334
suite_multiverse_updates = ("official_mirror",
335
codename + "-updates",
337
suite_multiverse_security = ("official_mirror",
338
codename + "-security",
340
suite_multiverse_proposed = ("official_mirror",
341
codename + "-proposed",
344
# packages from the esm respositories
345
# N.B. Origin: Ubuntu is not set for esm
346
suite_esm_main = (esm_site, "%s-infra-updates" % codename,
348
suite_esm_main_security = (esm_site,
349
"%s-infra-security" % codename, "main")
350
suite_esm_universe = (esm_site,
351
"%s-apps-updates" % codename, "main")
352
suite_esm_universe_security = (esm_site,
353
"%s-apps-security" % codename,
356
livepatch_enabled = livepatch_is_enabled()
357
esm_enabled = esm_is_enabled()
358
is_esm_infra_used = (suite_esm_main in all_origins) or \
359
(suite_esm_main_security in all_origins)
360
is_esm_apps_used = (suite_esm_universe in all_origins) or \
361
(suite_esm_universe_security in all_origins)
363
# Now do the final loop through
365
if not pkg.is_installed:
367
if not pkg.candidate or not pkg.candidate.downloadable:
368
pkgstats.pkgs_unavailable.add(pkg.name)
371
pkg_origins = origins_by_package[pkgname]
373
# This set of is_* booleans tracks specific situations we care about in
374
# the logic below; for instance, if the package has a main origin, or
375
# if the esm repos are enabled.
377
# Some packages get added in -updates and don't exist in the release
378
# pocket e.g. ubuntu-advantage-tools and libdrm-updates. To be safe all
379
# pockets are allowed.
380
is_mr_pkg_origin = (suite_main in pkg_origins) or \
381
(suite_main_updates in pkg_origins) or \
382
(suite_main_security in pkg_origins) or \
383
(suite_main_proposed in pkg_origins) or \
384
(suite_restricted in pkg_origins) or \
385
(suite_restricted_updates in pkg_origins) or \
386
(suite_restricted_security in pkg_origins) or \
387
(suite_restricted_proposed in pkg_origins)
388
is_um_pkg_origin = (suite_universe in pkg_origins) or \
389
(suite_universe_updates in pkg_origins) or \
390
(suite_universe_security in pkg_origins) or \
391
(suite_universe_proposed in pkg_origins) or \
392
(suite_multiverse in pkg_origins) or \
393
(suite_multiverse_updates in pkg_origins) or \
394
(suite_multiverse_security in pkg_origins) or \
395
(suite_multiverse_proposed in pkg_origins)
397
is_esm_infra_pkg_origin = (suite_esm_main in pkg_origins) or \
398
(suite_esm_main_security in pkg_origins)
399
is_esm_apps_pkg_origin = (suite_esm_universe in pkg_origins) or \
400
(suite_esm_universe_security in pkg_origins)
402
# A third party one won't appear in any of the above origins
403
if not is_mr_pkg_origin and not is_um_pkg_origin \
404
and not is_esm_infra_pkg_origin and not is_esm_apps_pkg_origin:
405
pkgstats.pkgs_thirdparty.add(pkgname)
407
if False: # TODO package has ESM fips origin
408
# TODO package has ESM fips-updates origin: OK
409
# If user has enabled FIPS, but not updates, BAD, but need some
410
# thought on how to display it, as it can't be patched at all
412
elif is_mr_pkg_origin:
413
pkgstats.pkgs_mr.add(pkgname)
414
elif is_um_pkg_origin:
415
pkgstats.pkgs_um.add(pkgname)
417
# TODO print information about packages in this category if in
419
pkgstats.pkgs_uncategorized.add(pkgname)
421
# Check to see if the package is available in esm-infra or esm-apps
422
# and add it to the right pkgstats category
423
# NB: apps is ordered first for testing the hello package which is both
425
if pkgname in pkgs_in_esma:
426
pkgstats.pkgs_updated_in_esma.add(pkgname)
427
elif pkgname in pkgs_in_esmi:
428
pkgstats.pkgs_updated_in_esmi.add(pkgname)
430
total_packages = (len(pkgstats.pkgs_mr) +
431
len(pkgstats.pkgs_um) +
432
len(pkgstats.pkgs_thirdparty) +
433
len(pkgstats.pkgs_unavailable))
434
width = len(str(total_packages))
435
print("%s packages installed, of which:" %
436
"{:>{width}}".format(total_packages, width=width))
438
# filters first as they provide less information
440
if pkgstats.pkgs_thirdparty:
441
pkgs_thirdparty = sorted(p for p in pkgstats.pkgs_thirdparty)
442
print_thirdparty_count()
443
print_wrapped(' '.join(pkgs_thirdparty))
444
msg = ("Packages from third parties are not provided by the "
445
"official Ubuntu archive, for example packages from "
446
"Personal Package Archives in Launchpad.")
450
print_wrapped("Run 'apt-cache policy %s' to learn more about "
451
"that package." % pkgs_thirdparty[0])
454
print_wrapped("You have no packages installed from a third party.")
457
if pkgstats.pkgs_unavailable:
458
pkgs_unavailable = sorted(p for p in pkgstats.pkgs_unavailable)
459
print_unavailable_count()
460
print_wrapped(' '.join(pkgs_unavailable))
461
msg = ("Packages that are not available for download "
462
"may be left over from a previous release of "
463
"Ubuntu, may have been installed directly from "
464
"a .deb file, or are from a source which has "
469
print_wrapped("Run 'apt-cache show %s' to learn more about "
470
"that package." % pkgs_unavailable[0])
473
print_wrapped("You have no packages installed that are no longer "
476
# Only show LTS patches and expiration notices if the release is not
477
# yet expired; showing LTS patches would give a false sense of
479
if not release_expired:
480
print("%s receive package updates%s until %d/%d" %
481
("{:>{width}}".format(len(pkgstats.pkgs_mr),
483
" with LTS" if lts else "",
484
eol.month, eol.year))
485
elif release_expired and lts:
486
print("%s %s security updates with ESM Infra "
488
("{:>{width}}".format(len(pkgstats.pkgs_mr),
490
"are receiving" if esm_enabled else "could receive",
491
eol_esm.month, eol_esm.year))
492
if lts and pkgstats.pkgs_um:
493
print("%s %s security updates with ESM Apps "
495
("{:>{width}}".format(len(pkgstats.pkgs_um),
497
"are receiving" if esm_enabled else "could receive",
498
eol_esm.month, eol_esm.year))
499
if pkgstats.pkgs_thirdparty:
500
print_thirdparty_count()
501
if pkgstats.pkgs_unavailable:
502
print_unavailable_count()
503
# print the detail messages after the count of packages
504
if pkgstats.pkgs_thirdparty:
505
msg = ("Packages from third parties are not provided by the "
506
"official Ubuntu archive, for example packages from "
507
"Personal Package Archives in Launchpad.")
510
action = ("For more information on the packages, run "
511
"'ubuntu-security-status --thirdparty'.")
512
print_wrapped(action)
513
if pkgstats.pkgs_unavailable:
514
msg = ("Packages that are not available for download "
515
"may be left over from a previous release of "
516
"Ubuntu, may have been installed directly from "
517
"a .deb file, or are from a source which has "
521
action = ("For more information on the packages, run "
522
"'ubuntu-security-status --unavailable'.")
523
print_wrapped(action)
524
# print the ESM calls to action last
525
if lts and not esm_enabled:
526
if release_expired and pkgstats.pkgs_mr:
527
pkgs_updated_in_esmi = pkgstats.pkgs_updated_in_esmi
529
print_wrapped(gettext.dngettext("update-manager",
530
"Enable Extended Security "
531
"Maintenance (ESM Infra) to "
532
"get %i security update (so far) "
533
"and enable coverage of %i "
535
"Enable Extended Security "
536
"Maintenance (ESM Infra) to "
537
"get %i security updates (so far) "
538
"and enable coverage of %i "
540
len(pkgs_updated_in_esmi)) %
541
(len(pkgs_updated_in_esmi),
542
len(pkgstats.pkgs_mr)))
543
if livepatch_enabled:
544
print("\nEnable ESM Infra with: ua enable esm-infra")
546
pkgs_updated_in_esma = pkgstats.pkgs_updated_in_esma
548
print_wrapped(gettext.dngettext("update-manager",
549
"Enable Extended Security "
550
"Maintenance (ESM Apps) to "
551
"get %i security update (so far) "
552
"and enable coverage of %i "
554
"Enable Extended Security "
555
"Maintenance (ESM Apps) to "
556
"get %i security updates (so far) "
557
"and enable coverage of %i "
559
len(pkgs_updated_in_esma)) %
560
(len(pkgs_updated_in_esma),
561
len(pkgstats.pkgs_um)))
562
if livepatch_enabled:
563
print("\nEnable ESM Apps with: ua enable esm-apps")
564
if lts and not livepatch_enabled:
565
print("\nThis machine is not attached to an Ubuntu Advantage "
566
"subscription.\nSee https://ubuntu.com/advantage")