Merge lp:~tealeg/landscape-client/invoke-registration-from-settings-ui into lp:~landscape/landscape-client/trunk

Proposed by Geoff Teale
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
Reviewer Review Type Date Requested Status
Fernando Correa Neto (community) Approve
Free Ekanayaka (community) Approve
Review via email: mp+88187@code.launchpad.net

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-client.conf. You need to have a version of landscape-client installed because it will invoke the installed version of landscape-config (or at least not when you have landscape-client installed). You can pass -c /etc/landscape/client.conf if that is the case and get it to work.

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

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 ObservableRegistration(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 ObservableRegistration(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
+ self.notified_message = None
+ self.notified_error = False
+
+ def notify_me(message, error=False):
+ self.notified = True
+ self.notified_message = message
+ 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.assertEqual([True], notified)
        self.assertEqual(notified_message, ["Blimey"])
        self.assertEqual(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):
        observable = ObservableRegistration()
        notify_me = self.mocker.mock()
        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)

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...

Read more...

review: Approve
Revision history for this message
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 ConfigController.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

Revision history for this message
Fernando Correa Neto (fcorrea) wrote :

Looks great! +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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",

Subscribers

People subscribed via source and target branches

to all changes: