~ubuntu-core-dev/update-manager/main

2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
1
#!/usr/bin/python3
2
3
import apt
4
import argparse
5
import distro_info
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
6
import json
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
7
import os
8
import sys
9
import gettext
10
import subprocess
11
12
from UpdateManager.Core.utils import get_dist
13
14
from datetime import datetime
15
from textwrap import wrap
2937 by Brian Murray
ubuntu-security-status: Check if ESM for Apps is enabled or if it is not
16
from uaclient.entitlements import ESMAppsEntitlement
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
17
from urllib.error import URLError, HTTPError
18
from urllib.request import urlopen
19
20
# TODO make DEBUG an environmental variable
21
DEBUG = False
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
22
UA_STATUS_FILE = "/var/lib/ubuntu-advantage/status.json"
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
23
24
25
class PatchStats:
26
    """Tracks overall patch status
27
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:
31
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
36
37
    We also track the total packages covered and uncovered, and for the
38
    uncovered packages, we track where they originate from.
39
40
    The Ubuntu main archive receives patches for 5 years.
41
    Canonical-owned archives (excluding partner) receive patches for 10 years.
42
        patches for 10 years.
43
    """
44
    def __init__(self):
45
        # TODO no-update FIPS is never patched
46
        self.pkgs_uncovered_fips = set()
47
48
        # list of package names available in ESM
49
        self.pkgs_updated_in_esmi = set()
50
        self.pkgs_updated_in_esma = set()
51
52
        self.pkgs_mr = set()
53
        self.pkgs_um = set()
54
        self.pkgs_unavailable = set()
55
        self.pkgs_thirdparty = set()
56
        # the bin of unknowns
57
        self.pkgs_uncategorized = set()
58
59
60
def print_debug(s):
61
    if DEBUG:
62
        print(s)
63
64
65
def whats_in_esm(url):
66
    pkgs = set()
67
    # return a set of package names in an esm archive
68
    try:
69
        response = urlopen(url)
70
    except (URLError, HTTPError):
71
        print_debug('failed to load: %s' % url)
72
        return pkgs
73
    try:
74
        content = response.read().decode('utf-8')
75
    except IOError:
76
        print('failed to read data at: %s' % url)
77
        sys.exit(1)
78
    for line in content.split('\n'):
79
        if not line.startswith('Package:'):
80
            continue
81
        else:
82
            pkg = line.split(': ')[1]
83
            pkgs.add(pkg)
84
    return pkgs
85
86
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
87
def get_ua_status():
88
    """Return dict of active ua status information from ubuntu-advantage-tools
89
90
    Prefer to obtain status information from cache on disk to avoid costly
91
    roundtrips to contracts.canonical.com to check on available services.
92
93
    Fallback to call ua status --format=json.
94
95
    If there are errors running: ua status --format=json or if the status on
96
    disk is unparseable, return an empty dict.
97
    """
98
    if os.path.exists(UA_STATUS_FILE):
99
        with open(UA_STATUS_FILE, "r") as stream:
100
            status = stream.read()
101
    else:
102
        try:
103
            status = subprocess.check_output(
104
                ['ua', 'status', '--format=json']
105
            ).decode()
106
        except subprocess.CalledProcessError as e:
107
            print_debug('failed to run ua status: %s' % e)
108
            return {}
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
109
    try:
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
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)
113
        return {}
114
115
116
def is_ua_service_enabled(service_name: str) -> bool:
117
    """Check to see if named esm service is enabled.
118
119
    :return: True if UA status reports service as enabled.
120
    """
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"
126
    return False
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
127
128
129
def trim_archive(archive):
130
    return archive.split("-")[-1]
131
132
133
def trim_site(host):
134
    # *.ec2.archive.ubuntu.com -> archive.ubuntu.com
135
    if host.endswith("archive.ubuntu.com"):
136
        return "archive.ubuntu.com"
137
    return host
138
139
140
def mirror_list():
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')
150
    return mirrors
151
152
153
def origins_for(ver: apt.package.Version) -> str:
154
    s = []
155
    for origin in ver.origins:
156
        if not origin.site:
157
            # When the package is installed, site is empty, archive/component
158
            # are "now/now"
159
            continue
160
        site = trim_site(origin.site)
161
        s.append("%s %s/%s" % (site, origin.archive, origin.component))
162
    return ",".join(s)
163
164
165
def print_wrapped(str):
166
    print("\n".join(wrap(str, break_on_hyphens=False)))
