13
from UpdateManager.Core.utils import get_dist
15
from datetime import datetime
16
from textwrap import wrap
17
from urllib.error import URLError, HTTPError
18
from urllib.request import urlopen
20
# TODO make DEBUG an environmental variable
22
UA_STATUS_FILE = "/var/lib/ubuntu-advantage/status.json"
26
"""Tracks overall patch status
28
The relationship between archives enabled and whether a patch is eligible
29
for receiving updates is non-trivial. We track here all the important
30
buckets a package can be in:
32
- Whether it is set to expire with no ESM coverage
33
- Whether it is in an archive covered by ESM
34
- Whether it received LTS patches
35
- whether it received ESM patches
37
We also track the total packages covered and uncovered, and for the
38
uncovered packages, we track where they originate from.
40
The Ubuntu main archive receives patches for 5 years.
41
Canonical-owned archives (excluding partner) receive patches for 10 years.
45
# TODO no-update FIPS is never patched
46
self.pkgs_uncovered_fips = set()
48
# list of package names available in ESM
49
self.pkgs_updated_in_esmi = set()
50
self.pkgs_updated_in_esma = set()
54
self.pkgs_unavailable = set()
55
self.pkgs_thirdparty = set()
57
self.pkgs_uncategorized = set()
65
def whats_in_esm(url):
67
# return a set of package names in an esm archive
69
response = urlopen(url)
70
except (URLError, HTTPError):
71
print_debug('failed to load: %s' % url)
74
content = response.read().decode('utf-8')
76
print('failed to read data at: %s' % url)
78
for line in content.split('\n'):
79
if not line.startswith('Package:'):
82
pkg = line.split(': ')[1]
88
"""Return dict of active ua status information from ubuntu-advantage-tools
90
Prefer to obtain status information from cache on disk to avoid costly
91
roundtrips to contracts.canonical.com to check on available services.
93
Fallback to call ua status --format=json.
95
If there are errors running: ua status --format=json or if the status on
96
disk is unparseable, return an empty dict.
98
if os.path.exists(UA_STATUS_FILE):
99
with open(UA_STATUS_FILE, "r") as stream:
100
status = stream.read()
103
status = subprocess.check_output(
104
['ua', 'status', '--format=json']
106
except subprocess.CalledProcessError as e:
107
print_debug('failed to run ua status: %s' % e)
110
return json.loads(status)
111
except json.decoder.JSONDecodeError as e:
112
print_debug('failed to parse JSON from ua status output: %s' % e)
116
def is_ua_service_enabled(service_name: str) -> bool:
117
"""Check to see if named esm service is enabled.
119
:return: True if UA status reports service as enabled.
121
status = get_ua_status()
122
for service in status.get("services", []):
123
if service["name"] == service_name:
124
# Machines unattached to UA will not provide service 'status' key.
125
return service.get("status") == "enabled"
129
def trim_archive(archive):
130
return archive.split("-")[-1]
134
# *.ec2.archive.ubuntu.com -> archive.ubuntu.com
135
if host.endswith("archive.ubuntu.com"):
136
return "archive.ubuntu.com"
141
m_file = '/usr/share/ubuntu-release-upgrader/mirrors.cfg'
142
if not os.path.exists(m_file):
143
print("Official mirror list not found.")
144
with open(m_file) as f:
145
items = [x.strip() for x in f]
146
mirrors = [s.split('//')[1].split('/')[0] for s in items
147
if not s.startswith("#") and not s == ""]
148
# ddebs.ubuntu.com isn't in mirrors.cfg for every release
149
mirrors.append('ddebs.ubuntu.com')
153
def origins_for(ver: apt.package.Version) -> str:
155
for origin in ver.origins:
157
# When the package is installed, site is empty, archive/component
160
site = trim_site(origin.site)
161
s.append("%s %s/%s" % (site, origin.archive, origin.component))
165
def print_wrapped(str):
166
print("\n".join(wrap(str, break_on_hyphens=False)))
169
def print_thirdparty_count():
170
print(gettext.dngettext("update-manager",
171
"%s package is from a third party",
172
"%s packages are from third parties",
173
len(pkgstats.pkgs_thirdparty)) %
174
"{:>{width}}".format(len(pkgstats.pkgs_thirdparty), width=width))
177
def print_unavailable_count():
178
print(gettext.dngettext("update-manager",
179
"%s package is no longer available for "
181
"%s packages are no longer available for "
183
len(pkgstats.pkgs_unavailable)) %
184
"{:>{width}}".format(len(pkgstats.pkgs_unavailable), width=width))
188
'''Parse command line arguments.
192
parser = argparse.ArgumentParser(
193
description='Return information about security support for packages')
194
parser.add_argument('--thirdparty', action='store_true')
195
parser.add_argument('--unavailable', action='store_true')
199
if __name__ == "__main__":
200
# Prefer redirecting to 'pro security-status' if it exists
201
if shutil.which("/usr/bin/pro"):
202
print("This command has been replaced with 'pro security-status'.")
203
subprocess.run(["/usr/bin/pro", "security-status"])
207
APP = "update-manager"
208
DIR = "/usr/share/locale"
209
gettext.bindtextdomain(APP, DIR)
210
gettext.textdomain(APP)
212
parser = parse_options()
213
args = parser.parse_args()
215
esm_site = "esm.ubuntu.com"
218
dpkg = subprocess.check_output(['dpkg', '--print-architecture'])
219
arch = dpkg.decode().strip()
220
except subprocess.CalledProcessError:
221
print("failed getting dpkg architecture")
225
pkgstats = PatchStats()
226
codename = get_dist()
227
di = distro_info.UbuntuDistroInfo()
228
lts = di.is_lts(codename)
229
release_expired = True
230
if codename in di.supported():
231
release_expired = False
232
# distro-info-data in Ubuntu 16.04 LTS does not have eol-esm data
233
if codename != 'xenial':
234
eol_data = [(r.eol, r.eol_esm)
235
for r in di._releases if r.series == codename][0]
236
elif codename == 'xenial':
237
eol_data = (datetime.strptime('2021-04-21', '%Y-%m-%d'),
238
datetime.strptime('2024-04-21', '%Y-%m-%d'))
240
eol_esm = eol_data[1]
243
origins_by_package = {}
244
official_mirrors = mirror_list()
246
# N.B. only the security pocket is checked because this tool displays
247
# information about security updates
249
'https://%s/%s/ubuntu/dists/%s-%s-%s/main/binary-%s/Packages'
250
pkgs_in_esma = whats_in_esm(esm_url %
251
(esm_site, 'apps', codename, 'apps',
253
pkgs_in_esmi = whats_in_esm(esm_url %
254
(esm_site, 'infra', codename, 'infra',
261
if not pkg.is_installed:
263
if not pkg.candidate or not pkg.candidate.downloadable:
266
origins_by_package[pkgname] = set()
268
for ver in pkg.versions:
269
# Loop through origins and store all of them. The idea here is that
270
# we don't care where the installed package comes from, provided
271
# there is at least one repository we identify as being
272
# security-assured under either LTS or ESM.
273
for origin in ver.origins:
274
# TODO: in order to handle FIPS and other archives which have
275
# root-level path names, we'll need to loop over ver.uris
279
site = trim_site(origin.site)
280
archive = origin.archive
281
component = origin.component
282
origin = origin.origin
283
official_mirror = False
285
# thirdparty providers like dl.google.com don't set "Origin"
286
if origin != "Ubuntu":
288
if site in official_mirrors:
289
site = "official_mirror"
290
if "MY_MIRROR" in os.environ:
291
if site in os.environ["MY_MIRROR"]:
292
site = "official_mirror"
293
t = (site, archive, component, thirdparty)
297
origins_by_package[pkgname].add(t)
300
pkg_sites.append("%s %s/%s" %
301
(site, archive, component))
303
print_debug("available versions for %s" % pkgname)
304
print_debug(",".join(pkg_sites))
306
# This tracks suites we care about. Sadly, it appears that the way apt
307
# stores origins truncates away the path that comes after the
308
# domainname in the site portion, or maybe I am just clueless, but
309
# there's no way to tell FIPS apart from ESM, for instance.
310
# See 00REPOS.txt for examples
312
# 2020-03-18 ver.filename has the path so why is that no good?
314
# TODO Need to handle:
315
# MAAS, lxd, juju PPAs
319
# TODO handle partner.c.c
321
# main and restricted from release, -updates, -proposed, or -security
323
suite_main = ("official_mirror", codename, "main", True)
324
suite_main_updates = ("official_mirror", codename + "-updates",
326
suite_main_security = ("official_mirror", codename + "-security",
328
suite_main_proposed = ("official_mirror", codename + "-proposed",
331
suite_restricted = ("official_mirror", codename, "restricted",
333
suite_restricted_updates = ("official_mirror",
334
codename + "-updates",
336
suite_restricted_security = ("official_mirror",
337
codename + "-security",
339
suite_restricted_proposed = ("official_mirror",
340
codename + "-proposed",
343
# universe and multiverse from release, -updates, -proposed, or -security
345
suite_universe = ("official_mirror", codename, "universe", True)
346
suite_universe_updates = ("official_mirror", codename + "-updates",
348
suite_universe_security = ("official_mirror",
349
codename + "-security",
351
suite_universe_proposed = ("official_mirror",
352
codename + "-proposed",
355
suite_multiverse = ("official_mirror", codename, "multiverse",
357
suite_multiverse_updates = ("official_mirror",
358
codename + "-updates",
360
suite_multiverse_security = ("official_mirror",
361
codename + "-security",
363
suite_multiverse_proposed = ("official_mirror",
364
codename + "-proposed",
367
# packages from the esm respositories
368
# N.B. Origin: Ubuntu is not set for esm
369
suite_esm_main = (esm_site, "%s-infra-updates" % codename,
371
suite_esm_main_security = (esm_site,
372
"%s-infra-security" % codename, "main")
373
suite_esm_universe = (esm_site,
374
"%s-apps-updates" % codename, "main")
375
suite_esm_universe_security = (esm_site,
376
"%s-apps-security" % codename,
379
esm_infra_enabled = is_ua_service_enabled("esm-infra")
380
esm_apps_enabled = is_ua_service_enabled("esm-apps")
381
ua_attached = get_ua_status().get("attached", False)
383
# Now do the final loop through
385
if not pkg.is_installed:
387
if not pkg.candidate or not pkg.candidate.downloadable:
388
pkgstats.pkgs_unavailable.add(pkg.name)
391
pkg_origins = origins_by_package[pkgname]
393
# This set of is_* booleans tracks specific situations we care about in
394
# the logic below; for instance, if the package has a main origin, or
395
# if the esm repos are enabled.
397
# Some packages get added in -updates and don't exist in the release
398
# pocket e.g. ubuntu-advantage-tools and libdrm-updates. To be safe all
399
# pockets are allowed.
400
is_mr_pkg_origin = (suite_main in pkg_origins) or \
401
(suite_main_updates in pkg_origins) or \
402
(suite_main_security in pkg_origins) or \
403
(suite_main_proposed in pkg_origins) or \
404
(suite_restricted in pkg_origins) or \
405
(suite_restricted_updates in pkg_origins) or \
406
(suite_restricted_security in pkg_origins) or \
407
(suite_restricted_proposed in pkg_origins)
408
is_um_pkg_origin = (suite_universe in pkg_origins) or \
409
(suite_universe_updates in pkg_origins) or \
410
(suite_universe_security in pkg_origins) or \
411
(suite_universe_proposed in pkg_origins) or \
412
(suite_multiverse in pkg_origins) or \
413
(suite_multiverse_updates in pkg_origins) or \
414
(suite_multiverse_security in pkg_origins) or \
415
(suite_multiverse_proposed in pkg_origins)
417
is_esm_infra_pkg_origin = (suite_esm_main in pkg_origins) or \
418
(suite_esm_main_security in pkg_origins)
419
is_esm_apps_pkg_origin = (suite_esm_universe in pkg_origins) or \
420
(suite_esm_universe_security in pkg_origins)
422
# A third party one won't appear in any of the above origins
423
if not is_mr_pkg_origin and not is_um_pkg_origin \
424
and not is_esm_infra_pkg_origin and not is_esm_apps_pkg_origin:
425
pkgstats.pkgs_thirdparty.add(pkgname)
427
if False: # TODO package has ESM fips origin
428
# TODO package has ESM fips-updates origin: OK
429
# If user has enabled FIPS, but not updates, BAD, but need some
430
# thought on how to display it, as it can't be patched at all
432
elif is_mr_pkg_origin:
433
pkgstats.pkgs_mr.add(pkgname)
434
elif is_um_pkg_origin:
435
pkgstats.pkgs_um.add(pkgname)
437
# TODO print information about packages in this category if in
439
pkgstats.pkgs_uncategorized.add(pkgname)
441
# Check to see if the package is available in esm-infra or esm-apps
442
# and add it to the right pkgstats category
443
# NB: apps is ordered first for testing the hello package which is both
445
if pkgname in pkgs_in_esma:
446
pkgstats.pkgs_updated_in_esma.add(pkgname)
447
elif pkgname in pkgs_in_esmi:
448
pkgstats.pkgs_updated_in_esmi.add(pkgname)
450
total_packages = (len(pkgstats.pkgs_mr) +
451
len(pkgstats.pkgs_um) +
452
len(pkgstats.pkgs_thirdparty) +
453
len(pkgstats.pkgs_unavailable))
454
width = len(str(total_packages))
455
print("%s packages installed, of which:" %
456
"{:>{width}}".format(total_packages, width=width))
458
# filters first as they provide less information
460
if pkgstats.pkgs_thirdparty:
461
pkgs_thirdparty = sorted(p for p in pkgstats.pkgs_thirdparty)
462
print_thirdparty_count()
463
print_wrapped(' '.join(pkgs_thirdparty))
464
msg = ("Packages from third parties are not provided by the "
465
"official Ubuntu archive, for example packages from "
466
"Personal Package Archives in Launchpad.")
470
print_wrapped("Run 'apt-cache policy %s' to learn more about "
471
"that package." % pkgs_thirdparty[0])
474
print_wrapped("You have no packages installed from a third party.")
477
if pkgstats.pkgs_unavailable:
478
pkgs_unavailable = sorted(p for p in pkgstats.pkgs_unavailable)
479
print_unavailable_count()
480
print_wrapped(' '.join(pkgs_unavailable))
481
msg = ("Packages that are not available for download "
482
"may be left over from a previous release of "
483
"Ubuntu, may have been installed directly from "
484
"a .deb file, or are from a source which has "
489
print_wrapped("Run 'apt-cache show %s' to learn more about "
490
"that package." % pkgs_unavailable[0])
493
print_wrapped("You have no packages installed that are no longer "
496
# Only show LTS patches and expiration notices if the release is not
497
# yet expired; showing LTS patches would give a false sense of
499
if not release_expired:
500
print("%s receive package updates%s until %d/%d" %
501
("{:>{width}}".format(len(pkgstats.pkgs_mr),
503
" with LTS" if lts else "",
504
eol.month, eol.year))
505
elif release_expired and lts:
506
receive_text = "could receive"
507
if esm_infra_enabled:
508
if len(pkgstats.pkgs_mr) == 1:
509
receive_text = "is receiving"
511
receive_text = "are receiving"
512
print("%s %s security updates with ESM Infra "
514
("{:>{width}}".format(len(pkgstats.pkgs_mr),
516
receive_text, eol_esm.month, eol_esm.year))
517
if lts and pkgstats.pkgs_um and is_ua_service_enabled("esm-apps"):
518
if len(pkgstats.pkgs_um) == 1:
519
receive_text = "is receiving"
521
receive_text = "are receiving"
522
print("%s %s security updates with ESM Apps "
524
("{:>{width}}".format(len(pkgstats.pkgs_um),
526
receive_text, eol_esm.month, eol_esm.year))
527
if pkgstats.pkgs_thirdparty:
528
print_thirdparty_count()
529
if pkgstats.pkgs_unavailable:
530
print_unavailable_count()
531
# print the detail messages after the count of packages
532
if pkgstats.pkgs_thirdparty:
533
msg = ("Packages from third parties are not provided by the "
534
"official Ubuntu archive, for example packages from "
535
"Personal Package Archives in Launchpad.")
538
action = ("For more information on the packages, run "
539
"'ubuntu-security-status --thirdparty'.")
540
print_wrapped(action)
541
if pkgstats.pkgs_unavailable:
542
msg = ("Packages that are not available for download "
543
"may be left over from a previous release of "
544
"Ubuntu, may have been installed directly from "
545
"a .deb file, or are from a source which has "
549
action = ("For more information on the packages, run "
550
"'ubuntu-security-status --unavailable'.")
551
print_wrapped(action)
552
# print the ESM calls to action last
553
if lts and not esm_infra_enabled:
554
if release_expired and pkgstats.pkgs_mr:
555
pkgs_updated_in_esmi = pkgstats.pkgs_updated_in_esmi
557
msg = gettext.dngettext(
559
"Enable Extended Security "
560
"Maintenance (ESM Infra) to "
561
"get %i security update (so far) ",
562
"Enable Extended Security "
563
"Maintenance (ESM Infra) to "
564
"get %i security updates (so far) ",
565
len(pkgs_updated_in_esmi)) % len(pkgs_updated_in_esmi)
566
msg += gettext.dngettext(
568
"and enable coverage of %i "
570
"and enable coverage of %i "
572
len(pkgstats.pkgs_mr)) % len(pkgstats.pkgs_mr)
575
print("\nEnable ESM Infra with: ua enable esm-infra")
577
if lts and not ua_attached:
578
print("\nThis machine is not attached to an Ubuntu Advantage "
579
"subscription.\nSee https://ubuntu.com/advantage")