Merge ~robert-ancell/software-properties:esm-bionic into software-properties:ubuntu/bionic

Proposed by Robert Ancell
Status: Needs review
Proposed branch: ~robert-ancell/software-properties:esm-bionic
Merge into: software-properties:ubuntu/bionic
Diff against target: 356 lines (+233/-6)
4 files modified
data/gtkbuilder/main.ui (+106/-6)
debian/changelog (+6/-0)
softwareproperties/gtk/SoftwarePropertiesGtk.py (+56/-0)
softwareproperties/gtk/utils.py (+65/-0)
Reviewer Review Type Date Requested Status
Ubuntu Core Development Team Pending
Review via email: mp+400304@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Robert Ancell (robert-ancell) wrote :

Unmerged commits

b8e712d... by Robert Ancell

Show extended security maintenance status

Also worked on by Chad Smith.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/data/gtkbuilder/main.ui b/data/gtkbuilder/main.ui
2index 642e45c..7df32b9 100644
3--- a/data/gtkbuilder/main.ui
4+++ b/data/gtkbuilder/main.ui
5@@ -550,7 +550,18 @@
6 <packing>
7 <property name="expand">False</property>
8 <property name="fill">True</property>
9- <property name="position">0</property>
10+ </packing>
11+ </child>
12+ <child>
13+ <object class="GtkLabel">
14+ <property name="visible">True</property>
15+ <property name="can_focus">False</property>
16+ <property name="label" translatable="yes">Snap package updates are checked routinely and installed automatically.</property>
17+ <property name="xalign">0</property>
18+ </object>
19+ <packing>
20+ <property name="expand">False</property>
21+ <property name="fill">False</property>
22 </packing>
23 </child>
24 <child>
25@@ -565,6 +576,98 @@
26 <property name="can_focus">False</property>
27 <property name="spacing">6</property>
28 <child>
29+ <object class="GtkHBox">
30+ <property name="visible">True</property>
31+ <property name="can_focus">False</property>
32+ <property name="spacing">6</property>
33+ <child>
34+ <object class="GtkLabel" id="label_esm_heading">
35+ <property name="visible">True</property>
36+ <property name="can_focus">False</property>
37+ <property name="xalign">1</property>
38+ <property name="label" translatable="yes">For other packages, this system has:</property>
39+ </object>
40+ <packing>
41+ <property name="expand">False</property>
42+ <property name="fill">False</property>
43+ <property name="position">0</property>
44+ </packing>
45+ </child>
46+ <child>
47+ <object class="GtkHBox">
48+ <property name="visible">True</property>
49+ <property name="can_focus">False</property>
50+ <property name="spacing">6</property>
51+ <child>
52+ <object class="GtkLabel" id="label_esm_status">
53+ <property name="visible">True</property>
54+ <property name="can_focus">False</property>
55+ <property name="xalign">0</property>
56+ </object>
57+ <packing>
58+ <property name="expand">False</property>
59+ <property name="fill">True</property>
60+ </packing>
61+ </child>
62+ <child>
63+ <object class="GtkLabel" id="label_esm_subscribe">
64+ <property name="visible">True</property>
65+ <property name="can_focus">False</property>
66+ <property name="xalign">1</property>
67+ </object>
68+ <packing>
69+ <property name="expand">True</property>
70+ <property name="fill">True</property>
71+ </packing>
72+ </child>
73+ </object>
74+ <packing>
75+ <property name="expand">True</property>
76+ <property name="fill">True</property>
77+ <property name="position">1</property>
78+ </packing>
79+ </child>
80+ </object>
81+ <packing>
82+ <property name="expand">False</property>
83+ <property name="fill">False</property>
84+ </packing>
85+ </child>
86+ <child>
87+ <object class="GtkHBox">
88+ <property name="visible">True</property>
89+ <property name="can_focus">False</property>
90+ <property name="spacing">6</property>
91+ <child>
92+ <object class="GtkLabel" id="label_eol_heading">
93+ <property name="visible">True</property>
94+ <property name="can_focus">False</property>
95+ </object>
96+ <packing>
97+ <property name="expand">False</property>
98+ <property name="fill">False</property>
99+ <property name="position">0</property>
100+ </packing>
101+ </child>
102+ <child>
103+ <object class="GtkLabel" id="label_eol">
104+ <property name="visible">True</property>
105+ <property name="can_focus">False</property>
106+ <property name="xalign">0</property>
107+ </object>
108+ <packing>
109+ <property name="expand">True</property>
110+ <property name="fill">True</property>
111+ <property name="position">1</property>
112+ </packing>
113+ </child>
114+ </object>
115+ <packing>
116+ <property name="expand">False</property>
117+ <property name="fill">False</property>
118+ </packing>
119+ </child>
120+ <child>
121 <object class="GtkHBox" id="hbox_check_for_updates">
122 <property name="visible">True</property>
123 <property name="can_focus">False</property>
124@@ -604,7 +707,6 @@
125 <packing>
126 <property name="expand">False</property>
127 <property name="fill">False</property>
128- <property name="position">0</property>
129 </packing>
130 </child>
131 <child>
132@@ -647,7 +749,6 @@
133 <packing>
134 <property name="expand">False</property>
135 <property name="fill">False</property>
136- <property name="position">1</property>
137 </packing>
138 </child>
139 <child>
140@@ -690,7 +791,6 @@
141 <packing>
142 <property name="expand">False</property>
143 <property name="fill">False</property>
144- <property name="position">2</property>
145 </packing>
146 </child>
147 </object>
148@@ -699,7 +799,6 @@
149 <packing>
150 <property name="expand">True</property>
151 <property name="fill">True</property>
152- <property name="position">1</property>
153 </packing>
154 </child>
155 <child>
156@@ -749,7 +848,6 @@
157 <packing>
158 <property name="expand">False</property>
159 <property name="fill">False</property>
160- <property name="position">2</property>
161 </packing>
162 </child>
163 </object>
164@@ -1406,6 +1504,8 @@
165 </object>
166 <object class="GtkSizeGroup" id="sizegroup1">
167 <widgets>
168+ <widget name="label_esm_heading"/>
169+ <widget name="label_eol_heading"/>
170 <widget name="label3"/>
171 <widget name="label4"/>
172 <widget name="label5"/>
173diff --git a/debian/changelog b/debian/changelog
174index 0079494..463fc2c 100644
175--- a/debian/changelog
176+++ b/debian/changelog
177@@ -1,3 +1,9 @@
178+software-properties (0.96.24.32.15) UNRELEASED; urgency=medium
179+
180+ * Show ESM support status (LP: #1920836)
181+
182+ -- Robert Ancell <robert.ancell@canonical.com> Tue, 23 Mar 2021 16:44:44 +1300
183+
184 software-properties (0.96.24.32.14) bionic-security; urgency=medium
185
186 * SECURITY UPDATE: malicious repo could send ANSI sequences to terminal
187diff --git a/softwareproperties/gtk/SoftwarePropertiesGtk.py b/softwareproperties/gtk/SoftwarePropertiesGtk.py
188index 9444e55..867b7a8 100644
189--- a/softwareproperties/gtk/SoftwarePropertiesGtk.py
190+++ b/softwareproperties/gtk/SoftwarePropertiesGtk.py
191@@ -27,6 +27,7 @@ from __future__ import absolute_import, print_function
192
193 import apt
194 import apt_pkg
195+import datetime
196 import dbus
197 from gettext import gettext as _
198 import gettext
199@@ -57,6 +58,13 @@ import softwareproperties.distro
200 from softwareproperties.SoftwareProperties import SoftwareProperties
201 import softwareproperties.SoftwareProperties
202
203+from softwareproperties.gtk.utils import (
204+ get_ua_status,
205+ get_ua_service_status,
206+ current_distro,
207+ is_current_distro_lts,
208+)
209+
210 from UbuntuDrivers import detect
211
212 if GLib.pyglib_version < (3, 9, 1):
213@@ -369,6 +377,54 @@ class SoftwarePropertiesGtk(SoftwareProperties, SimpleGtkbuilderApp):
214 self.vbox_updates.add(checkbox)
215 checkbox.show()
216
217+ status = get_ua_status()
218+ if not is_current_distro_lts():
219+ esm_available = False
220+ esm_enabled = False
221+ else:
222+ (infra_available, infra_status) = get_ua_service_status("esm-infra", status=status)
223+ (apps_available, apps_status) = get_ua_service_status("esm-apps", status=status)
224+ esm_available = bool(infra_available or apps_available)
225+ esm_enabled = "enabled" in (infra_status, apps_status)
226+ distro = current_distro()
227+ if esm_enabled:
228+ eol_text = _("Extended Security Maintenance")
229+ # EOL date should probably be UA contract expiry.
230+ # This is probably sooner than ESM EOL for the distro and
231+ # gives software properties dialogs a chance to interact about
232+ # renewals if needed.
233+ try:
234+ eol_date = datetime.datetime.strptime(
235+ status.get("expires"), "%Y-%m-%dT%H:%M:%S"
236+ ).date()
237+ except ValueError:
238+ print("Unable to determine UA contract expiry")
239+ eol_date = distro.eol
240+ else:
241+ eol_text = _("Basic Security Maintenance")
242+ eol_date = distro.eol
243+ self.label_esm_status.set_markup(eol_text)
244+ esm_url = "https://ubuntu.com/esm" # Non-EOL LTS generic ESM
245+ today = datetime.datetime.now().date()
246+ if today >= eol_date:
247+ if esm_available:
248+ # EOL LTS uses release-specific ESM ubuntu.com/XX-YY
249+ distro_ver = distro.version.replace(' LTS', '')
250+ esm_url = "https://ubuntu.com/%s" % distro_ver.replace(".", "-")
251+ eol_expiry_text = _("Ended %s - extend or upgrade now") % eol_date.strftime("%x")
252+ elif today >= eol_date - datetime.timedelta(days=60):
253+ eol_expiry_text = _("Ends %s - extend or upgrade soon") % eol_date.strftime("%x")
254+ else:
255+ eol_expiry_text = _("Active until %s") % eol_date.strftime("%x")
256+ self.label_eol.set_label(eol_expiry_text)
257+ self.label_esm_subscribe.set_markup(
258+ "<a href=\"%s\">%s</a>" % (esm_url, _("Extend…"))
259+ )
260+ self.label_esm_subscribe.set_visible(
261+ esm_available and not esm_enabled
262+ )
263+ eol_expiry_text = _("Ended %s") % eol_date.strftime("%x")
264+
265 # setup the server chooser
266 cell = Gtk.CellRendererText()
267 self.combobox_server.pack_start(cell, True)
268diff --git a/softwareproperties/gtk/utils.py b/softwareproperties/gtk/utils.py
269index e0ddca9..0061785 100644
270--- a/softwareproperties/gtk/utils.py
271+++ b/softwareproperties/gtk/utils.py
272@@ -25,12 +25,16 @@ from functools import wraps
273 import gi
274 gi.require_version("Gtk", "3.0")
275 from gi.repository import Gio, Gtk
276+import json
277+import subprocess
278
279 import logging
280 LOG=logging.getLogger(__name__)
281
282 import time
283
284+UA_STATUS_JSON = "/var/lib/ubuntu-advantage/status.json"
285+
286 def setup_ui(self, path, domain):
287 # setup ui
288 self.builder = Gtk.Builder()
289@@ -61,6 +65,67 @@ def is_current_distro_supported():
290 di = distro_info.UbuntuDistroInfo()
291 return distro.codename in di.supported(datetime.now().date())
292
293+def current_distro():
294+ distro = aptsources.distro.get_distro()
295+ di = distro_info.UbuntuDistroInfo()
296+ releases = di.get_all(result="object")
297+ for release in releases:
298+ if release.series == distro.codename:
299+ return release
300+
301+def get_ua_status():
302+ """Return a dict of all UA status information or empty dict on error."""
303+ # status.json will exist on any attached system or any unattached system
304+ # which has already run `ua status`. Calling ua status directly on
305+ # network disconnected machines will raise a TimeoutException trying to
306+ # access contracts.canonical.com/v1/resources.
307+ try:
308+ # Success writes UA_STATUS_JSON
309+ result = subprocess.run(['ua', 'status', '--format=json'], stdout=subprocess.PIPE)
310+ except Exception as e:
311+ print("Failed to call ubuntu advantage client:\n%s" % e)
312+ return {}
313+ if result.returncode != 0:
314+ print("Ubuntu advantage client returned code %d" % result.returncode)
315+ return {}
316+
317+ with open(UA_STATUS_JSON, "r") as stream:
318+ status_json = stream.read()
319+ try:
320+ status = json.loads(status_json)
321+ except json.JSONDecodeError as e:
322+ print("Failed to parse ubuntu advantage client JSON:\n%s" % e)
323+ return {}
324+ return status
325+
326+def get_ua_service_status(service_name='esm-infra', status=None):
327+ """Get service availability and status for a specific UA service.
328+
329+ Return a tuple (available, service_status).
330+ :boolean available: set True when either:
331+ - attached contract is entitled to the service
332+ - unattached machine reports service "availability" as "yes"
333+ :str service_status: will be one of the following:
334+ - "disabled" when the service is available but not active
335+ - "enabled" when the service is available and active
336+ - "n/a" when the service is not applicable on the environment or not
337+ entitled for the attached contract
338+ """
339+ if not status:
340+ status = get_ua_status()
341+ available = False
342+ service_status = "n/a"
343+ for service in status.get("services", []):
344+ if service.get("name") == service_name:
345+ if service.get("available"): # then we are not attached
346+ if service["available"] == "yes":
347+ available = True
348+ service_status = "disabled" # Disabled since unattached
349+ else: # attached
350+ available = service.get("entitled") == "yes"
351+ service_status = service.get("status") # Will be enabled, disabled or n/a
352+ return (available, service_status)
353+
354 def retry(exceptions, tries=10, delay=0.1, backoff=2):
355 """
356 Retry calling the decorated function using an exponential backoff.

Subscribers

People subscribed via source and target branches