167
168
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))
175
176
177
def print_unavailable_count():
178
    print(gettext.dngettext("update-manager",
179
                            "%s package is no longer available for "
180
                            "download",
181
                            "%s packages are no longer available for "
182
                            "download",
183
                            len(pkgstats.pkgs_unavailable)) %
184
          "{:>{width}}".format(len(pkgstats.pkgs_unavailable), width=width))
185
186
187
def parse_options():
188
    '''Parse command line arguments.
189
190
    Return parser
191
    '''
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')
196
    return parser
197
198
199
if __name__ == "__main__":
200
    # gettext
201
    APP = "update-manager"
202
    DIR = "/usr/share/locale"
203
    gettext.bindtextdomain(APP, DIR)
204
    gettext.textdomain(APP)
205
206
    parser = parse_options()
207
    args = parser.parse_args()
208
209
    esm_site = "esm.ubuntu.com"
210
211
    try:
212
        dpkg = subprocess.check_output(['dpkg', '--print-architecture'])
213
        arch = dpkg.decode().strip()
214
    except subprocess.CalledProcessError:
215
        print("failed getting dpkg architecture")
216
        sys.exit(1)
217
218
    cache = apt.Cache()
219
    pkgstats = PatchStats()
220
    codename = get_dist()
221
    di = distro_info.UbuntuDistroInfo()
222
    lts = di.is_lts(codename)
223
    release_expired = True
224
    if codename in di.supported():
225
        release_expired = False
226
    # distro-info-data in Ubuntu 16.04 LTS does not have eol-esm data
227
    if codename != 'xenial':
228
        eol_data = [(r.eol, r.eol_esm)
229
                    for r in di._releases if r.series == codename][0]
230
    elif codename == 'xenial':
231
        eol_data = (datetime.strptime('2021-04-21', '%Y-%m-%d'),
232
                    datetime.strptime('2024-04-21', '%Y-%m-%d'))
233
    eol = eol_data[0]
234
    eol_esm = eol_data[1]
235
236
    all_origins = set()
237
    origins_by_package = {}
238
    official_mirrors = mirror_list()
239
240
    # N.B. only the security pocket is checked because this tool displays
241
    # information about security updates
242
    esm_url = \
243
        'https://%s/%s/ubuntu/dists/%s-%s-%s/main/binary-%s/Packages'
244
    pkgs_in_esma = whats_in_esm(esm_url %
245
                                (esm_site, 'apps', codename, 'apps',
246
                                 'security', arch))
247
    pkgs_in_esmi = whats_in_esm(esm_url %
248
                                (esm_site, 'infra', codename, 'infra',
249
                                 'security', arch))
250
251
    for pkg in cache:
252
        pkgname = pkg.name
253
254
        downloadable = True
255
        if not pkg.is_installed:
256
            continue
257
        if not pkg.candidate or not pkg.candidate.downloadable:
258
            downloadable = False
259
        pkg_sites = []
260
        origins_by_package[pkgname] = set()
261
262
        for ver in pkg.versions:
263
            # Loop through origins and store all of them. The idea here is that
264
            # we don't care where the installed package comes from, provided
265
            # there is at least one repository we identify as being
266
            # security-assured under either LTS or ESM.
267
            for origin in ver.origins:
268
                # TODO: in order to handle FIPS and other archives which have
269
                # root-level path names, we'll need to loop over ver.uris
270
                # instead
271
                if not origin.site:
272
                    continue
273
                site = trim_site(origin.site)
274
                archive = origin.archive
275
                component = origin.component
276
                origin = origin.origin
277
                official_mirror = False
278
                thirdparty = True
279
                # thirdparty providers like dl.google.com don't set "Origin"
280
                if origin != "Ubuntu":
281
                    thirdparty = False
282
                if site in official_mirrors:
283
                    site = "official_mirror"
284
                if "MY_MIRROR" in os.environ:
285
                    if site in os.environ["MY_MIRROR"]:
286
                        site = "official_mirror"
287
                t = (site, archive, component, thirdparty)
288
                if not site:
289
                    continue
290
                all_origins.add(t)
291
                origins_by_package[pkgname].add(t)
292
293
            if DEBUG:
294
                pkg_sites.append("%s %s/%s" %
295
                                 (site, archive, component))
296
297
        print_debug("available versions for %s" % pkgname)
298
        print_debug(",".join(pkg_sites))
299
300
    # This tracks suites we care about. Sadly, it appears that the way apt
301
    # stores origins truncates away the path that comes after the
302
    # domainname in the site portion, or maybe I am just clueless, but
303
    # there's no way to tell FIPS apart from ESM, for instance.
304
    # See 00REPOS.txt for examples
305
306
    # 2020-03-18 ver.filename has the path so why is that no good?
307
308
    # TODO Need to handle:
