Merge lp:~tribaal/landscape-client/more-information-on-ssl-errors into lp:~landscape/landscape-client/trunk

Proposed by Chris Glass
Status: Merged
Approved by: Chris Glass
Approved revision: 811
Merged at revision: 807
Proposed branch: lp:~tribaal/landscape-client/more-information-on-ssl-errors
Merge into: lp:~landscape/landscape-client/trunk
Diff against target: 291 lines (+141/-19)
8 files modified
landscape/broker/amp.py (+2/-1)
landscape/broker/exchange.py (+13/-2)
landscape/broker/server.py (+2/-2)
landscape/broker/tests/test_amp.py (+1/-1)
landscape/broker/tests/test_exchange.py (+39/-2)
landscape/broker/tests/test_server.py (+18/-4)
landscape/configuration.py (+14/-6)
landscape/tests/test_configuration.py (+52/-1)
To merge this branch: bzr merge lp:~tribaal/landscape-client/more-information-on-ssl-errors
Reviewer Review Type Date Requested Status
Данило Шеган (community) Approve
Free Ekanayaka (community) Approve
Review via email: mp+246090@code.launchpad.net

Commit message

Add a specific error message when the cause of registration failure is an SSL error.

Description of the change

This branch adds a specific error message when the cause of registration failure is an SSL error, since it's a common pitfall for new users, this makes the failure more explicit.

Hopefully, this should put users on the right track when try to report/fix the problem.

It's implemented by simply firing a custom message when the SSL failure is detected, and making the GUI react to this message by printing a more specific error message in this particular case.

How to test:
- Build this branche's packages with "make package", install them on a machine
- Try to register against LDS without specifying an SSL certificate. You should see the new message.
- Try to register against hosted, specifying the LDS's instance certificate. You should see the new message as well.
- Registering using a valid certificate (LDS) or no certificate (hosted) should work.

To post a comment you must log in.
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Looks good, but I'd suggest to not introduce a new event (see below). +1 with that addressed

review: Approve
Revision history for this message
Chris Glass (tribaal) :
809. By Chris Glass

Changed code to use a parameter to the exchange-failed message as per code review.

It seems the mocking infrastructure is not able to handle parameters, however,
a test is still failing.

810. By Chris Glass

Fixed parameter name. Test still fails though.

Revision history for this message
Free Ekanayaka (free.ekanayaka) :
Revision history for this message
Free Ekanayaka (free.ekanayaka) wrote :

Please also file a bug for this MP.

Revision history for this message
Данило Шеган (danilo) wrote :

Free's test fixes from https://pastebin.canonical.com/123160/ do indeed fix tests for me for all but

landscape.broker.tests.test_amp.RemoteBrokerTest.test_listen_events

which fails with https://pastebin.canonical.com/123161/

Revision history for this message
Данило Шеган (danilo) wrote :

FWIW, while testing this, landscape-config fails nicely with this error message (yet it still hangs for a while [like 20-30s] after that), yet landscape-client-install-ui just hangs for a while without any information on what's going on and then just exits. I'd lean on that being fixed in a new branch, though.

Otherwise, the code looks good.

review: Approve
811. By Chris Glass

Added free's fix for the AMQ handling of kwargs.
Small test fix on top.

Revision history for this message
Chris Glass (tribaal) wrote :

Thanks for the help Free!

