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
diff --git a/debian/changelog b/debian/changelog
index 847c543..ccadab3 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,12 @@
1software-properties (0.99.15) jammy; urgency=medium
2
3 * utils: prefer /var/lib/ubuntu-advantage/status.json over ua status
4 - Handle absent /var/lib/ubuntu-advantage/status.json for non-root
5 users (LP: #1939732)
6 - print unexcepted errors and if _schema_version not equal to 0.1
7
8 -- Chad Smith <chad.smith@canonical.com> Fri, 29 Oct 2021 15:46:58 -0600
9
1software-properties (0.99.14) jammy; urgency=medium10software-properties (0.99.14) jammy; urgency=medium
211
3 * cloudarchive: Enable support for the Yoga Ubuntu Cloud Archive on12 * cloudarchive: Enable support for the Yoga Ubuntu Cloud Archive on
diff --git a/softwareproperties/gtk/utils.py b/softwareproperties/gtk/utils.py
index d478671..522faa0 100644
--- a/softwareproperties/gtk/utils.py
+++ b/softwareproperties/gtk/utils.py
@@ -26,6 +26,7 @@ import gi
26gi.require_version("Gtk", "3.0")26gi.require_version("Gtk", "3.0")
27from gi.repository import Gio, Gtk27from gi.repository import Gio, Gtk
28import json28import json
29import os
29import subprocess30import subprocess
3031
31import logging32import logging
@@ -73,37 +74,52 @@ def current_distro():
73 if release.series == distro.codename:74 if release.series == distro.codename:
74 return release75 return release
7576
77
76def get_ua_status():78def get_ua_status():
77 """Return a dict of all UA status information or empty dict on error."""79 """Return a dict of all UA status information or empty dict on error."""
78 # status.json will exist on any attached system or any unattached system80 # status.json will exist on any attached system. It will also be created
79 # which has already run `ua status`. Calling ua status directly on81 # by the systemd timer ua-timer which will update UA_STATUS_JSON every 12
80 # network disconnected machines will raise a TimeoutException trying to82 # hours to reflect current status of UA subscription services.
81 # access contracts.canonical.com/v1/resources.83 # Invoking `ua status` with subp will result in a network call to
82 try:84 # contracts.canonical.com which could raise Timeouts on network limited
83 # Success writes UA_STATUS_JSON85 # machines. So, prefer the status.json file when possible.
84 result = subprocess.run(['ua', 'status', '--format=json'], capture_output=True)86 status_json = ""
85 except Exception as e:87 if os.path.exists(UA_STATUS_JSON):
86 print("Failed to call ubuntu advantage client:\n%s" % e)88 with open(UA_STATUS_JSON) as stream:
87 return {}89 status_json = stream.read()
88 if result.returncode != 0:90 else:
89 print("Ubuntu advantage client returned code %d" % result.returncode)91 try:
92 # Success writes UA_STATUS_JSON
93 result = subprocess.run(
94 ['ua', 'status', '--format=json'], stdout=subprocess.PIPE
95 )
96 except Exception as e:
97 print("Failed to run `ua status`:\n%s" % e)
98 return {}
99 if result.returncode != 0:
100 print(
101 "Ubuntu Advantage client returned code %d" % result.returncode
102 )
103 return {}
104 status_json = result.stdout
105 if not status_json:
106 print(
107 "Warning: no Ubuntu Advantage status found."
108 " Is ubuntu-advantage-tools installed?"
109 )
90 return {}110 return {}
91
92 try:
93 status_file = open(UA_STATUS_JSON, "r")
94 except Exception as e:
95 print("No ua status file written:\n%s" % e)
96 return {}
97
98 with status_file as stream:
99 status_json = stream.read()
100 try:111 try:
101 status = json.loads(status_json)112 status = json.loads(status_json)
102 except json.JSONDecodeError as e:113 except json.JSONDecodeError as e:
103 print("Failed to parse ubuntu advantage client JSON:\n%s" % e)114 print("Failed to parse ubuntu advantage client JSON:\n%s" % e)
104 return {}115 return {}
116 if status.get("_schema_version", "0.1") != "0.1":
117 print(
118 "UA status schema version change: %s" % status["_schema_version"]
119 )
105 return status120 return status
106121
122
107def get_ua_service_status(service_name='esm-infra', status=None):123def get_ua_service_status(service_name='esm-infra', status=None):
108 """Get service availability and status for a specific UA service.124 """Get service availability and status for a specific UA service.
109125
@@ -112,26 +128,27 @@ def get_ua_service_status(service_name='esm-infra', status=None):
112 - attached contract is entitled to the service128 - attached contract is entitled to the service
113 - unattached machine reports service "availability" as "yes"129 - unattached machine reports service "availability" as "yes"
114 :str service_status: will be one of the following:130 :str service_status: will be one of the following:
115 - "disabled" when the service is available but not active131 - "disabled" when the service is available and applicable but not
132 active
116 - "enabled" when the service is available and active133 - "enabled" when the service is available and active
117 - "n/a" when the service is not applicable on the environment or not134 - "n/a" when the service is not applicable to the environment or not
118 entitled for the attached contract135 entitled for the attached contract
119 """136 """
120 if not status:137 if not status:
121 status = get_ua_status()138 status = get_ua_status()
139 # Assume unattached on empty status dict
122 available = False140 available = False
123 service_status = "n/a"141 service_status = "n/a"
124 for service in status.get("services", []):142 for service in status.get("services", []):
125 if service.get("name") == service_name:143 if service.get("name") != service_name:
126 if service.get("available"): # then we are not attached144 continue
127 if service["available"] == "yes":145 if "available" in service:
128 available = True146 available = bool("yes" == service["available"])
129 service_status = "disabled" # Disabled since unattached147 if "status" in service:
130 else: # attached148 service_status = service["status"] # enabled, disabled or n/a
131 available = service.get("entitled") == "yes"
132 service_status = service.get("status") # Will be enabled, disabled or n/a
133 return (available, service_status)149 return (available, service_status)
134150
151
135def retry(exceptions, tries=10, delay=0.1, backoff=2):152def retry(exceptions, tries=10, delay=0.1, backoff=2):
136 """153 """
137 Retry calling the decorated function using an exponential backoff.154 Retry calling the decorated function using an exponential backoff.

Subscribers

People subscribed via source and target branches