309
    #   MAAS, lxd, juju PPAs
310
    #   other PPAs
311
    #   other repos
312
313
    # TODO handle partner.c.c
314
315
    # main and restricted from release, -updates, -proposed, or -security
316
    # pockets
317
    suite_main = ("official_mirror", codename, "main", True)
318
    suite_main_updates = ("official_mirror", codename + "-updates",
319
                          "main", True)
320
    suite_main_security = ("official_mirror", codename + "-security",
321
                           "main", True)
322
    suite_main_proposed = ("official_mirror", codename + "-proposed",
323
                           "main", True)
324
325
    suite_restricted = ("official_mirror", codename, "restricted",
326
                        True)
327
    suite_restricted_updates = ("official_mirror",
328
                                codename + "-updates",
329
                                "restricted", True)
330
    suite_restricted_security = ("official_mirror",
331
                                 codename + "-security",
332
                                 "restricted", True)
333
    suite_restricted_proposed = ("official_mirror",
334
                                 codename + "-proposed",
335
                                 "restricted", True)
336
337
    # universe and multiverse from release, -updates, -proposed, or -security
338
    # pockets
339
    suite_universe = ("official_mirror", codename, "universe", True)
340
    suite_universe_updates = ("official_mirror", codename + "-updates",
341
                              "universe", True)
342
    suite_universe_security = ("official_mirror",
343
                               codename + "-security",
344
                               "universe", True)
345
    suite_universe_proposed = ("official_mirror",
346
                               codename + "-proposed",
347
                               "universe", True)
348
349
    suite_multiverse = ("official_mirror", codename, "multiverse",
350
                        True)
351
    suite_multiverse_updates = ("official_mirror",
352
                                codename + "-updates",
353
                                "multiverse", True)
354
    suite_multiverse_security = ("official_mirror",
355
                                 codename + "-security",
356
                                 "multiverse", True)
357
    suite_multiverse_proposed = ("official_mirror",
358
                                 codename + "-proposed",
359
                                 "multiverse", True)
360
361
    # packages from the esm respositories
362
    # N.B. Origin: Ubuntu is not set for esm
363
    suite_esm_main = (esm_site, "%s-infra-updates" % codename,
364
                      "main")
365
    suite_esm_main_security = (esm_site,
366
                               "%s-infra-security" % codename, "main")
367
    suite_esm_universe = (esm_site,
368
                          "%s-apps-updates" % codename, "main")
369
    suite_esm_universe_security = (esm_site,
370
                                   "%s-apps-security" % codename,
371
                                   "main")
372
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
373
    esm_infra_enabled = is_ua_service_enabled("esm-infra")
374
    esm_apps_enabled = is_ua_service_enabled("esm-apps")
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
375
    ua_attached = get_ua_status().get("attached", False)
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
376
377
    # Now do the final loop through
378
    for pkg in cache:
379
        if not pkg.is_installed:
380
            continue
381
        if not pkg.candidate or not pkg.candidate.downloadable:
382
            pkgstats.pkgs_unavailable.add(pkg.name)
383
            continue
384
        pkgname = pkg.name
385
        pkg_origins = origins_by_package[pkgname]
386
387
        # This set of is_* booleans tracks specific situations we care about in
388
        # the logic below; for instance, if the package has a main origin, or
389
        # if the esm repos are enabled.
390
391
        # Some packages get added in -updates and don't exist in the release
392
        # pocket e.g. ubuntu-advantage-tools and libdrm-updates. To be safe all
393
        # pockets are allowed.
394
        is_mr_pkg_origin = (suite_main in pkg_origins) or \
395
                           (suite_main_updates in pkg_origins) or \
396
                           (suite_main_security in pkg_origins) or \
397
                           (suite_main_proposed in pkg_origins) or \
398
                           (suite_restricted in pkg_origins) or \
399
                           (suite_restricted_updates in pkg_origins) or \
400
                           (suite_restricted_security in pkg_origins) or \
401
                           (suite_restricted_proposed in pkg_origins)
402
        is_um_pkg_origin = (suite_universe in pkg_origins) or \
403
                           (suite_universe_updates in pkg_origins) or \
404
                           (suite_universe_security in pkg_origins) or \
405
                           (suite_universe_proposed in pkg_origins) or \
406
                           (suite_multiverse in pkg_origins) or \
407
                           (suite_multiverse_updates in pkg_origins) or \
408
                           (suite_multiverse_security in pkg_origins) or \
409
                           (suite_multiverse_proposed in pkg_origins)
410
411
        is_esm_infra_pkg_origin = (suite_esm_main in pkg_origins) or \
412
                                  (suite_esm_main_security in pkg_origins)
