Merge lp:~tealeg/landscape-client/invoke-registration-from-settings-ui into lp:~landscape/landscape-client/trunk
- invoke-registration-from-settings-ui
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Fernando Correa Neto |
Approved revision: | 476 |
Merged at revision: | 426 |
Proposed branch: | lp:~tealeg/landscape-client/invoke-registration-from-settings-ui |
Merge into: | lp:~landscape/landscape-client/trunk |
Diff against target: |
1022 lines (+552/-63) 11 files modified
landscape/configuration.py (+80/-39) landscape/tests/test_configuration.py (+9/-8) landscape/ui/controller/configuration.py (+67/-8) landscape/ui/controller/tests/test_app.py (+4/-5) landscape/ui/controller/tests/test_configuration.py (+16/-1) landscape/ui/model/registration.py (+87/-0) landscape/ui/model/tests/test_registration.py (+80/-0) landscape/ui/view/configuration.py (+79/-1) landscape/ui/view/tests/test_configuration.py (+28/-1) landscape/ui/view/ui/landscape-client-settings.glade (+96/-0) setup.py (+6/-0) |
To merge this branch: | bzr merge lp:~tealeg/landscape-client/invoke-registration-from-settings-ui |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Fernando Correa Neto (community) | Approve | ||
Free Ekanayaka (community) | Approve | ||
Review via email: mp+88187@code.launchpad.net |
Commit message
Description of the change
Addresses bug #911666
Adds UI and mechanism for invoking landscape-config --silent with the config file generated by the UI.
Note, this will not work with the default values in landscape-
Geoff Teale (tealeg) wrote : | # |
I've addressed Free's issues from above and repushed.
- 469. By Geoff Teale
-
Add docstrings and defaults as per Free's review suggestion
- 470. By Geoff Teale
-
Change names of first class function parameters in accordance with comments from Free's review
- 471. By Geoff Teale
-
Change variable names fun -> function in accordance with comments from Free's review
- 472. By Geoff Teale
-
Restructured notification tests to use local scope instead of object scope (from Free's review)
- 473. By Geoff Teale
-
Rename method default_machine -> default_
computer_ title (From Free's review) - 474. By Geoff Teale
-
Monkey patch ConfigControlle
r.getfqdn instead of socket.getfqdn (From Free's review) - 475. By Geoff Teale
-
Fix lint issues
- 476. By Geoff Teale
-
Added UI packages to setup.py as per a request from Andreas
Fernando Correa Neto (fcorrea) wrote : | # |
Looks great! +1
Preview Diff
1 | === modified file 'landscape/configuration.py' |
2 | --- landscape/configuration.py 2012-01-05 10:42:54 +0000 |
3 | +++ landscape/configuration.py 2012-01-12 15:18:24 +0000 |
4 | @@ -423,60 +423,101 @@ |
5 | |
6 | |
7 | def setup_init_script_and_start_client(): |
8 | + "Configure the init script to start the client on boot." |
9 | # XXX This function is misnamed; it doesn't start the client. |
10 | sysvconfig = SysVConfig() |
11 | sysvconfig.set_start_on_boot(True) |
12 | |
13 | |
14 | def stop_client_and_disable_init_script(): |
15 | + """ |
16 | + Stop landscape-client and change configuration to prevent starting |
17 | + landscape-client on boot. |
18 | + """ |
19 | sysvconfig = SysVConfig() |
20 | sysvconfig.stop_landscape() |
21 | sysvconfig.set_start_on_boot(False) |
22 | |
23 | |
24 | -def setup(config): |
25 | - sysvconfig = SysVConfig() |
26 | - if not config.no_start: |
27 | - if config.silent: |
28 | - setup_init_script_and_start_client() |
29 | - elif not sysvconfig.is_configured_to_run(): |
30 | - answer = raw_input("\nThe Landscape client must be started " |
31 | - "on boot to operate correctly.\n\n" |
32 | - "Start Landscape client on boot? (Y/n): ") |
33 | - if not answer.upper().startswith("N"): |
34 | - setup_init_script_and_start_client() |
35 | - else: |
36 | - sys.exit("Aborting Landscape configuration") |
37 | - |
38 | +def setup_http_proxy(config): |
39 | + """ |
40 | + If a http_proxy and a https_proxy value are not set then copy the values, |
41 | + if any, from the environment variables L{http_proxy} and L{https_proxy}. |
42 | + """ |
43 | if config.http_proxy is None and os.environ.get("http_proxy"): |
44 | config.http_proxy = os.environ["http_proxy"] |
45 | if config.https_proxy is None and os.environ.get("https_proxy"): |
46 | config.https_proxy = os.environ["https_proxy"] |
47 | |
48 | + |
49 | +def check_account_name_and_password(config): |
50 | + """ |
51 | + Ensure that silent configurations which plan to start landscape-client are |
52 | + either configured for OTP or have both an account_name and computer title. |
53 | + """ |
54 | if config.silent and not config.no_start: |
55 | if not (config.get("otp") or config.provisioning_otp or |
56 | (config.get("account_name") and config.get("computer_title"))): |
57 | raise ConfigurationError("An account name and computer title are " |
58 | "required.") |
59 | - if config.silent: |
60 | - if config.get("script_users"): |
61 | - invalid_users = get_invalid_users(config.get("script_users")) |
62 | - if invalid_users: |
63 | - raise ConfigurationError("Unknown system users: %s" % |
64 | - ", ".join(invalid_users)) |
65 | - if not config.include_manager_plugins: |
66 | - config.include_manager_plugins = "ScriptExecution" |
67 | - else: |
68 | - script = LandscapeSetupScript(config) |
69 | - script.run() |
70 | - |
71 | - # WARNING: ssl_public_key is misnamed, it's not the key of the certificate, |
72 | - # but the actual certificate itself. |
73 | + |
74 | + |
75 | +def check_script_users(config): |
76 | + """ |
77 | + If the configuration allows for script execution ensure that the configured |
78 | + users are valid for that purpose. |
79 | + """ |
80 | + if config.get("script_users"): |
81 | + invalid_users = get_invalid_users(config.get("script_users")) |
82 | + if invalid_users: |
83 | + raise ConfigurationError("Unknown system users: %s" % |
84 | + ", ".join(invalid_users)) |
85 | + if not config.include_manager_plugins: |
86 | + config.include_manager_plugins = "ScriptExecution" |
87 | + |
88 | + |
89 | +def decode_base64_ssl_public_certificate(config): |
90 | + """ |
91 | + Decode base64 encoded SSL certificate and push that back into place in the |
92 | + config object. |
93 | + """ |
94 | + # WARNING: ssl_public_certificate is misnamed, it's not the key of the |
95 | + # certificate, but the actual certificate itself. |
96 | if config.ssl_public_key and config.ssl_public_key.startswith("base64:"): |
97 | decoded_cert = base64.decodestring(config.ssl_public_key[7:]) |
98 | config.ssl_public_key = store_public_key_data( |
99 | config, decoded_cert) |
100 | |
101 | + |
102 | +def setup(config): |
103 | + """ |
104 | + Perform steps to ensure that landscape-client is correctly configured |
105 | + before we attempt to register it with a landscape server. |
106 | + |
107 | + If we are not configured to be silent then interrogate the user to provide |
108 | + necessary details for registration. |
109 | + """ |
110 | + sysvconfig = SysVConfig() |
111 | + if not config.no_start: |
112 | + if config.silent: |
113 | + setup_init_script_and_start_client() |
114 | + elif not sysvconfig.is_configured_to_run(): |
115 | + answer = raw_input("\nThe Landscape client must be started " |
116 | + "on boot to operate correctly.\n\n" |
117 | + "Start Landscape client on boot? (Y/n): ") |
118 | + if not answer.upper().startswith("N"): |
119 | + setup_init_script_and_start_client() |
120 | + else: |
121 | + sys.exit("Aborting Landscape configuration") |
122 | + |
123 | + setup_http_proxy(config) |
124 | + check_account_name_and_password(config) |
125 | + if config.silent: |
126 | + check_script_users(config) |
127 | + else: |
128 | + script = LandscapeSetupScript(config) |
129 | + script.run() |
130 | + decode_base64_ssl_public_certificate(config) |
131 | config.write() |
132 | # Restart the client to ensure that it's using the new configuration. |
133 | if not config.no_start and not config.otp: |
134 | @@ -512,7 +553,7 @@ |
135 | return key_filename |
136 | |
137 | |
138 | -def register(config, reactor=None): |
139 | +def register(config, on_message=print_text, on_error=sys.exit, reactor=None): |
140 | """Instruct the Landscape Broker to register the client. |
141 | |
142 | The broker will be instructed to reload its configuration and then to |
143 | @@ -533,16 +574,16 @@ |
144 | reactor.stop() |
145 | |
146 | def failure(): |
147 | - print_text("Invalid account name or " |
148 | + on_message("Invalid account name or " |
149 | "registration password.", error=True) |
150 | stop() |
151 | |
152 | def success(): |
153 | - print_text("System successfully registered.") |
154 | + on_message("System successfully registered.") |
155 | stop() |
156 | |
157 | def exchange_failure(): |
158 | - print_text("We were unable to contact the server. " |
159 | + on_message("We were unable to contact the server. " |
160 | "Your internet connection may be down. " |
161 | "The landscape client will continue to try and contact " |
162 | "the server periodically.", |
163 | @@ -555,11 +596,11 @@ |
164 | connector.disconnect() |
165 | |
166 | def catch_all(failure): |
167 | - print_text(failure.getTraceback(), error=True) |
168 | - print_text("Unknown error occurred.", error=True) |
169 | + on_message(failure.getTraceback(), error=True) |
170 | + on_message("Unknown error occurred.", error=True) |
171 | stop() |
172 | |
173 | - print_text("Please wait... ", "") |
174 | + on_message("Please wait... ", "") |
175 | |
176 | time.sleep(2) |
177 | |
178 | @@ -577,9 +618,9 @@ |
179 | return results.addErrback(catch_all) |
180 | |
181 | def got_error(failure): |
182 | - print_text("There was an error communicating with the Landscape " |
183 | - "client.", error=True) |
184 | - print_text("This machine will be registered with the provided " |
185 | + on_message("There was an error communicating with the Landscape" |
186 | + " client.", error=True) |
187 | + on_message("This machine will be registered with the provided " |
188 | "details when the client runs.", error=True) |
189 | if not config.ok_no_register: |
190 | exit_with_error.append(2) |
191 | @@ -593,7 +634,7 @@ |
192 | reactor.run() |
193 | |
194 | if exit_with_error: |
195 | - sys.exit(exit_with_error[0]) |
196 | + on_error(exit_with_error[0]) |
197 | |
198 | return result |
199 | |
200 | |
201 | === modified file 'landscape/tests/test_configuration.py' |
202 | --- landscape/tests/test_configuration.py 2012-01-05 10:42:54 +0000 |
203 | +++ landscape/tests/test_configuration.py 2012-01-12 15:18:24 +0000 |
204 | @@ -1,4 +1,5 @@ |
205 | import os |
206 | +import sys |
207 | from getpass import getpass |
208 | from ConfigParser import ConfigParser |
209 | from cStringIO import StringIO |
210 | @@ -1163,7 +1164,7 @@ |
211 | |
212 | # Registration logic should not be invoked. |
213 | register_mock = self.mocker.replace(register, passthrough=False) |
214 | - register_mock(ANY) |
215 | + register_mock(ANY, ANY, ANY) |
216 | self.mocker.count(0) |
217 | |
218 | self.mocker.replay() |
219 | @@ -1669,7 +1670,7 @@ |
220 | self.mocker.replay() |
221 | |
222 | # DO IT! |
223 | - return register(service.config) |
224 | + return register(service.config, print_text, sys.exit) |
225 | |
226 | def test_register_failure(self): |
227 | """ |
228 | @@ -1721,7 +1722,7 @@ |
229 | self.mocker.replay() |
230 | |
231 | # DO IT! |
232 | - return register(service.config) |
233 | + return register(service.config, print_text, sys.exit) |
234 | |
235 | def test_register_exchange_failure(self): |
236 | """ |
237 | @@ -1771,7 +1772,7 @@ |
238 | self.mocker.replay() |
239 | |
240 | # DO IT! |
241 | - return register(service.config) |
242 | + return register(service.config, print_text, sys.exit) |
243 | |
244 | def test_register_timeout_failure(self): |
245 | service = self.broker_service |
246 | @@ -1815,7 +1816,7 @@ |
247 | self.mocker.replay() |
248 | |
249 | # DO IT! |
250 | - return register(service.config) |
251 | + return register(service.config, print_text, sys.exit) |
252 | |
253 | def test_register_bus_connection_failure(self): |
254 | """ |
255 | @@ -1852,7 +1853,7 @@ |
256 | self.mocker.replay() |
257 | |
258 | config = get_config(self, ["-a", "accountname", "--silent"]) |
259 | - return register(config) |
260 | + return register(config, print_text, sys.exit) |
261 | |
262 | def test_register_bus_connection_failure_ok_no_register(self): |
263 | """ |
264 | @@ -1879,7 +1880,7 @@ |
265 | |
266 | config = get_config(self, ["-a", "accountname", "--silent", |
267 | "--ok-no-register"]) |
268 | - return self.assertSuccess(register(config)) |
269 | + return self.assertSuccess(register(config, print_text, sys.exit)) |
270 | |
271 | |
272 | class RegisterFunctionNoServiceTest(LandscapeTest): |
273 | @@ -1932,7 +1933,7 @@ |
274 | |
275 | self.mocker.replay() |
276 | |
277 | - return register(configuration) |
278 | + return register(configuration, print_text, sys.exit) |
279 | |
280 | |
281 | class StoreSSLCertificateDataTest(LandscapeTest): |
282 | |
283 | === modified file 'landscape/ui/controller/configuration.py' |
284 | --- landscape/ui/controller/configuration.py 2012-01-09 11:35:16 +0000 |
285 | +++ landscape/ui/controller/configuration.py 2012-01-12 15:18:24 +0000 |
286 | @@ -1,5 +1,8 @@ |
287 | +import socket |
288 | import threading |
289 | |
290 | +from landscape.ui.model.registration import ObservableRegistration |
291 | + |
292 | |
293 | class ConfigControllerLockError(Exception): |
294 | pass |
295 | @@ -14,27 +17,64 @@ |
296 | |
297 | HOSTED_HOST_NAME = "landscape.canonical.com" |
298 | DEFAULT_SERVER_HOST_NAME = "landscape.localdomain" |
299 | + DEFAULT_DEDICATED_ACCOUNT_NAME = "standalone" |
300 | |
301 | def __init__(self, configuration, args=[]): |
302 | + self._observers = [] |
303 | self._initial_server_host_name = self.DEFAULT_SERVER_HOST_NAME |
304 | + self._initial_account_name = self.DEFAULT_DEDICATED_ACCOUNT_NAME |
305 | self._configuration = configuration |
306 | self._args = args |
307 | self._lock_out = False |
308 | self._lock = threading.Lock() |
309 | |
310 | + def register_observer(self, function): |
311 | + "Register functions that observer modify/unmodify." |
312 | + self._observers.append(function) |
313 | + |
314 | + def notify_observers(self, modified): |
315 | + "Notify observers of modification events. L{Modified} is boolean." |
316 | + for function in self._observers: |
317 | + function(modified) |
318 | + |
319 | + def modify(self): |
320 | + "Mark this config as modified and notify observers." |
321 | + self._modified = True |
322 | + self.notify_observers(True) |
323 | + |
324 | + def unmodify(self): |
325 | + "Mark this config as being unmodified and notify observers." |
326 | + self._modified = False |
327 | + self.notify_observers(False) |
328 | + |
329 | def load(self): |
330 | "Load the initial data from the configuration" |
331 | self.lock() |
332 | self._configuration.load(self._args) |
333 | self._pull_data_from_config() |
334 | - self._modified = False |
335 | + self.default_computer_title() |
336 | + self.unmodify() |
337 | self.unlock() |
338 | |
339 | + def getfqdn(self): |
340 | + """ |
341 | + Wrap socket.getfqdn so we can test reliably. |
342 | + """ |
343 | + return socket.getfqdn() |
344 | + |
345 | + def default_computer_title(self): |
346 | + """ |
347 | + Default machine name to FQDN. |
348 | + """ |
349 | + if self._computer_title is None: |
350 | + self._computer_title = self.getfqdn() |
351 | + |
352 | def default_dedicated(self): |
353 | """ |
354 | Set L{server_host_name} to something sane when switching from hosted to |
355 | dedicated. |
356 | """ |
357 | + self._account_name = self.DEFAULT_DEDICATED_ACCOUNT_NAME |
358 | if self._initial_server_host_name != self.HOSTED_HOST_NAME: |
359 | self._server_host_name = self._initial_server_host_name |
360 | else: |
361 | @@ -43,7 +83,7 @@ |
362 | self._server_host_name) |
363 | self._ping_url = self._derive_ping_url_from_host_name( |
364 | self._server_host_name) |
365 | - self._modified = True |
366 | + self.modify() |
367 | |
368 | def default_hosted(self): |
369 | """ |
370 | @@ -56,7 +96,8 @@ |
371 | self._server_host_name) |
372 | self._ping_url = self._derive_ping_url_from_host_name( |
373 | self._server_host_name) |
374 | - self._modified = True |
375 | + self._account_name = self._initial_account_name |
376 | + self.modify() |
377 | |
378 | def _pull_data_from_config(self): |
379 | """ |
380 | @@ -69,6 +110,7 @@ |
381 | self._url = self._configuration.url |
382 | self._ping_url = self._configuration.ping_url |
383 | self._account_name = self._configuration.account_name |
384 | + self._initial_account_name = self._account_name |
385 | self._registration_password = \ |
386 | self._configuration.registration_password |
387 | self._computer_title = self._configuration.computer_title |
388 | @@ -80,7 +122,7 @@ |
389 | else: |
390 | self._server_host_name = self.HOSTED_HOST_NAME |
391 | self._initial_server_host_name = self._server_host_name |
392 | - self._modified = False |
393 | + self.unmodify() |
394 | self._lock.release() |
395 | |
396 | def lock(self): |
397 | @@ -141,7 +183,7 @@ |
398 | self._server_host_name) |
399 | self._ping_url = self._derive_ping_url_from_host_name( |
400 | self._server_host_name) |
401 | - self._modified = True |
402 | + self.modify() |
403 | self._lock.release() |
404 | server_host_name = property(_get_server_host_name, _set_server_host_name) |
405 | |
406 | @@ -171,7 +213,7 @@ |
407 | raise ConfigControllerLockError |
408 | else: |
409 | self._account_name = value |
410 | - self._modified = True |
411 | + self.modify() |
412 | self._lock.release() |
413 | account_name = property(_get_account_name, _set_account_name) |
414 | |
415 | @@ -185,7 +227,7 @@ |
416 | raise ConfigControllerLockError |
417 | else: |
418 | self._registration_password = value |
419 | - self._modified = True |
420 | + self.modify() |
421 | self._lock.release() |
422 | registration_password = property(_get_registration_password, |
423 | _set_registration_password) |
424 | @@ -229,4 +271,21 @@ |
425 | self._configuration.https_proxy = self._https_proxy |
426 | self._configuration.ping_url = self._ping_url |
427 | self._configuration.write() |
428 | - self._modified = False |
429 | + self.unmodify() |
430 | + |
431 | + def register(self, on_notify, on_error, on_success, on_failure, on_idle): |
432 | + "Invoke model level registration without completely locking the view." |
433 | + on_idle() |
434 | + registration = ObservableRegistration(on_idle) |
435 | + on_idle() |
436 | + self.commit() |
437 | + on_idle() |
438 | + registration.register_notification_observer(on_notify) |
439 | + on_idle() |
440 | + registration.register_error_observer(on_error) |
441 | + on_idle() |
442 | + registration.register_succeed_observer(on_success) |
443 | + on_idle() |
444 | + registration.register_fail_observer(on_failure) |
445 | + on_idle() |
446 | + registration.register(self._configuration) |
447 | |
448 | === modified file 'landscape/ui/controller/tests/test_app.py' |
449 | --- landscape/ui/controller/tests/test_app.py 2012-01-09 10:13:34 +0000 |
450 | +++ landscape/ui/controller/tests/test_app.py 2012-01-12 15:18:24 +0000 |
451 | @@ -17,16 +17,15 @@ |
452 | |
453 | class ConnectionRecordingSettingsApplicationController( |
454 | SettingsApplicationController): |
455 | - |
456 | _connections = set() |
457 | _connection_args = {} |
458 | _connection_kwargs = {} |
459 | |
460 | - def __init__(self, get_config_f=None): |
461 | + def __init__(self, get_config=None): |
462 | super(ConnectionRecordingSettingsApplicationController, |
463 | self).__init__() |
464 | - if get_config_f: |
465 | - self.get_config = get_config_f |
466 | + if get_config: |
467 | + self.get_config = get_config |
468 | |
469 | def _make_connection_name(self, signal, func): |
470 | return signal + ">" + func.__name__ |
471 | @@ -103,7 +102,7 @@ |
472 | return config |
473 | |
474 | self.app = ConnectionRecordingSettingsApplicationController( |
475 | - get_config_f=get_config) |
476 | + get_config=get_config) |
477 | |
478 | def tearDown(self): |
479 | Gtk.Dialog.run = self._real_run |
480 | |
481 | === modified file 'landscape/ui/controller/tests/test_configuration.py' |
482 | --- landscape/ui/controller/tests/test_configuration.py 2012-01-09 11:35:16 +0000 |
483 | +++ landscape/ui/controller/tests/test_configuration.py 2012-01-12 15:18:24 +0000 |
484 | @@ -26,6 +26,11 @@ |
485 | self.config = MyLandscapeSetupConfiguration() |
486 | self.controller = ConfigController(self.config) |
487 | |
488 | + def get_fqdn(): |
489 | + return "me.here.com" |
490 | + |
491 | + self.controller.getfqdn = get_fqdn |
492 | + |
493 | def test_init(self): |
494 | """ |
495 | Test that when we create a controller it has initial state read in |
496 | @@ -202,8 +207,9 @@ |
497 | self.assertEqual(self.controller.registration_password, None) |
498 | self.assertEqual(self.controller.server_host_name, |
499 | "landscape.canonical.com") |
500 | + self.controller.account_name = "Bungle" |
501 | self.controller.default_dedicated() |
502 | - self.assertEqual(self.controller.account_name, None) |
503 | + self.assertEqual(self.controller.account_name, "standalone") |
504 | self.assertEqual(self.controller.registration_password, None) |
505 | self.assertEqual(self.controller.server_host_name, |
506 | "landscape.localdomain") |
507 | @@ -221,3 +227,12 @@ |
508 | "landscape.canonical.com") |
509 | self.controller.default_dedicated() |
510 | self.assertEqual(self.controller.server_host_name, "test.machine") |
511 | + |
512 | + def test_default_computer_title(self): |
513 | + """ |
514 | + Test we set the computer title to host name when it isn't already set |
515 | + in the config file. |
516 | + """ |
517 | + self.makeFile("", path=self.config_filename) # Empty config |
518 | + self.controller.load() |
519 | + self.assertEqual(self.controller.computer_title, "me.here.com") |
520 | |
521 | === added directory 'landscape/ui/model' |
522 | === added file 'landscape/ui/model/__init__.py' |
523 | === added file 'landscape/ui/model/registration.py' |
524 | --- landscape/ui/model/registration.py 1970-01-01 00:00:00 +0000 |
525 | +++ landscape/ui/model/registration.py 2012-01-12 15:18:24 +0000 |
526 | @@ -0,0 +1,87 @@ |
527 | +import select |
528 | +import subprocess |
529 | +import os |
530 | + |
531 | + |
532 | +class ObservableRegistration(object): |
533 | + """ |
534 | + L{ObservableRegistration} provides the ability to run the landscape-client |
535 | + registration in a way that can be observed by code using it. This allows |
536 | + for registration in an environment other than an interactive terminal |
537 | + session. |
538 | + |
539 | + @param on_idle: Optionally, a callable which will be invoked by repeatedly |
540 | + during the registration process to allow cooperative yielding of control. |
541 | + This can be used, for example, with Gtk to allow processing of Gtk events |
542 | + without requiring either threading or the leaking of Gtk into the model |
543 | + layer. |
544 | + """ |
545 | + |
546 | + def __init__(self, on_idle=None): |
547 | + self._notification_observers = [] |
548 | + self._error_observers = [] |
549 | + self._succeed_observers = [] |
550 | + self._fail_observers = [] |
551 | + self._on_idle = on_idle |
552 | + |
553 | + def do_idle(self): |
554 | + if self._on_idle: |
555 | + self._on_idle() |
556 | + |
557 | + def notify_observers(self, message, end="\n", error=False): |
558 | + for function in self._notification_observers: |
559 | + function(message, error) |
560 | + self.do_idle() |
561 | + |
562 | + def error_observers(self, error_list): |
563 | + for function in self._error_observers: |
564 | + function(error_list) |
565 | + self.do_idle() |
566 | + |
567 | + def register_notification_observer(self, function): |
568 | + self._notification_observers.append(function) |
569 | + |
570 | + def register_error_observer(self, function): |
571 | + self._error_observers.append(function) |
572 | + |
573 | + def register_succeed_observer(self, function): |
574 | + self._succeed_observers.append(function) |
575 | + |
576 | + def register_fail_observer(self, function): |
577 | + self._fail_observers.append(function) |
578 | + |
579 | + def succeed(self): |
580 | + for function in self._succeed_observers: |
581 | + function() |
582 | + self.do_idle() |
583 | + |
584 | + def fail(self, error=None): |
585 | + for function in self._fail_observers: |
586 | + function(error=error) |
587 | + self.do_idle() |
588 | + |
589 | + def register(self, config): |
590 | + self.notify_observers("Trying to register ...\n") |
591 | + cmd = ["landscape-config", "--silent", "-c", |
592 | + os.path.abspath(config.get_config_filename())] |
593 | + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, |
594 | + stderr=subprocess.PIPE) |
595 | + return_code = None |
596 | + while return_code is None: |
597 | + readables, w, x = select.select([process.stdout, process.stderr], |
598 | + [], [], 0) |
599 | + for readable in readables: |
600 | + message = readable.readline() |
601 | + if readable is process.stdout: |
602 | + self.notify_observers(message) |
603 | + else: |
604 | + self.error_observers(message) |
605 | + self.do_idle() |
606 | + return_code = process.poll() |
607 | + self.do_idle() |
608 | + if return_code == 0: |
609 | + self.succeed() |
610 | + return True |
611 | + else: |
612 | + self.fail("Failed with code %s" % str(return_code)) |
613 | + return False |
614 | |
615 | === added directory 'landscape/ui/model/tests' |
616 | === added file 'landscape/ui/model/tests/__init__.py' |
617 | === added file 'landscape/ui/model/tests/test_registration.py' |
618 | --- landscape/ui/model/tests/test_registration.py 1970-01-01 00:00:00 +0000 |
619 | +++ landscape/ui/model/tests/test_registration.py 2012-01-12 15:18:24 +0000 |
620 | @@ -0,0 +1,80 @@ |
621 | +from landscape.tests.helpers import LandscapeTest, BrokerServiceHelper |
622 | +from landscape.ui.model.registration import ObservableRegistration |
623 | + |
624 | + |
625 | +class RegistrationTest(LandscapeTest): |
626 | + |
627 | + helpers = [BrokerServiceHelper] |
628 | + |
629 | + def setUp(self): |
630 | + super(RegistrationTest, self).setUp() |
631 | + |
632 | + def test_notify_observers(self): |
633 | + """ |
634 | + Test that when an observer is registered it is called by |
635 | + L{notify_observers}. |
636 | + """ |
637 | + observable = ObservableRegistration() |
638 | + notified = [] |
639 | + notified_message = [] |
640 | + notified_error = [] |
641 | + |
642 | + def notify_me(message, error=False): |
643 | + notified.append(True) |
644 | + notified_message.append(message) |
645 | + notified_error.append(error) |
646 | + |
647 | + observable.register_notification_observer(notify_me) |
648 | + observable.notify_observers("Blimey", error=False) |
649 | + observable.notify_observers("Gor lummey!", error=True) |
650 | + self.assertEqual(notified, [True, True]) |
651 | + self.assertEqual(notified_message, ["Blimey", "Gor lummey!"]) |
652 | + self.assertTrue(notified_error, [False, True]) |
653 | + |
654 | + def test_error_observers(self): |
655 | + """ |
656 | + Test that when an error observer is registered it is called by |
657 | + L{error_observers}. |
658 | + """ |
659 | + observable = ObservableRegistration() |
660 | + errored = [] |
661 | + errored_error_list = [] |
662 | + |
663 | + def error_me(error_list): |
664 | + errored.append(True) |
665 | + errored_error_list.append(error_list) |
666 | + |
667 | + observable.register_error_observer(error_me) |
668 | + observable.error_observers(["Ouch", "Dang"]) |
669 | + self.assertTrue(errored, [True]) |
670 | + self.assertEqual(errored_error_list, [["Ouch", "Dang"]]) |
671 | + |
672 | + def test_success_observers(self): |
673 | + """ |
674 | + Test that when a success observer is registered it is called when |
675 | + L{succeed} is called on the model. |
676 | + """ |
677 | + observable = ObservableRegistration() |
678 | + self.succeeded = False |
679 | + |
680 | + def success(): |
681 | + self.succeeded = True |
682 | + |
683 | + observable.register_succeed_observer(success) |
684 | + observable.succeed() |
685 | + self.assertTrue(self.succeeded) |
686 | + |
687 | + def test_failure_observers(self): |
688 | + """ |
689 | + Test that when a failure observer is registered it is called when |
690 | + L{fail} is called on the model. |
691 | + """ |
692 | + observable = ObservableRegistration() |
693 | + self.failed = False |
694 | + |
695 | + def failure(error=None): |
696 | + self.failed = True |
697 | + |
698 | + observable.register_fail_observer(failure) |
699 | + observable.fail() |
700 | + self.assertTrue(self.failed) |
701 | |
702 | === modified file 'landscape/ui/view/configuration.py' |
703 | --- landscape/ui/view/configuration.py 2012-01-09 15:14:12 +0000 |
704 | +++ landscape/ui/view/configuration.py 2012-01-12 15:18:24 +0000 |
705 | @@ -1,6 +1,6 @@ |
706 | import os |
707 | |
708 | -from gi.repository import Gtk |
709 | +from gi.repository import Gtk, Gdk |
710 | |
711 | |
712 | class ClientSettingsDialog(Gtk.Dialog): |
713 | @@ -9,7 +9,9 @@ |
714 | |
715 | def __init__(self, controller): |
716 | super(ClientSettingsDialog, self).__init__() |
717 | + self._continue = False |
718 | self.controller = controller |
719 | + self.controller.register_observer(self._on_modify) |
720 | self._ui_path = os.path.join( |
721 | os.path.dirname(__file__), "ui", |
722 | ClientSettingsDialog.GLADE_FILE) |
723 | @@ -41,6 +43,18 @@ |
724 | self._revert_button.connect("clicked", self.revert) |
725 | self._close_button.connect("clicked", self._possibly_save_and_exit) |
726 | |
727 | + def _setup_registration_controls(self): |
728 | + """ |
729 | + Obtain handles for controls relating to the registration process. |
730 | + """ |
731 | + self._registration_button = self._builder.get_object( |
732 | + "registration-button") |
733 | + self._registration_image = self._builder.get_object( |
734 | + "registration-image") |
735 | + self._registration_textbuffer = self._builder.get_object( |
736 | + "registration-textbuffer") |
737 | + self._registration_button.connect("clicked", self._register) |
738 | + |
739 | def _setup_entries(self): |
740 | """ |
741 | Obtain handles for entry widgets, set initial state and connect them to |
742 | @@ -73,8 +87,14 @@ |
743 | self._setup_radiobuttons() |
744 | self._setup_entries() |
745 | self._setup_action_buttons() |
746 | + self._setup_registration_controls() |
747 | |
748 | def _load_data(self): |
749 | + """ |
750 | + Pull data up from the controller into the view widgets. Note that we |
751 | + don't want to propagate events back to the controller as this will set |
752 | + up an infinite loop - hence the lock. |
753 | + """ |
754 | self.controller.lock() |
755 | if not self.controller.account_name is None: |
756 | self._account_entry.set_text(self.controller.account_name) |
757 | @@ -147,6 +167,64 @@ |
758 | self.controller.commit() |
759 | |
760 | def _possibly_save_and_exit(self, button): |
761 | + """ |
762 | + Write back if something has been modified. |
763 | + """ |
764 | if self.controller.is_modified: |
765 | self._write_back() |
766 | self.destroy() |
767 | + |
768 | + def _process_gtk_events(self): |
769 | + """ |
770 | + Deal with any outstanding Gtk events. Used to keep the GUI ticking |
771 | + over during long running activities. |
772 | + """ |
773 | + while Gtk.events_pending(): |
774 | + Gtk.main_iteration() |
775 | + |
776 | + def _registration_message(self, message, error=None): |
777 | + self._registration_textbuffer.insert_at_cursor(message) |
778 | + self._registration_image.set_from_stock(Gtk.STOCK_DIALOG_INFO, 4) |
779 | + self._process_gtk_events() |
780 | + |
781 | + def _registration_error(self, message): |
782 | + self._registration_textbuffer.insert_at_cursor(message) |
783 | + self._registration_image.set_from_stock(Gtk.STOCK_DIALOG_WARNING, 4) |
784 | + self._process_gtk_events() |
785 | + |
786 | + def _registration_succeed(self): |
787 | + self._registration_image.set_from_stock(Gtk.STOCK_CONNECT, 4) |
788 | + self._close_button.set_sensitive(True) |
789 | + self._set_normal_cursor() |
790 | + |
791 | + def _registration_fail(self, error=None): |
792 | + if error: |
793 | + self._registration_textbuffer.insert_at_cursor(str(error)) |
794 | + self._registration_image.set_from_stock(Gtk.STOCK_DISCONNECT, 4) |
795 | + self._set_normal_cursor() |
796 | + |
797 | + def _set_wait_cursor(self): |
798 | + watch = Gdk.Cursor(Gdk.CursorType.WATCH) |
799 | + self.get_window().set_cursor(watch) |
800 | + self._process_gtk_events() |
801 | + |
802 | + def _set_normal_cursor(self): |
803 | + arrow = Gdk.Cursor(Gdk.CursorType.ARROW) |
804 | + self.get_window().set_cursor(arrow) |
805 | + self._process_gtk_events() |
806 | + |
807 | + def _register(self, button): |
808 | + self._set_wait_cursor() |
809 | + self._write_back() |
810 | + self._registration_image.set_from_stock(Gtk.STOCK_CONNECT, 4) |
811 | + self.controller.register(self._registration_message, |
812 | + self._registration_error, |
813 | + self._registration_succeed, |
814 | + self._registration_fail, |
815 | + self._process_gtk_events) |
816 | + |
817 | + def _on_modify(self, modified): |
818 | + self._close_button.set_sensitive(not modified) |
819 | + if modified: |
820 | + self._registration_image.set_from_stock(Gtk.STOCK_DISCONNECT, 4) |
821 | + self._registration_textbuffer.set_text("") |
822 | |
823 | === modified file 'landscape/ui/view/tests/test_configuration.py' |
824 | --- landscape/ui/view/tests/test_configuration.py 2012-01-05 14:16:47 +0000 |
825 | +++ landscape/ui/view/tests/test_configuration.py 2012-01-12 15:18:24 +0000 |
826 | @@ -52,6 +52,12 @@ |
827 | self.assertTrue(dialog._account_entry.get_sensitive()) |
828 | self.assertTrue(dialog._password_entry.get_sensitive()) |
829 | self.assertFalse(dialog._server_host_name_entry.get_sensitive()) |
830 | + self.assertTrue(dialog._registration_button.get_sensitive()) |
831 | + start, end = dialog._registration_textbuffer.get_bounds() |
832 | + self.assertEqual( |
833 | + len(dialog._registration_textbuffer.get_text( |
834 | + start, end, include_hidden_chars=True)), |
835 | + 0) |
836 | |
837 | def test_toggle_radio_button(self): |
838 | """ |
839 | @@ -90,6 +96,27 @@ |
840 | self.assertEqual(dialog._server_host_name_entry.get_text(), |
841 | "landscape.canonical.com") |
842 | |
843 | + def test_modification(self): |
844 | + """ |
845 | + Test that modifications to the dialog cause the close button to be |
846 | + disabled and the connect button to be reset. |
847 | + """ |
848 | + controller = ConfigController(self.config) |
849 | + dialog = ClientSettingsDialog(controller) |
850 | + self.assertEqual(dialog._registration_image.get_stock(), |
851 | + (Gtk.STOCK_DISCONNECT, 4)) |
852 | + dialog._registration_image.set_from_stock(Gtk.STOCK_CONNECT, 4) |
853 | + while Gtk.events_pending(): |
854 | + Gtk.main_iteration() |
855 | + self.assertEqual(dialog._registration_image.get_stock(), |
856 | + (Gtk.STOCK_CONNECT, 4)) |
857 | + dialog._account_entry.set_text("Spangles!") |
858 | + while Gtk.events_pending(): |
859 | + Gtk.main_iteration() |
860 | + self.assertEqual(dialog._registration_image.get_stock(), |
861 | + (Gtk.STOCK_DISCONNECT, 4)) |
862 | + self.assertFalse(dialog._close_button.get_sensitive()) |
863 | + |
864 | def test_revert(self): |
865 | """ |
866 | Test that we can revert the UI values using the controller. |
867 | @@ -102,7 +129,7 @@ |
868 | "landscape.canonical.com") |
869 | dialog._dedicated_radiobutton.set_active(True) |
870 | dialog._server_host_name_entry.set_text("more.barn") |
871 | - self.assertEqual(dialog._account_entry.get_text(), "foo") |
872 | + self.assertEqual(dialog._account_entry.get_text(), "standalone") |
873 | self.assertEqual(dialog._password_entry.get_text(), "bar") |
874 | self.assertEqual(dialog._server_host_name_entry.get_text(), |
875 | "more.barn") |
876 | |
877 | === modified file 'landscape/ui/view/ui/landscape-client-settings.glade' |
878 | --- landscape/ui/view/ui/landscape-client-settings.glade 2012-01-02 12:04:18 +0000 |
879 | +++ landscape/ui/view/ui/landscape-client-settings.glade 2012-01-12 15:18:24 +0000 |
880 | @@ -14,6 +14,7 @@ |
881 | <child internal-child="vbox"> |
882 | <object class="GtkBox" id="landscape-client-settings-dialog-vbox"> |
883 | <property name="can_focus">False</property> |
884 | + <property name="vexpand">True</property> |
885 | <property name="orientation">vertical</property> |
886 | <property name="spacing">2</property> |
887 | <child internal-child="action_area"> |
888 | @@ -62,6 +63,7 @@ |
889 | <object class="GtkBox" id="vbox"> |
890 | <property name="visible">True</property> |
891 | <property name="can_focus">False</property> |
892 | + <property name="vexpand">True</property> |
893 | <property name="orientation">vertical</property> |
894 | <property name="spacing">6</property> |
895 | <child> |
896 | @@ -383,6 +385,99 @@ |
897 | <property name="position">0</property> |
898 | </packing> |
899 | </child> |
900 | + <child> |
901 | + <object class="GtkBox" id="registration-hbox"> |
902 | + <property name="visible">True</property> |
903 | + <property name="can_focus">False</property> |
904 | + <property name="margin_left">12</property> |
905 | + <property name="margin_right">6</property> |
906 | + <property name="hexpand">True</property> |
907 | + <property name="vexpand">True</property> |
908 | + <property name="spacing">12</property> |
909 | + <child> |
910 | + <object class="GtkButton" id="registration-button"> |
911 | + <property name="visible">True</property> |
912 | + <property name="can_focus">True</property> |
913 | + <property name="receives_default">True</property> |
914 | + <property name="valign">start</property> |
915 | + <property name="use_action_appearance">False</property> |
916 | + <child> |
917 | + <object class="GtkBox" id="registration-button-hbox"> |
918 | + <property name="visible">True</property> |
919 | + <property name="can_focus">False</property> |
920 | + <property name="margin_left">6</property> |
921 | + <property name="margin_right">6</property> |
922 | + <property name="margin_top">6</property> |
923 | + <property name="margin_bottom">6</property> |
924 | + <property name="orientation">vertical</property> |
925 | + <property name="spacing">6</property> |
926 | + <property name="homogeneous">True</property> |
927 | + <child> |
928 | + <object class="GtkLabel" id="label1"> |
929 | + <property name="visible">True</property> |
930 | + <property name="can_focus">False</property> |
931 | + <property name="label" translatable="yes">Connect</property> |
932 | + </object> |
933 | + <packing> |
934 | + <property name="expand">False</property> |
935 | + <property name="fill">True</property> |
936 | + <property name="position">0</property> |
937 | + </packing> |
938 | + </child> |
939 | + <child> |
940 | + <object class="GtkImage" id="registration-image"> |
941 | + <property name="visible">True</property> |
942 | + <property name="can_focus">False</property> |
943 | + <property name="stock">gtk-disconnect</property> |
944 | + </object> |
945 | + <packing> |
946 | + <property name="expand">False</property> |
947 | + <property name="fill">True</property> |
948 | + <property name="position">1</property> |
949 | + </packing> |
950 | + </child> |
951 | + </object> |
952 | + </child> |
953 | + </object> |
954 | + <packing> |
955 | + <property name="expand">False</property> |
956 | + <property name="fill">True</property> |
957 | + <property name="position">0</property> |
958 | + </packing> |
959 | + </child> |
960 | + <child> |
961 | + <object class="GtkScrolledWindow" id="registration-scrolledwindow"> |
962 | + <property name="visible">True</property> |
963 | + <property name="can_focus">True</property> |
964 | + <property name="hexpand">True</property> |
965 | + <property name="vscrollbar_policy">always</property> |
966 | + <property name="shadow_type">in</property> |
967 | + <child> |
968 | + <object class="GtkTextView" id="registration-textview"> |
969 | + <property name="visible">True</property> |
970 | + <property name="can_focus">True</property> |
971 | + <property name="hexpand">True</property> |
972 | + <property name="vexpand">True</property> |
973 | + <property name="editable">False</property> |
974 | + <property name="wrap_mode">word-char</property> |
975 | + <property name="cursor_visible">False</property> |
976 | + <property name="buffer">registration-textbuffer</property> |
977 | + </object> |
978 | + </child> |
979 | + </object> |
980 | + <packing> |
981 | + <property name="expand">False</property> |
982 | + <property name="fill">True</property> |
983 | + <property name="position">1</property> |
984 | + </packing> |
985 | + </child> |
986 | + </object> |
987 | + <packing> |
988 | + <property name="expand">False</property> |
989 | + <property name="fill">True</property> |
990 | + <property name="position">1</property> |
991 | + </packing> |
992 | + </child> |
993 | </object> |
994 | <packing> |
995 | <property name="expand">False</property> |
996 | @@ -404,4 +499,5 @@ |
997 | <widget name="account-label"/> |
998 | </widgets> |
999 | </object> |
1000 | + <object class="GtkTextBuffer" id="registration-textbuffer"/> |
1001 | </interface> |
1002 | |
1003 | === modified file 'setup.py' |
1004 | --- setup.py 2011-12-01 14:01:29 +0000 |
1005 | +++ setup.py 2012-01-12 15:18:24 +0000 |
1006 | @@ -16,10 +16,16 @@ |
1007 | "landscape.monitor", |
1008 | "landscape.package", |
1009 | "landscape.sysinfo", |
1010 | + "landscape.ui", |
1011 | + "landscape.ui.model", |
1012 | + "landscape.ui.controller", |
1013 | + "landscape.ui.view", |
1014 | "landscape.upgraders", |
1015 | "landscape.user", |
1016 | "landscape.lib"], |
1017 | + package_data={"landscape.ui.view": ["ui/landscape-client-settings.glade"]}, |
1018 | scripts=["scripts/landscape-client", |
1019 | + "scripts/landscape-client-settings-ui", |
1020 | "scripts/landscape-config", |
1021 | "scripts/landscape-message", |
1022 | "scripts/landscape-broker", |
Nice work, a few comments but only about cosmetics. +1!
[1]
+def check_script_ users(config) :
+def setup_http_ proxy(config) :
+def check_account_ name_and_ password( config) :
Please add a docstring to these new functions (and maybe to setup() as well while you're at it).
[2]
-def register(config, reactor=None):
+def register(config, message_handler_f, error_handler_f, reactor=None):
We don't usually encode type information in variable names, or at least not with abbreviations. Please consider renaming these parameters to something like "on_finish" and "on_error".
Also what do you think of adding print_text and sys.exit as default values for these new parameters? So you get a more user-friendly interface (you don't need to customize what you don't need) and call sites or tests don't need to be modified.
[3]
+class ObservableRegis tration( object) :
Please add a class docstring for this class and for all its methods. The class docstring should include __init__ parameters to, for example
class ObservableRegis tration( object) :
"""Meant to do something useful.
@param idle_f: Optionally, a function that will be called when...
"""
[4]
+ for fun in self._succeed_ observers:
+ fun()
+ self.do_idle()
Please use full names, s/fun/function/
[5]
+ self.notified = False message = None message = message
+ self.notified_
+ self.notified_error = False
+
+ def notify_me(message, error=False):
+ self.notified = True
+ self.notified_
+ self.notified_error = error
This is okay, but of course leaves some "attribute garbage" on the test case instance, that in principle could get in the way of other tests, if don't write them carefully.
Please consider change it to a version without side effects, like:
notified = []
notified_ message = []
notified_ error = []
def notify_me(message, error=False):
notified. append( True)
notified_ message. append( message)
notified_ error.append( error)
plus
self.assertEqu al([True] , notified)
self.assertEqu al(notified_ message, ["Blimey"])
self.assertEqu al(notified_ error, [False])
Also, this has the advantage of asserting also how many times notify_me was called, and not only which parameters it was passed.
Alternatively, you could go for a Mocker version:
def test_notify_ observers( self): tration( )
self.expect( notify_ me("Blimey" , error=False))
self.expect( notify_ me("Go lummey!", error=True))
self.mocker. replay( )
observable. notify_ observers( "Blimey" , error=False)
observable. notify_ observers( "Gor lummey!", error=True)
observable = ObservableRegis
notify_me = self.mocker.mock()
and you can rely on mocker to assert that notify_me was called exactly in that order, with those arguments and that number for times. I just thought I'd mention it, not sure how much I like this myself though :)
[6]
+ def default_ machine( self):
Please consider renaming this to set_default_ computer_ title (and maybe other similar methods to). The rationals are that the "set_" prefix makes it more clear wh...