Merge ~chad.smith/software-properties:ua-status-from-json into software-properties:ubuntu/master

Proposed by Chad Smith
Status: Merged
Merged at revision: e2e7a5a9674f70961df8ccbc17961763de44e5f6
Proposed branch: ~chad.smith/software-properties:ua-status-from-json
Merge into: software-properties:ubuntu/master
Diff against target: 141 lines (+57/-31)
2 files modified
debian/changelog (+9/-0)
softwareproperties/gtk/utils.py (+48/-31)
Reviewer Review Type Date Requested Status
Robert Ancell (community) Approve
Grant Orndorff Pending
Lucas Albuquerque Medeiros de Moura Pending
Review via email: mp+411046@code.launchpad.net

Commit message

utils: prefer ua status from status.json. Support schema 0.1 format

Adjust gtk.utils.get_ua_status to prefer reading
/var/lib/ubuntu-advantage/status.json instead of invoking
ua status on the commandline due to a network roundtrip that
is performed while running the command.

This status.json file will exist on all machines attached to an
Ubuntu Advantage subscription.

Unattached machines will persist status.json due to a systemd timer
that will sync current unattached or attached status to
/var/lib/ubuntu-advantage/status.json.

Allow get_ua_status will now also check a _schema_version key from
ua status which will log if the schema version has changed from the
expected version "0.1".

Changes in schema version may imply incompatibility with reading
UA status.

Description of the change

bumped changelog version here. not sure if it's needed or not

To post a comment you must log in.
Revision history for this message
Chad Smith (chad.smith) wrote :

Testing performed
   1. launched desktop install of Jammy daily desktop images
   2. confirmed failure path from LP: #1939732

jammy-desktop:~$ dpkg-query --show software-properties-gtk
software-properties-gtk 0.99.13
jammy-desktop:~$ software-properties-gtk
No ua status file written:
[Errno 2] No such file or directory: '/var/lib/ubuntu-advantage/status.json'
No ua status file written:
[Errno 2] No such file or directory: '/var/lib/ubuntu-advantage/status.json'
No ua status file written:
[Errno 2] No such file or directory: '/var/lib/ubuntu-advantage/status.json'

jammy-desktop:~$ sudo cp utils.py /usr/lib/python3/dist-packages/softwareproperties/gtk/utils.py

jammy-desktop:~$ dpkg-query --show software-properties-gtk

# no error on gtk dialog load on absent /var/lib/ubuntu-advantage/status.json
jammy-desktop:~$ [ -f /var/lib/ubuntu-advantage/status.json ] && echo "status.json exists"
jammy-desktop:~$ software-properties-gtk
# click "Updates" table in dialog and see "Basic Security Maintenance"

# Attach to a subscription
jammy-desktop:~$ ua attach <mytoken>

jammy-desktop:~$ software-properties-gtk
# no errors
# click "Updates" table in dialog and see "Extended Security Maintenance" with an expiry from my contract: 12/31/1999
# no errors printed to console

# editing _schema_version to print informational messages on CLI
jammy-desktop:~$ sudo sed -i 's/"_schema_version": "0.1"/"_schema_version": "0.3"/' /var/lib/ubuntu-advantage/status.json
jammy-desktop:~$ software-properties-gtk
UA status schema version change: 0.3

Revision history for this message
Chad Smith (chad.smith) wrote :
Revision history for this message
Robert Ancell (robert-ancell) wrote :