413
        is_esm_apps_pkg_origin = (suite_esm_universe in pkg_origins) or \
414
                                 (suite_esm_universe_security in pkg_origins)
415
416
        # A third party one won't appear in any of the above origins
417
        if not is_mr_pkg_origin and not is_um_pkg_origin \
418
                and not is_esm_infra_pkg_origin and not is_esm_apps_pkg_origin:
419
            pkgstats.pkgs_thirdparty.add(pkgname)
420
421
        if False:  # TODO package has ESM fips origin
422
            # TODO package has ESM fips-updates origin: OK
423
            # If user has enabled FIPS, but not updates, BAD, but need some
424
            # thought on how to display it, as it can't be patched at all
425
            pass
426
        elif is_mr_pkg_origin:
427
            pkgstats.pkgs_mr.add(pkgname)
428
        elif is_um_pkg_origin:
429
            pkgstats.pkgs_um.add(pkgname)
430
        else:
431
            # TODO print information about packages in this category if in
432
            # debugging mode
433
            pkgstats.pkgs_uncategorized.add(pkgname)
434
435
        # Check to see if the package is available in esm-infra or esm-apps
436
        # and add it to the right pkgstats category
437
        # NB: apps is ordered first for testing the hello package which is both
438
        # in esmi and esma
439
        if pkgname in pkgs_in_esma:
440
            pkgstats.pkgs_updated_in_esma.add(pkgname)
441
        elif pkgname in pkgs_in_esmi:
442
            pkgstats.pkgs_updated_in_esmi.add(pkgname)
443
444
    total_packages = (len(pkgstats.pkgs_mr) +
445
                      len(pkgstats.pkgs_um) +
446
                      len(pkgstats.pkgs_thirdparty) +
447
                      len(pkgstats.pkgs_unavailable))
448
    width = len(str(total_packages))
449
    print("%s packages installed, of which:" %
450
          "{:>{width}}".format(total_packages, width=width))
451
452
    # filters first as they provide less information
453
    if args.thirdparty:
454
        if pkgstats.pkgs_thirdparty:
455
            pkgs_thirdparty = sorted(p for p in pkgstats.pkgs_thirdparty)
456
            print_thirdparty_count()
457
            print_wrapped(' '.join(pkgs_thirdparty))
458
            msg = ("Packages from third parties are not provided by the "
459
                   "official Ubuntu archive, for example packages from "
460
                   "Personal Package Archives in Launchpad.")
461
            print("")
462
            print_wrapped(msg)
463
            print("")
464
            print_wrapped("Run 'apt-cache policy %s' to learn more about "
465
                          "that package." % pkgs_thirdparty[0])
466
            sys.exit(0)
467
        else:
468
            print_wrapped("You have no packages installed from a third party.")
469
            sys.exit(0)
470
    if args.unavailable:
471
        if pkgstats.pkgs_unavailable:
472
            pkgs_unavailable = sorted(p for p in pkgstats.pkgs_unavailable)
473
            print_unavailable_count()
474
            print_wrapped(' '.join(pkgs_unavailable))
475
            msg = ("Packages that are not available for download "
476
                   "may be left over from a previous release of "
477
                   "Ubuntu, may have been installed directly from "
478
                   "a .deb file, or are from a source which has "
479
                   "been disabled.")
480
            print("")
481
            print_wrapped(msg)
482
            print("")
483
            print_wrapped("Run 'apt-cache show %s' to learn more about "
484
                          "that package." % pkgs_unavailable[0])
485
            sys.exit(0)
486
        else:
487
            print_wrapped("You have no packages installed that are no longer "
488
                          "available.")
489
            sys.exit(0)
490
    # Only show LTS patches and expiration notices if the release is not
491
    # yet expired; showing LTS patches would give a false sense of
492
    # security.
493
    if not release_expired:
494
        print("%s receive package updates%s until %d/%d" %
495
              ("{:>{width}}".format(len(pkgstats.pkgs_mr),
496
                                    width=width),
497
               " with LTS" if lts else "",
498
               eol.month, eol.year))
499
    elif release_expired and lts:
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
500
        receive_text = "could receive"
501
        if esm_infra_enabled:
502
            if len(pkgstats.pkgs_mr) == 1:
503
                receive_text = "is receiving"
504
            else:
505
                receive_text = "are receiving"
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
506
        print("%s %s security updates with ESM Infra "
507
              "until %d/%d" %
508
              ("{:>{width}}".format(len(pkgstats.pkgs_mr),
509
                                    width=width),
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
510
               receive_text, eol_esm.month, eol_esm.year))