@Danilo: I'll open a bug for the UI part since non-SSL failures have the same problem, AFAICT.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'landscape/broker/amp.py'
2--- landscape/broker/amp.py 2013-07-11 08:40:49 +0000
3+++ landscape/broker/amp.py 2015-01-12 12:10:59 +0000
4@@ -29,7 +29,8 @@
5 callable will be fired.
6 """
7 result = self.listen_events(handlers.keys())
8- return result.addCallback(lambda event_type: handlers[event_type]())
9+ return result.addCallback(
10+ lambda (event_type, kwargs): handlers[event_type](**kwargs))
11
12
13 class FakeRemoteBroker(object):
14
15=== modified file 'landscape/broker/exchange.py'
16--- landscape/broker/exchange.py 2014-10-03 10:51:12 +0000
17+++ landscape/broker/exchange.py 2015-01-12 12:10:59 +0000
18@@ -348,7 +348,7 @@
19
20 from twisted.internet.defer import Deferred, succeed
21
22-from landscape.lib.fetch import HTTPCodeError
23+from landscape.lib.fetch import HTTPCodeError, PyCurlError
24 from landscape.lib.message import got_next_expected, ANCIENT
25 from landscape.lib.versioning import is_version_higher, sort_versions
26 from landscape.log import format_delta
27@@ -573,6 +573,7 @@
28
29 def handle_failure(error_class, error, traceback):
30 self._exchanging = False
31+
32 if isinstance(error, HTTPCodeError) and error.http_code == 404:
33 # If we got a 404 HTTP error it could be that we're trying to
34 # speak a server API version that the server does not support,
35@@ -583,7 +584,17 @@
36 self._message_store.set_server_api(DEFAULT_SERVER_API)
37 self.exchange()
38 return
39- self._reactor.fire("exchange-failed")
40+
41+ ssl_error = False
42+ if isinstance(error, PyCurlError) and error.error_code == 60:
43+ # The error returned is an SSL error, most likely the server
44+ # is using a self-signed certificate. Let's fire a special
45+ # event so that the GUI can display a nice message.
46+ logging.error("Message exchange failed: %s" % error.message)
47+ ssl_error = True
48+
49+ self._reactor.fire("exchange-failed", ssl_error=ssl_error)
50+
51 self._message_store.record_failure(int(self._reactor.time()))
52 logging.info("Message exchange failed.")
53 exchange_completed()
54
55=== modified file 'landscape/broker/server.py'
56--- landscape/broker/server.py 2014-11-06 07:42:13 +0000
57+++ landscape/broker/server.py 2015-01-12 12:10:59 +0000
58@@ -312,10 +312,10 @@
59
60 def get_handler(event_type):
61
62- def handler():
63+ def handler(**kwargs):
64 for call in calls:
65 self._reactor.cancel_call(call)
66- deferred.callback(event_type)
67+ deferred.callback((event_type, kwargs))
68
69 return handler
70
71
72=== modified file 'landscape/broker/tests/test_amp.py'
73--- landscape/broker/tests/test_amp.py 2013-05-07 21:29:44 +0000
74+++ landscape/broker/tests/test_amp.py 2015-01-12 12:10:59 +0000
75@@ -171,7 +171,7 @@
76 self.reactor.call_later(0.05, self.reactor.fire, "event2")
77 self.reactor.advance(0.05)
78 self.remote._factory.fake_connection.flush()
79- self.assertEqual("event2", self.successResultOf(deferred))
80+ self.assertEqual(("event2", {}), self.successResultOf(deferred))
81
82 def test_call_on_events(self):
83 """
84
85=== modified file 'landscape/broker/tests/test_exchange.py'
86--- landscape/broker/tests/test_exchange.py 2014-10-03 10:06:24 +0000
87+++ landscape/broker/tests/test_exchange.py 2015-01-12 12:10:59 +0000
88@@ -1,6 +1,6 @@
89 from landscape import CLIENT_API
90 from landscape.lib.persist import Persist
91-from landscape.lib.fetch import HTTPCodeError
92+from landscape.lib.fetch import HTTPCodeError, PyCurlError
93 from landscape.lib.hashlib import md5
94 from landscape.schema import Message, Int
95 from landscape.broker.config import BrokerConfiguration
96@@ -993,7 +993,7 @@
97 """
98 events = []
99
100- def failed_exchange():
101+ def failed_exchange(ssl_error=False):
102 events.append(None)
103
104 self.reactor.call_on("exchange-failed", failed_exchange)
105@@ -1001,6 +1001,43 @@
106 self.exchanger.exchange()
107 self.assertEqual([None], events)
108
109+ def test_SSL_error_exchanging_causes_failed_exchange(self):
110+ """
111+ If an SSL error occurs when exchanging, the 'exchange-failed'
112+ event should be fired with the optional "ssl_error" flag set to True.
113+ """
114+ self.log_helper.ignore_errors("Message exchange failed: Failed to "
115+ "communicate.")
116+ events = []
117+
118+ def failed_exchange(ssl_error):
119+ if ssl_error:
120+ events.append(None)
121+
122+ self.reactor.call_on("exchange-failed", failed_exchange)
123+ self.transport.responses.append(PyCurlError(60,
124+ "Failed to communicate."))
125+ self.exchanger.exchange()
126+ self.assertEqual([None], events)
127+
128+ def test_pycurl_error_exchanging_causes_failed_exchange(self):
129+ """
130+ If an undefined PyCurl error is raised during exchange, (not an SSL
131+ error), the 'exchange-failed' event should be fired with the ssl_error
132+ flag set to False.
133+ """
134+ events = []
135+
136+ def failed_exchange(ssl_error):
137+ if not ssl_error:
138+ events.append(None)
139+
140+ self.reactor.call_on("exchange-failed", failed_exchange)
141+ self.transport.responses.append(PyCurlError(10, # Not 60
142+ "Failed to communicate."))
143+ self.exchanger.exchange()
144+ self.assertEqual([None], events)
145+
146 def test_wb_error_exchanging_records_failure_in_message_store(self):
147 """
148 If a traceback occurs whilst exchanging, the failure is recorded
149
150=== modified file 'landscape/broker/tests/test_server.py'
151--- landscape/broker/tests/test_server.py 2014-11-05 13:14:55 +0000
152+++ landscape/broker/tests/test_server.py 2015-01-12 12:10:59 +0000
153@@ -348,19 +348,33 @@
154 The L{BrokerServer.listen_events} method returns a deferred which is
155 fired when the first of the given events occurs.
156 """
157- result = self.broker.listen_events(["event1", "event2"])
158+ deferred = self.broker.listen_events(["event1", "event2"])
159 self.reactor.fire("event2")
160- return self.assertSuccess(result, "event2")
161+ result = self.successResultOf(deferred)
162+ self.assertIsNotNone(result)
163+
164+ def test_listen_events_with_payload(self):
165+ """
166+ The L{BrokerServer.listen_events} method returns a deferred which is
167+ fired when the first of the given events occurs. The result of the
168+ deferred is a 2-tuple with name of the event and any keyword arguments
169+ passed when the event was fired.
170+ """
171+ deferred = self.broker.listen_events(["event1", "event2"])
172+ self.reactor.fire("event2", foo=123)
173+ result = self.successResultOf(deferred)
174+ self.assertEqual(("event2", {"foo": 123}), result)
175
176 def test_listen_event_only_once(self):
177 """
178 The L{BrokerServer.listen_events} listens only to one occurrence of
179 the given events.
180 """
181- result = self.broker.listen_events(["event"])
182+ deferred = self.broker.listen_events(["event"])
183 self.assertEqual(self.reactor.fire("event"), [None])
184 self.assertEqual(self.reactor.fire("event"), [])
185- return self.assertSuccess(result, "event")
186+ result = self.successResultOf(deferred)
187+ self.assertEqual("event", result[0])
188
189 def test_listen_events_call_cancellation(self):
190 """
191
192=== modified file 'landscape/configuration.py'
193--- landscape/configuration.py 2014-10-03 10:06:24 +0000
194+++ landscape/configuration.py 2015-01-12 12:10:59 +0000
195@@ -626,12 +626,20 @@
196 def success():
197 on_message("System successfully registered.")
198
199- def exchange_failure():
200- on_message("We were unable to contact the server. "
201- "Your internet connection may be down. "
202- "The landscape client will continue to try and contact "
203- "the server periodically.",
204- error=True)
205+ def exchange_failure(ssl_error=False):
206+ if ssl_error:
207+ message = ("\nThe server's SSL information is incorrect, or fails "
208+ "signature verification!\n"
209+ "If the server is using a self-signed certificate, "
210+ "please ensure you supply it with the --ssl-public-key "
211+ "parameter.")
212+ else:
213+ message = ("\nWe were unable to contact the server.\n"
214+ "Your internet connection may be down. "
215+ "The landscape client will continue to try and contact "
216+ "the server periodically.")
217+
218+ on_message(message, error=True)
219 return 2
220
221 def handle_registration_errors(failure):
222
223=== modified file 'landscape/tests/test_configuration.py'
224--- landscape/tests/test_configuration.py 2014-10-03 10:06:24 +0000
225+++ landscape/tests/test_configuration.py 2015-01-12 12:10:59 +0000
226@@ -1805,7 +1805,7 @@
227 self.mocker.call(register_done)
228
229 # The deferred errback finally prints out this message.
230- print_text_mock("We were unable to contact the server. "
231+ print_text_mock("\nWe were unable to contact the server.\n"
232 "Your internet connection may be down. "
233 "The landscape client will continue to try and "
234 "contact the server periodically.",
235@@ -1828,6 +1828,57 @@
236 register(self.config, print_text, exit.append, reactor=FakeReactor())
237 self.assertEqual([2], exit)
238
239+ def test_register_exchange_SSL_failure(self):
240+ """
241+ When registration fails because the server's SSL certificate could not
242+ be validated, a message is printed and the program quits.
243+ """
244+ service = self.broker_service
245+
246+ registration_mock = self.mocker.replace(service.registration)
247+ print_text_mock = self.mocker.replace(print_text)
248+ reactor_mock = self.mocker.patch(FakeReactor)
249+
250+ # This must necessarily happen in the following order.
251+ self.mocker.order()
252+
253+ # This very informative message is printed out.
254+ print_text_mock("Please wait... ", "")
255+
256+ time_mock = self.mocker.replace("time")
257+ time_mock.sleep(ANY)
258+ self.mocker.count(1)
259+
260+ def register_done():
261+ service.reactor.fire("exchange-failed", ssl_error=True)
262+ registration_mock.register()
263+ self.mocker.call(register_done)
264+
265+ # The deferred errback finally prints out this message.
266+ print_text_mock("\nThe server's SSL information is incorrect, or "
267+ "fails signature verification!\n"
268+ "If the server is using a self-signed certificate, "
269+ "please ensure you supply it with the "
270+ "--ssl-public-key parameter.",
271+ error=True)
272+
273+ reactor_mock.stop()
274+
275+ # This is actually called after everything else since all deferreds
276+ # are synchronous and callbacks will be executed immediately.
277+ reactor_mock.run()
278+
279+ # Nothing else is printed!
280+ print_text_mock(ANY)
281+ self.mocker.count(0)
282+
283+ self.mocker.replay()
284+
285+ # DO IT!
286+ exit = []
287+ register(self.config, print_text, exit.append, reactor=FakeReactor())
288+ self.assertEqual([2], exit)
289+
290 def test_register_timeout_failure(self):
291 service = self.broker_service
292

Subscribers

People subscribed via source and target branches

to all changes: