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

Proposed by Chad Smith
Status: Merged
Merged at revision: cf0e4d7a9c528fb90cd7b8596e94e17bd5d3eac4
Proposed branch: ~chad.smith/software-properties:ua-status-from-json-impish
Merge into: software-properties:ubuntu/impish
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
Corey Bryant Approve
Grant Orndorff Pending
Lucas Albuquerque Medeiros de Moura Pending
Robert Ancell Pending
Review via email: mp+411092@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.

To post a comment you must log in.
Revision history for this message
Corey Bryant (corey.bryant) wrote :

I think this is safe to merge without Robert's approval. Robert approved the jammy and hirsute changes, which are the same.

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 0cf9bd5..2487bde 100644
3--- a/debian/changelog
4+++ b/debian/changelog
5@@ -1,3 +1,12 @@
6+software-properties (0.99.13.1) impish; 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> Mon, 01 Nov 2021 11:42:27 -0600
14+
15 software-properties (0.99.13) impish; urgency=medium
16
17 * Catch exception if UA status file is not written
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