2937 by Brian Murray
ubuntu-security-status: Check if ESM for Apps is enabled or if it is not
511
    if lts and pkgstats.pkgs_um and (
512
        is_ua_service_enabled("esm-apps") or not ESMAppsEntitlement.is_beta
513
    ):
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
514
        receive_text = "could receive"
515
        if esm_apps_enabled:
516
            if len(pkgstats.pkgs_um) == 1:
517
                receive_text = "is receiving"
518
            else:
519
                receive_text = "are receiving"
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
520
        print("%s %s security updates with ESM Apps "
521
              "until %d/%d" %
522
              ("{:>{width}}".format(len(pkgstats.pkgs_um),
523
                                    width=width),
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
524
               receive_text, eol_esm.month, eol_esm.year))
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
525
    if pkgstats.pkgs_thirdparty:
526
        print_thirdparty_count()
527
    if pkgstats.pkgs_unavailable:
528
        print_unavailable_count()
529
    # print the detail messages after the count of packages
530
    if pkgstats.pkgs_thirdparty:
531
        msg = ("Packages from third parties are not provided by the "
532
               "official Ubuntu archive, for example packages from "
533
               "Personal Package Archives in Launchpad.")
534
        print("")
535
        print_wrapped(msg)
536
        action = ("For more information on the packages, run "
537
                  "'ubuntu-security-status --thirdparty'.")
538
        print_wrapped(action)
539
    if pkgstats.pkgs_unavailable:
540
        msg = ("Packages that are not available for download "
541
               "may be left over from a previous release of "
542
               "Ubuntu, may have been installed directly from "
543
               "a .deb file, or are from a source which has "
544
               "been disabled.")
545
        print("")
546
        print_wrapped(msg)
547
        action = ("For more information on the packages, run "
548
                  "'ubuntu-security-status --unavailable'.")
549
        print_wrapped(action)
550
    # print the ESM calls to action last
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
551
    if lts and not esm_infra_enabled:
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
552
        if release_expired and pkgstats.pkgs_mr:
553
            pkgs_updated_in_esmi = pkgstats.pkgs_updated_in_esmi
554
            print("")
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
555
            msg = gettext.dngettext(
556
                "update-manager",
557
                "Enable Extended Security "
558
                "Maintenance (ESM Infra) to "
559
                "get %i security update (so far) ",
560
                "Enable Extended Security "
561
                "Maintenance (ESM Infra) to "
562
                "get %i security updates (so far) ",
563
                len(pkgs_updated_in_esmi)) % len(pkgs_updated_in_esmi)
564
            msg += gettext.dngettext(
565
                "update-manager",
566
                "and enable coverage of %i "
567
                "package.",
568
                "and enable coverage of %i "
569
                "packages.",
570
                len(pkgstats.pkgs_mr)) % len(pkgstats.pkgs_mr)
571
            print_wrapped(msg)
2937 by Brian Murray
ubuntu-security-status: Check if ESM for Apps is enabled or if it is not
572
            if ua_attached:
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
573
                print("\nEnable ESM Infra with: ua enable esm-infra")
2937 by Brian Murray
ubuntu-security-status: Check if ESM for Apps is enabled or if it is not
574
    if lts and pkgstats.pkgs_um:
575
        if (
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
576
            not esm_apps_enabled and
2937 by Brian Murray
ubuntu-security-status: Check if ESM for Apps is enabled or if it is not
577
            not ESMAppsEntitlement.is_beta
578
        ):
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
579
            pkgs_updated_in_esma = pkgstats.pkgs_updated_in_esma
580
            print("")
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
581
            msg = gettext.dngettext(
582
                "update-manager",
583
                "Enable Extended Security "
584
                "Maintenance (ESM Apps) to "
585
                "get %i security update (so far) ",
586
                "Enable Extended Security "
587
                "Maintenance (ESM Apps) to "
588
                "get %i security updates (so far) ",
589
                len(pkgs_updated_in_esma)) % len(pkgs_updated_in_esma)
590
            msg += gettext.dngettext(
591
                "update-manager",
592
                "and enable coverage of %i "
593
                "package.",
594
                "and enable coverage of %i "
595
                "packages.",
596
                len(pkgstats.pkgs_um)) % len(pkgstats.pkgs_um)
597
            print_wrapped(msg)
2937 by Brian Murray
ubuntu-security-status: Check if ESM for Apps is enabled or if it is not
598
            if ua_attached:
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
599
                print("\nEnable ESM Apps with: ua enable esm-apps")
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
600
    if lts and not ua_attached:
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
601
        print("\nThis machine is not attached to an Ubuntu Advantage "
602
              "subscription.\nSee https://ubuntu.com/advantage")