Merge lp:~azzar1/update-manager/canonical-livepatch into lp:update-manager
- canonical-livepatch
- Merge into main
Status: | Merged |
---|---|
Merged at revision: | 2788 |
Proposed branch: | lp:~azzar1/update-manager/canonical-livepatch |
Merge into: | lp:update-manager |
Diff against target: |
440 lines (+366/-2) 5 files modified
UpdateManager/Core/LivePatchSocket.py (+118/-0) UpdateManager/Dialogs.py (+46/-1) debian/control (+2/-0) tests/test_livepatch_socket.py (+200/-0) tests/test_utils.py (+0/-1) |
To merge this branch: | bzr merge lp:~azzar1/update-manager/canonical-livepatch |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brian Murray | Needs Fixing | ||
Review via email: mp+329452@code.launchpad.net |
Commit message
Add Canonical LivePatch status to update-manager.
Description of the change
Add Canonical LivePatch status to update-manager as per https:/
Andrea Azzarone (azzar1) wrote : | # |
That's a problem with the specification. With livepatch you either get all
the patches correctly applied or none.
Il 26 ago 2017 00:29, "Brian Murray" <email address hidden> ha scritto:
> Review: Needs Fixing
>
> How will these changes allow a message like "7 Livepatch updates applied
> since the last restart. 1 other update failed to apply.”, from the
> specification, to be displayed? Does set_desc allow both the failure and
> success messages to be displayed?
>
> Diff comments:
>
> >
> > === modified file 'UpdateManager/
> > --- UpdateManager/
> > +++ UpdateManager/
> > @@ -142,6 +144,45 @@
> > self.main_
> > self.main_
> >
> > + def on_livepatch_
> patch_state, fixes):
> > + self.set_desc(None)
> > +
> > + if not active:
> > + return
> > +
> > + needs_reschedule = False
> > +
> > + if check_state == "needs-check":
> > + needs_reschedule = True
> > + elif check_state == "check-failed":
> > + pass
> > + elif check_state == "checked":
> > + if patch_state == "unapplied" or patch_state == "applying":
> > + needs_reschedule = True
> > + elif patch_state == "applied":
> > + patched_fixes = [fix for fix in fixes if fix.patched]
> > + if len(patched_fixes) == 1:
> > + self.set_desc(_("1 Livepatch update applied since
> the last restart."))
> > + elif len(patched_fixes) > 1:
> > + self.set_desc(_("%d Livepatch updates applied since
> the last restart." % len(patched_
> > + elif patch_state == "applied-with-bug" or patch_state ==
> "apply-failed":
> > + patched_fixes = [fix for fix in fixes if fix.patched]
> > + if len(patched_fixes) == 1:
> > + self.set_desc(_("1 update failed to apply since
> the last restart"))
>
> The other calls to set_desc end with a period so lets have this one also
> end with a period.
>
> > + elif len(patched_fixes) > 1:
> > + self.set_desc(_("%d updates failed to apply since
> the last restart" % len(patched_
>
> The other calls to set_desc end with a period so lets have this one also
> end with a period.
>
> > + elif patch_state == "nothing-to-apply":
> > + pass
> > + elif patch_state == "unknown":
> > + pass
> > +
> > + if needs_reschedule:
> > + self.lp_
> > +
> > + def check_livepatch
> > + self.lp_socket = LivePatchSocket()
> > + self.lp_
> > +
> >
> > class StoppedUpdatesD
> > def __init__(self, window_main):
>
>
> --
> https:/
> canonical-
> You are the owner of lp:~azzar1/update-manager/canonical-livepatch.
>...
Brian Murray (brian-murray) wrote : | # |
Andrea - I'd also made a couple of in-line comments in my review. If you make those changes, I'll get this merged and uploaded.
Andrea Azzarone (azzar1) wrote : | # |
Fixed.
Brian Murray (brian-murray) wrote : | # |
As I was getting ready to upload this I discovered there are a fair number of pep8 failures with the added code.
$ xvfb-run nosetests3
.......
/home/bdmurray/
/home/bdmurray/
/home/bdmurray/
/home/bdmurray/
/home/bdmurray/
F.F....
=======
FAIL: test_pep8_clean (test_pep8.
-------
Traceback (most recent call last):
File "/home/
self.
AssertionError: 0 != 1
=======
FAIL: test_pyflakes_clean (test_pyflakes.
-------
Traceback (most recent call last):
File "/home/
self.
AssertionError: 0 != 8
-------
../tests/
../tests/
../tests/
../tests/
../UpdateManage
../UpdateManage
../UpdateManage
../UpdateManage
-------
-------
Sorry about asking for additional fixes, but could you address the ones in the code you've added? Thanks!
/home/bdmurray/
/home/bdmurray/
/home/bdmurray/
Andrea Azzarone (azzar1) wrote : | # |
Done!
Preview Diff
1 | === added file 'UpdateManager/Core/LivePatchSocket.py' |
2 | --- UpdateManager/Core/LivePatchSocket.py 1970-01-01 00:00:00 +0000 |
3 | +++ UpdateManager/Core/LivePatchSocket.py 2017-08-31 07:59:52 +0000 |
4 | @@ -0,0 +1,118 @@ |
5 | +# LivePatchSocket.py |
6 | +# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*- |
7 | +# |
8 | +# Copyright (c) 2017 Canonical |
9 | +# |
10 | +# Author: Andrea Azzarone <andrea.azzarone@canonical.com> |
11 | +# |
12 | +# This program is free software; you can redistribute it and/or |
13 | +# modify it under the terms of the GNU General Public License as |
14 | +# published by the Free Software Foundation; either version 2 of the |
15 | +# License, or (at your option) any later version. |
16 | +# |
17 | +# This program is distributed in the hope that it will be useful, |
18 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
19 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
20 | +# GNU General Public License for more details. |
21 | +# |
22 | +# You should have received a copy of the GNU General Public License |
23 | +# along with this program; if not, write to the Free Software |
24 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 |
25 | +# USA |
26 | + |
27 | +from gi.repository import GLib |
28 | +import http.client |
29 | +import socket |
30 | +import threading |
31 | +import yaml |
32 | + |
33 | +HOST_NAME = '/var/snap/canonical-livepatch/current/livepatchd.sock' |
34 | + |
35 | + |
36 | +class UHTTPConnection(http.client.HTTPConnection): |
37 | + |
38 | + def __init__(self, path): |
39 | + http.client.HTTPConnection.__init__(self, 'localhost') |
40 | + self.path = path |
41 | + |
42 | + def connect(self): |
43 | + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
44 | + sock.connect(self.path) |
45 | + self.sock = sock |
46 | + |
47 | + |
48 | +class LivePatchSocket(object): |
49 | + |
50 | + def __init__(self, http_conn=None): |
51 | + if http_conn is None: |
52 | + self.conn = UHTTPConnection(HOST_NAME) |
53 | + else: |
54 | + self.conn = http_conn |
55 | + |
56 | + def get_status(self, on_done): |
57 | + |
58 | + def do_call(): |
59 | + try: |
60 | + self.conn.request('GET', '/status?verbose=True') |
61 | + r = self.conn.getresponse() |
62 | + active = r.status == 200 |
63 | + data = yaml.safe_load(r.read()) |
64 | + except Exception as e: |
65 | + active = False |
66 | + data = dict() |
67 | + check_state = LivePatchSocket.get_check_state(data) |
68 | + patch_state = LivePatchSocket.get_patch_state(data) |
69 | + fixes = LivePatchSocket.get_fixes(data) |
70 | + GLib.idle_add(lambda: on_done( |
71 | + active, check_state, patch_state, fixes)) |
72 | + |
73 | + thread = threading.Thread(target=do_call) |
74 | + thread.start() |
75 | + |
76 | + @staticmethod |
77 | + def get_check_state(data): |
78 | + try: |
79 | + status = data['status'] |
80 | + kernel = next((k for k in status if k['running']), None) |
81 | + return kernel['livepatch']['checkState'] |
82 | + except Exception as e: |
83 | + return 'check-failed' |
84 | + |
85 | + @staticmethod |
86 | + def get_patch_state(data): |
87 | + try: |
88 | + status = data['status'] |
89 | + kernel = next((k for k in status if k['running']), None) |
90 | + return kernel['livepatch']['patchState'] |
91 | + except Exception as e: |
92 | + return 'unknown' |
93 | + |
94 | + @staticmethod |
95 | + def get_fixes(data): |
96 | + try: |
97 | + status = data['status'] |
98 | + kernel = next((k for k in status if k['running']), None) |
99 | + fixes = kernel['livepatch']['fixes'] |
100 | + return [LivePatchFix(f) |
101 | + for f in fixes.replace('* ', '').split('\n') if len(f) > 0] |
102 | + except Exception as e: |
103 | + return list() |
104 | + |
105 | + |
106 | +class LivePatchFix(object): |
107 | + |
108 | + def __init__(self, text): |
109 | + patched_pattern = ' (unpatched)' |
110 | + self.patched = text.find(patched_pattern) == -1 |
111 | + self.name = text.replace(patched_pattern, '') |
112 | + |
113 | + def __eq__(self, other): |
114 | + if isinstance(other, LivePatchFix): |
115 | + return self.name == other.name and self.patched == other.patched |
116 | + return NotImplemented |
117 | + |
118 | + def __ne__(self, other): |
119 | + result = self.__eq__(other) |
120 | + if result is NotImplemented: |
121 | + return result |
122 | + return not result |
123 | |
124 | === modified file 'UpdateManager/Dialogs.py' |
125 | --- UpdateManager/Dialogs.py 2017-08-07 20:54:58 +0000 |
126 | +++ UpdateManager/Dialogs.py 2017-08-31 07:59:52 +0000 |
127 | @@ -26,7 +26,6 @@ |
128 | gi.require_version("Gtk", "3.0") |
129 | from gi.repository import Gtk |
130 | from gi.repository import Gdk |
131 | - |
132 | import warnings |
133 | warnings.filterwarnings( |
134 | "ignore", "Accessed deprecated property", DeprecationWarning) |
135 | @@ -37,8 +36,10 @@ |
136 | import os |
137 | |
138 | import HweSupportStatus.consts |
139 | +from .Core.LivePatchSocket import LivePatchSocket |
140 | |
141 | from gettext import gettext as _ |
142 | +from gettext import ngettext |
143 | |
144 | |
145 | class Dialog(object): |
146 | @@ -142,6 +143,49 @@ |
147 | self.main_container.add(content_widget) |
148 | self.main_container.set_visible(bool(content_widget)) |
149 | |
150 | + def on_livepatch_status_ready(self, active, cs, ps, fixes): |
151 | + self.set_desc(None) |
152 | + |
153 | + if not active: |
154 | + return |
155 | + |
156 | + needs_reschedule = False |
157 | + |
158 | + if cs == "needs-check": |
159 | + needs_reschedule = True |
160 | + elif cs == "check-failed": |
161 | + pass |
162 | + elif cs == "checked": |
163 | + if ps == "unapplied" or ps == "applying": |
164 | + needs_reschedule = True |
165 | + elif ps == "applied": |
166 | + fixes = [fix for fix in fixes if fix.patched] |
167 | + d = ngettext("1 Livepatch update applied since the last " |
168 | + "restart.", |
169 | + "%d Livepatch updates applied since the last " |
170 | + "restart." % len(fixes), |
171 | + len(fixes)) |
172 | + self.set_desc(d) |
173 | + elif ps == "applied-with-bug" or ps == "apply-failed": |
174 | + fixes = [fix for fix in fixes if fix.patched] |
175 | + d = ngettext("1 Livepatch update failed to apply since the " |
176 | + "last restart.", |
177 | + "%d Livepatch updates failed to apply since the " |
178 | + "last restart." % len(fixes), |
179 | + len(fixes)) |
180 | + self.set_desc(d) |
181 | + elif ps == "nothing-to-apply": |
182 | + pass |
183 | + elif ps == "unknown": |
184 | + pass |
185 | + |
186 | + if needs_reschedule: |
187 | + self.lp_socket.get_status(self.on_livepatch_status_ready) |
188 | + |
189 | + def check_livepatch_status(self): |
190 | + self.lp_socket = LivePatchSocket() |
191 | + self.lp_socket.get_status(self.on_livepatch_status_ready) |
192 | + |
193 | |
194 | class StoppedUpdatesDialog(InternalDialog): |
195 | def __init__(self, window_main): |
196 | @@ -166,6 +210,7 @@ |
197 | self.add_settings_button() |
198 | self.focus_button = self.add_button(Gtk.STOCK_OK, |
199 | self.window_main.close) |
200 | + self.check_livepatch_status() |
201 | |
202 | |
203 | class DistUpgradeDialog(InternalDialog): |
204 | |
205 | === modified file 'debian/control' |
206 | --- debian/control 2017-07-18 22:32:49 +0000 |
207 | +++ debian/control 2017-08-31 07:59:52 +0000 |
208 | @@ -7,6 +7,7 @@ |
209 | python3-distutils-extra (>= 2.38), |
210 | python3-dbus, |
211 | python3-gi (>= 3.8), |
212 | + python3-yaml, |
213 | gir1.2-gtk-3.0, |
214 | lsb-release, |
215 | apt-clone (>= 0.2.3~ubuntu1) |
216 | @@ -59,6 +60,7 @@ |
217 | policykit-1, |
218 | python3-dbus, |
219 | python3-gi (>= 3.8), |
220 | + python3-yaml, |
221 | gir1.2-gtk-3.0, |
222 | ubuntu-release-upgrader-gtk, |
223 | update-notifier, |
224 | |
225 | === added file 'tests/test_livepatch_socket.py' |
226 | --- tests/test_livepatch_socket.py 1970-01-01 00:00:00 +0000 |
227 | +++ tests/test_livepatch_socket.py 2017-08-31 07:59:52 +0000 |
228 | @@ -0,0 +1,200 @@ |
229 | +# |
230 | +# This program is free software; you can redistribute it and/or |
231 | +# modify it under the terms of the GNU General Public License as |
232 | +# published by the Free Software Foundation; either version 2 of the |
233 | +# License, or (at your option) any later version. |
234 | +# |
235 | +# This program is distributed in the hope that it will be useful, |
236 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
237 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
238 | +# GNU General Public License for more details. |
239 | +# |
240 | +# You should have received a copy of the GNU General Public License |
241 | +# along with this program; if not, write to the Free Software |
242 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 |
243 | +# USA |
244 | + |
245 | +import datetime |
246 | +from gi.repository import GLib |
247 | +import http.client |
248 | +from mock import Mock |
249 | +import logging |
250 | +import sys |
251 | +import unittest |
252 | +import yaml |
253 | + |
254 | +from UpdateManager.Core.LivePatchSocket import LivePatchSocket, LivePatchFix |
255 | + |
256 | + |
257 | +status0 = {'architecture': 'x86_64', |
258 | + 'boot-time': datetime.datetime(2017, 6, 27, 11, 16), |
259 | + 'client-version': '7.21', |
260 | + 'cpu-model': 'Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz', |
261 | + 'last-check': datetime.datetime(2017, 6, 28, 14, 23, 29, 683361), |
262 | + 'machine-id': 123456789, |
263 | + 'machine-token': 987654321, |
264 | + 'uptime': '27h12m12s'} |
265 | + |
266 | +status1 = {'architecture': 'x86_64', |
267 | + 'boot-time': datetime.datetime(2017, 6, 27, 11, 16), |
268 | + 'client-version': '7.21', |
269 | + 'cpu-model': 'Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz', |
270 | + 'last-check': datetime.datetime(2017, 6, 28, 14, 23, 29, 683361), |
271 | + 'machine-id': 123456789, |
272 | + 'machine-token': 987654321, |
273 | + 'status': [{'kernel': '4.4.0-78.99-generic', |
274 | + 'livepatch': {'checkState': 'needs-check', |
275 | + 'fixes': '', |
276 | + 'patchState': 'nothing-to-apply', |
277 | + 'version': '24.2'}, |
278 | + 'running': True}], |
279 | + 'uptime': '27h12m12s'} |
280 | + |
281 | +status2 = {'architecture': 'x86_64', |
282 | + 'boot-time': datetime.datetime(2017, 6, 27, 11, 16), |
283 | + 'client-version': '7.21', |
284 | + 'cpu-model': 'Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz', |
285 | + 'last-check': datetime.datetime(2017, 6, 28, 14, 23, 29, 683361), |
286 | + 'machine-id': 123456789, |
287 | + 'machine-token': 987654321, |
288 | + 'status': [{'kernel': '4.4.0-78.99-generic', |
289 | + 'livepatch': {'checkState': 'checked', |
290 | + 'fixes': '* CVE-2016-0001\n' |
291 | + '* CVE-2016-0002\n' |
292 | + '* CVE-2017-0001 (unpatched)\n' |
293 | + '* CVE-2017-0001', |
294 | + 'patchState': 'applied', |
295 | + 'version': '24.2'}, |
296 | + 'running': True}], |
297 | + 'uptime': '27h12m12s'} |
298 | + |
299 | + |
300 | +class TestUtils(object): |
301 | + |
302 | + @staticmethod |
303 | + def __TimeoutCallback(user_data=None): |
304 | + user_data[0] = True |
305 | + return False |
306 | + |
307 | + @staticmethod |
308 | + def __ScheduleTimeout(timeout_reached, timeout_duration=10): |
309 | + return GLib.timeout_add(timeout_duration, |
310 | + TestUtils.__TimeoutCallback, |
311 | + timeout_reached) |
312 | + |
313 | + @staticmethod |
314 | + def WaitUntilMSec(instance, check_function, expected_result=True, |
315 | + max_wait=500, error_msg=''): |
316 | + instance.assertIsNotNone(check_function) |
317 | + |
318 | + timeout_reached = [False] |
319 | + timeout_id = TestUtils.__ScheduleTimeout(timeout_reached, max_wait) |
320 | + |
321 | + result = None |
322 | + while not timeout_reached[0]: |
323 | + result = check_function() |
324 | + if result == expected_result: |
325 | + break |
326 | + GLib.MainContext.default().iteration(True) |
327 | + |
328 | + if result == expected_result: |
329 | + GLib.Source.remove(timeout_id) |
330 | + |
331 | + instance.assertEqual(expected_result, result, error_msg) |
332 | + |
333 | + |
334 | +class MockResponse(): |
335 | + |
336 | + def __init__(self, status, data): |
337 | + self.status = status |
338 | + self.data = data |
339 | + |
340 | + def read(self): |
341 | + return yaml.dump(self.data) |
342 | + |
343 | + |
344 | +class TestLivePatchSocket(unittest.TestCase): |
345 | + |
346 | + def test_get_check_state(self): |
347 | + check_state = LivePatchSocket.get_check_state(status0) |
348 | + self.assertEqual(check_state, 'check-failed') |
349 | + check_state = LivePatchSocket.get_check_state(status1) |
350 | + self.assertEqual(check_state, 'needs-check') |
351 | + check_state = LivePatchSocket.get_check_state(status2) |
352 | + self.assertEqual(check_state, 'checked') |
353 | + |
354 | + def test_get_patch_state(self): |
355 | + patch_state = LivePatchSocket.get_patch_state(status0) |
356 | + self.assertEqual(patch_state, 'unknown') |
357 | + patch_state = LivePatchSocket.get_patch_state(status1) |
358 | + self.assertEqual(patch_state, 'nothing-to-apply') |
359 | + patch_state = LivePatchSocket.get_patch_state(status2) |
360 | + self.assertEqual(patch_state, 'applied') |
361 | + |
362 | + def test_get_fixes(self): |
363 | + fixes = LivePatchSocket.get_fixes(status0) |
364 | + self.assertEqual(fixes, []) |
365 | + fixes = LivePatchSocket.get_fixes(status1) |
366 | + self.assertEqual(fixes, []) |
367 | + fixes = LivePatchSocket.get_fixes(status2) |
368 | + self.assertEqual(fixes, [LivePatchFix('CVE-2016-0001'), |
369 | + LivePatchFix('CVE-2016-0002'), |
370 | + LivePatchFix('CVE-2017-0001 (unpatched)'), |
371 | + LivePatchFix('CVE-2017-0001')]) |
372 | + |
373 | + def test_livepatch_fix(self): |
374 | + fix = LivePatchFix('CVE-2016-0001') |
375 | + self.assertEqual(fix.name, 'CVE-2016-0001') |
376 | + self.assertTrue(fix.patched) |
377 | + |
378 | + fix = LivePatchFix('CVE-2016-0001 (unpatched)') |
379 | + self.assertEqual(fix.name, 'CVE-2016-0001') |
380 | + self.assertFalse(fix.patched) |
381 | + |
382 | + def test_callback_not_active(self): |
383 | + mock_http_conn = Mock(spec=http.client.HTTPConnection) |
384 | + attrs = {'getresponse.return_value': MockResponse(400, None)} |
385 | + mock_http_conn.configure_mock(**attrs) |
386 | + |
387 | + cb_called = [False] |
388 | + |
389 | + def on_done(active, check_state, patch_state, fixes): |
390 | + cb_called[0] = True |
391 | + self.assertFalse(active) |
392 | + |
393 | + lp = LivePatchSocket(mock_http_conn) |
394 | + lp.get_status(on_done) |
395 | + |
396 | + mock_http_conn.request.assert_called_with( |
397 | + 'GET', '/status?verbose=True') |
398 | + TestUtils.WaitUntilMSec(self, lambda: cb_called[0] is True) |
399 | + |
400 | + def test_callback_active(self): |
401 | + mock_http_conn = Mock(spec=http.client.HTTPConnection) |
402 | + attrs = {'getresponse.return_value': MockResponse(200, status2)} |
403 | + mock_http_conn.configure_mock(**attrs) |
404 | + |
405 | + cb_called = [False] |
406 | + |
407 | + def on_done(active, check_state, patch_state, fixes): |
408 | + cb_called[0] = True |
409 | + self.assertTrue(active) |
410 | + self.assertEqual(check_state, 'checked') |
411 | + self.assertEqual(patch_state, 'applied') |
412 | + self.assertEqual(fixes, [LivePatchFix('CVE-2016-0001'), |
413 | + LivePatchFix('CVE-2016-0002'), |
414 | + LivePatchFix('CVE-2017-0001 (unpatched)'), |
415 | + LivePatchFix('CVE-2017-0001')]) |
416 | + |
417 | + lp = LivePatchSocket(mock_http_conn) |
418 | + lp.get_status(on_done) |
419 | + |
420 | + mock_http_conn.request.assert_called_with( |
421 | + 'GET', '/status?verbose=True') |
422 | + TestUtils.WaitUntilMSec(self, lambda: cb_called[0] is True) |
423 | + |
424 | + |
425 | +if __name__ == '__main__': |
426 | + if len(sys.argv) > 1 and sys.argv[1] == "-v": |
427 | + logging.basicConfig(level=logging.DEBUG) |
428 | + unittest.main() |
429 | |
430 | === modified file 'tests/test_utils.py' |
431 | --- tests/test_utils.py 2017-08-09 23:21:06 +0000 |
432 | +++ tests/test_utils.py 2017-08-31 07:59:52 +0000 |
433 | @@ -2,7 +2,6 @@ |
434 | # -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*- |
435 | |
436 | import logging |
437 | -import glob |
438 | import mock |
439 | import sys |
440 | import unittest |
How will these changes allow a message like "7 Livepatch updates applied since the last restart. 1 other update failed to apply.”, from the specification, to be displayed? Does set_desc allow both the failure and success messages to be displayed?