Change LGTM, thanks @chad.smith!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/changelog b/debian/changelog
2index 847c543..ccadab3 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -1,3 +1,12 @@
6+software-properties (0.99.15) jammy; urgency=medium
7+
8+ * utils: prefer /var/lib/ubuntu-advantage/status.json over ua status
9+ - Handle absent /var/lib/ubuntu-advantage/status.json for non-root
10+ users (LP: #1939732)
11+ - print unexcepted errors and if _schema_version not equal to 0.1
12+
13+ -- Chad Smith <chad.smith@canonical.com> Fri, 29 Oct 2021 15:46:58 -0600
14+
15 software-properties (0.99.14) jammy; urgency=medium
16
17 * cloudarchive: Enable support for the Yoga Ubuntu Cloud Archive on
18diff --git a/softwareproperties/gtk/utils.py b/softwareproperties/gtk/utils.py
19index d478671..522faa0 100644
20--- a/softwareproperties/gtk/utils.py
21+++ b/softwareproperties/gtk/utils.py
22@@ -26,6 +26,7 @@ import gi
23 gi.require_version("Gtk", "3.0")
24 from gi.repository import Gio, Gtk
25 import json
26+import os
27 import subprocess
28
29 import logging
30@@ -73,37 +74,52 @@ def current_distro():
31 if release.series == distro.codename:
32 return release
33
34+
35 def get_ua_status():
36 """Return a dict of all UA status information or empty dict on error."""
37- # status.json will exist on any attached system or any unattached system
38- # which has already run `ua status`. Calling ua status directly on
39- # network disconnected machines will raise a TimeoutException trying to
40- # access contracts.canonical.com/v1/resources.
41- try:
42- # Success writes UA_STATUS_JSON
43- result = subprocess.run(['ua', 'status', '--format=json'], capture_output=True)
44- except Exception as e:
45- print("Failed to call ubuntu advantage client:\n%s" % e)
46- return {}
47- if result.returncode != 0:
48- print("Ubuntu advantage client returned code %d" % result.returncode)
49+ # status.json will exist on any attached system. It will also be created
50+ # by the systemd timer ua-timer which will update UA_STATUS_JSON every 12
51+ # hours to reflect current status of UA subscription services.
52+ # Invoking `ua status` with subp will result in a network call to
53+ # contracts.canonical.com which could raise Timeouts on network limited
54+ # machines. So, prefer the status.json file when possible.
55+ status_json = ""
56+ if os.path.exists(UA_STATUS_JSON):
57+ with open(UA_STATUS_JSON) as stream:
58+ status_json = stream.read()
59+ else:
60+ try:
61+ # Success writes UA_STATUS_JSON
62+ result = subprocess.run(
63+ ['ua', 'status', '--format=json'], stdout=subprocess.PIPE
64+ )
65+ except Exception as e:
66+ print("Failed to run `ua status`:\n%s" % e)
67+ return {}
68+ if result.returncode != 0:
69+ print(
70+ "Ubuntu Advantage client returned code %d" % result.returncode
71+ )
72+ return {}
73+ status_json = result.stdout
74+ if not status_json:
75+ print(
76+ "Warning: no Ubuntu Advantage status found."
77+ " Is ubuntu-advantage-tools installed?"
78+ )
79 return {}
80-
81- try:
82- status_file = open(UA_STATUS_JSON, "r")
83- except Exception as e:
84- print("No ua status file written:\n%s" % e)
85- return {}
86-
87- with status_file as stream:
88- status_json = stream.read()
89 try:
90 status = json.loads(status_json)
91 except json.JSONDecodeError as e:
92 print("Failed to parse ubuntu advantage client JSON:\n%s" % e)
93 return {}
94+ if status.get("_schema_version", "0.1") != "0.1":
95+ print(
96+ "UA status schema version change: %s" % status["_schema_version"]
97+ )
98 return status
99
100+
101 def get_ua_service_status(service_name='esm-infra', status=None):
102 """Get service availability and status for a specific UA service.
103
104@@ -112,26 +128,27 @@ def get_ua_service_status(service_name='esm-infra', status=None):
105 - attached contract is entitled to the service
106 - unattached machine reports service "availability" as "yes"
107 :str service_status: will be one of the following:
108- - "disabled" when the service is available but not active
109+ - "disabled" when the service is available and applicable but not
110+ active
111 - "enabled" when the service is available and active
112- - "n/a" when the service is not applicable on the environment or not
113+ - "n/a" when the service is not applicable to the environment or not
114 entitled for the attached contract
115 """
116 if not status:
117 status = get_ua_status()
118+ # Assume unattached on empty status dict
119 available = False
120 service_status = "n/a"
121 for service in status.get("services", []):
122- if service.get("name") == service_name:
123- if service.get("available"): # then we are not attached
124- if service["available"] == "yes":
125- available = True
126- service_status = "disabled" # Disabled since unattached
127- else: # attached
128- available = service.get("entitled") == "yes"
129- service_status = service.get("status") # Will be enabled, disabled or n/a
130+ if service.get("name") != service_name:
131+ continue
132+ if "available" in service:
133+ available = bool("yes" == service["available"])
134+ if "status" in service:
135+ service_status = service["status"] # enabled, disabled or n/a
136 return (available, service_status)
137
138+
139 def retry(exceptions, tries=10, delay=0.1, backoff=2):
140 """
141 Retry calling the decorated function using an exponential backoff.

Subscribers

People subscribed via source and target branches