Merge lp:~jimbaker/pyjuju/robust-zk-connect into lp:pyjuju
- robust-zk-connect
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Kapil Thangavelu | ||||
Approved revision: | 423 | ||||
Merged at revision: | 432 | ||||
Proposed branch: | lp:~jimbaker/pyjuju/robust-zk-connect | ||||
Merge into: | lp:pyjuju | ||||
Diff against target: |
715 lines (+231/-335) 7 files modified
juju/lib/tests/test_twistutils.py (+12/-1) juju/lib/twistutils.py (+15/-1) juju/providers/common/base.py (+0/-2) juju/providers/common/connect.py (+50/-29) juju/providers/common/tests/test_connect.py (+154/-48) juju/providers/ec2/tests/test_connect.py (+0/-143) juju/providers/orchestra/tests/test_connect.py (+0/-111) |
||||
To merge this branch: | bzr merge lp:~jimbaker/pyjuju/robust-zk-connect | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kapil Thangavelu (community) | Approve | ||
William Reade (community) | Approve | ||
Review via email: mp+82768@code.launchpad.net |
Commit message
Description of the change
This branch implements the change in the API, per this mailing list discussion:
https:/
Some things not covered in this branch:
Documentation of the rationale, as requested by https:/
Support for timeouts. However, no one discussed this on the mailing list, so this issue may be moot.
Running with --verbose is *very* verbose indeed with these retries, due to ZooKeeper client logging. This could be addressed by providing for more logging levels (maybe --verbose=
Bug 892254 was added to take in account that during retries, spurious messages are printed to stderr. This has no impact however on the robustness of the retry process, however.
Lastly, I removed what appears to be redundant test code in the orchestra and ec2 providers. From what I can tell, this was simply combining some aspects of testing the findzookeepers code, which is necessarily different for each provider, with the same connection tests in juju.providers.
Kapil Thangavelu (hazmat) wrote : | # |
It works :-) I'm a little concerned by the content and verbosity of
the output both when run normally or in verbose mode.
verbose mode -> http://
normal mode -> http://
[0]
The retry is a bit aggressive, i see 3-4 calls per second to try and
resolve the host ip address, over the course of avg ~30s. Some backoff
might be beneficial here, like 1s between retries on a machine without
an address.
[1]
2011-11-29 21:06:27,
Unhandled error in Deferred:
Unhandled error in Deferred:
Unhandled Error
Traceback (most recent call last):
Failure: txzookeeper.
it would be nice if this could be resolved as well in this branch, i
tracked it down to the called_
failures if the parent deferred is called.
The more i look at this branch, i find the sshclient/forward code more
mystifying. Its not germane to this branch, but i've got a refactoring
branch that will followup this branch to make this code a bit cleaner.
The gist of the cleanup http://
[2] The zookeeper c library doesn't have a notion of timeouts, its an
internal thing the txzk library manages using a twisted
reactor.callLater. Effectively it means everytime we attempt a
connect, there will be a background attempt continously running in the
zk io thread.
An effective usage change would be to just increase the timeout on connect, the tunnel is already active, so its just a matter of waiting. I just commited r45 on txzookeeper to handle this issue there, namely tackle closing the conn should terminate the background connect activity, but i don't see why we wouldn't just go for the longer delay since its a continual reconnection attempt.
[3]
2011-12-05 17:54:46,490 ERROR Connection refused
Normal stdout log for the command has a few of these error
messages. Since this is normal activity it would be good to ellide
them unless in verbose mode.
Kapil Thangavelu (hazmat) wrote : | # |
i've fixed the verbosity/output issues from [1-3] in a follow up branch (lp:~hazmat/juju/sshclient-refactor) that also cleanups the ssh client connect code.. so outside of [0] this branch should be fine.
Jim Baker (jimbaker) wrote : | # |
For [0], I added a 1 second deferred sleep so as to implement the desired backoff. This also marks the addition of a sleep function finally to twistutils, after a number of thoughts of doing so.
Kapil Thangavelu (hazmat) wrote : | # |
LGTM, let me know when your planning on merging, i'd like to get the followup branch in with it to avoid getting builds with this branch alone. Also this functionality could definitely use an email out to the list once landed.
Kapil Thangavelu (hazmat) : | # |
Preview Diff
1 | === modified file 'juju/lib/tests/test_twistutils.py' |
2 | --- juju/lib/tests/test_twistutils.py 2011-09-15 18:50:23 +0000 |
3 | +++ juju/lib/tests/test_twistutils.py 2011-12-08 20:20:29 +0000 |
4 | @@ -1,4 +1,5 @@ |
5 | import os |
6 | +import time |
7 | |
8 | from twisted.internet.defer import ( |
9 | succeed, fail, Deferred, DeferredList, inlineCallbacks, returnValue) |
10 | @@ -8,7 +9,7 @@ |
11 | |
12 | from juju.lib.testing import TestCase |
13 | from juju.lib.twistutils import ( |
14 | - concurrent_execution_guard, gather_results, get_module_directory) |
15 | + concurrent_execution_guard, gather_results, get_module_directory, sleep) |
16 | |
17 | |
18 | class Bar(object): |
19 | @@ -144,3 +145,13 @@ |
20 | self.assertIn("juju", directory) |
21 | self.assertNotIn("_trial_temp", directory) |
22 | self.assertTrue(os.path.isdir(directory)) |
23 | + |
24 | + |
25 | +class SleepTest(TestCase): |
26 | + |
27 | + @inlineCallbacks |
28 | + def test_sleep(self): |
29 | + """Directly test deferred sleep.""" |
30 | + start = time.time() |
31 | + yield sleep(0.1) |
32 | + self.assertGreaterEqual(time.time() - start, 0.1) |
33 | |
34 | === modified file 'juju/lib/twistutils.py' |
35 | --- juju/lib/twistutils.py 2011-02-15 15:15:16 +0000 |
36 | +++ juju/lib/twistutils.py 2011-12-08 20:20:29 +0000 |
37 | @@ -1,7 +1,9 @@ |
38 | import inspect |
39 | import os |
40 | |
41 | -from twisted.internet.defer import maybeDeferred, succeed, DeferredList |
42 | +from twisted.internet import reactor |
43 | +from twisted.internet.defer import ( |
44 | + Deferred, maybeDeferred, succeed, DeferredList) |
45 | from twisted.python.util import mergeFunctionMetadata |
46 | |
47 | |
48 | @@ -53,3 +55,15 @@ |
49 | """ |
50 | return os.path.abspath(os.path.dirname(inspect.getabsfile(module)).replace( |
51 | "/_trial_temp", "")) |
52 | + |
53 | + |
54 | +def sleep(delay): |
55 | + """Non-blocking sleep. |
56 | + |
57 | + :param int delay: time in seconds to sleep. |
58 | + :return: a Deferred that fires after the desired delay. |
59 | + :rtype: :class:`twisted.internet.defer.Deferred` |
60 | + """ |
61 | + deferred = Deferred() |
62 | + reactor.callLater(delay, deferred.callback, None) |
63 | + return deferred |
64 | |
65 | === modified file 'juju/providers/common/base.py' |
66 | --- juju/providers/common/base.py 2011-09-28 09:48:30 +0000 |
67 | +++ juju/providers/common/base.py 2011-12-08 20:20:29 +0000 |
68 | @@ -148,8 +148,6 @@ |
69 | |
70 | :raises: :exc:`juju.errors.EnvironmentNotFound` when no zookeepers |
71 | exist |
72 | - :raises: :exc:`juju.errors.EnvironmentPending` when zookeepers |
73 | - exist but connection attempt fails |
74 | """ |
75 | return ZookeeperConnect(self).run(share=share) |
76 | |
77 | |
78 | === modified file 'juju/providers/common/connect.py' |
79 | --- juju/providers/common/connect.py 2011-09-16 19:37:15 +0000 |
80 | +++ juju/providers/common/connect.py 2011-12-08 20:20:29 +0000 |
81 | @@ -1,8 +1,11 @@ |
82 | +import random |
83 | + |
84 | from twisted.internet.defer import inlineCallbacks, returnValue |
85 | |
86 | from txzookeeper.client import ConnectionTimeoutException |
87 | |
88 | -from juju.errors import EnvironmentPending, NoConnection |
89 | +from juju.errors import EnvironmentNotFound, EnvironmentPending, NoConnection |
90 | +from juju.lib.twistutils import sleep |
91 | from juju.state.sshclient import SSHClient |
92 | |
93 | from .utils import log |
94 | @@ -15,51 +18,69 @@ |
95 | |
96 | @inlineCallbacks |
97 | def run(self, share=False): |
98 | - """Attempt to connect to a running zookeeper node. |
99 | + """Attempt to connect to a running zookeeper node, retrying as needed. |
100 | |
101 | :param bool share: where feasible, attempt to share a connection with |
102 | - other clients |
103 | + other clients. |
104 | |
105 | :return: an open :class:`txzookeeper.client.ZookeeperClient` |
106 | :rtype: :class:`twisted.internet.defer.Deferred` |
107 | |
108 | :raises: :exc:`juju.errors.EnvironmentNotFound` when no zookeepers |
109 | exist |
110 | - :raises: :exc:`juju.errors.EnvironmentPending` when zookeepers |
111 | - exist but connection attempt fails |
112 | + |
113 | + Internally this method catches all |
114 | + :exc:`juju.errors.EnvironmentPending`, since |
115 | + this exception explicitly means that a retry is feasible. |
116 | + |
117 | + TODO consider supporting a timeout for this method, instead of |
118 | + any such timeouts being done externally. |
119 | """ |
120 | + log.info("Connecting to environment...") |
121 | + while True: |
122 | + try: |
123 | + client = yield self._internal_connect(share) |
124 | + log.info("Connected to environment.") |
125 | + returnValue(client) |
126 | + except EnvironmentPending as e: |
127 | + log.debug("Retrying connection: %s", e) |
128 | + except EnvironmentNotFound: |
129 | + # Expected if not bootstrapped, simply raise up |
130 | + raise |
131 | + except Exception as e: |
132 | + # Otherwise this is unexpected, log with some details |
133 | + log.exception("Cannot connect to environment: %s", e) |
134 | + raise |
135 | + |
136 | + @inlineCallbacks |
137 | + def _internal_connect(self, share): |
138 | + """Attempt connection to one of the ZK nodes.""" |
139 | candidates = yield self._provider.get_zookeeper_machines() |
140 | - chosen = yield self._pick_machine(candidates) |
141 | - client = yield self._connect_to_machine(chosen, share) |
142 | + assigned = [machine for machine in candidates if machine.dns_name] |
143 | + if not assigned: |
144 | + yield sleep(1) # Momentarily backoff |
145 | + raise EnvironmentPending("No machines have assigned addresses") |
146 | + |
147 | + chosen = random.choice(assigned) |
148 | + log.debug("Connecting to environment using %s...", chosen.dns_name) |
149 | + try: |
150 | + client = yield SSHClient().connect( |
151 | + chosen.dns_name + ":2181", timeout=30, share=share) |
152 | + except (NoConnection, ConnectionTimeoutException) as e: |
153 | + raise EnvironmentPending( |
154 | + "Cannot connect to environment using %s " |
155 | + "(perhaps still initializing): %s" % ( |
156 | + chosen.dns_name, str(e))) |
157 | + |
158 | yield self.wait_for_initialization(client) |
159 | returnValue(client) |
160 | |
161 | - def _pick_machine(self, machines): |
162 | - # TODO Should we pick a random entry from the nodes list? |
163 | - for machine in machines: |
164 | - if machine.dns_name: |
165 | - return machine |
166 | - raise EnvironmentPending("No machines have addresses assigned yet") |
167 | - |
168 | - def _connect_to_machine(self, machine, share): |
169 | - log.info("Connecting to environment.") |
170 | - result = SSHClient().connect( |
171 | - machine.dns_name + ":2181", timeout=30, share=share) |
172 | - result.addErrback(self._cannot_connect, machine) |
173 | - return result |
174 | - |
175 | - def _cannot_connect(self, failure, machine): |
176 | - failure.trap(NoConnection, ConnectionTimeoutException) |
177 | - raise EnvironmentPending( |
178 | - "Cannot connect to machine %s (perhaps still initializing): %s" |
179 | - % (machine.instance_id, str(failure.value))) |
180 | - |
181 | @inlineCallbacks |
182 | def wait_for_initialization(self, client): |
183 | exists_d, watch_d = client.exists_and_watch("/initialized") |
184 | exists = yield exists_d |
185 | if not exists: |
186 | - log.info("Environment still initializing. Will wait.") |
187 | + log.debug("Environment still initializing. Will wait.") |
188 | yield watch_d |
189 | else: |
190 | - log.debug("Environment already initialized.") |
191 | + log.debug("Environment is initialized.") |
192 | |
193 | === modified file 'juju/providers/common/tests/test_connect.py' |
194 | --- juju/providers/common/tests/test_connect.py 2011-09-15 18:50:23 +0000 |
195 | +++ juju/providers/common/tests/test_connect.py 2011-12-08 20:20:29 +0000 |
196 | @@ -1,3 +1,6 @@ |
197 | +import logging |
198 | +import random |
199 | + |
200 | from twisted.internet.defer import fail, inlineCallbacks, succeed |
201 | |
202 | import zookeeper |
203 | @@ -6,7 +9,7 @@ |
204 | from txzookeeper.client import ConnectionTimeoutException |
205 | from txzookeeper.tests.utils import deleteTree |
206 | |
207 | -from juju.errors import EnvironmentPending, NoConnection |
208 | +from juju.errors import EnvironmentNotFound, NoConnection |
209 | from juju.lib.testing import TestCase |
210 | from juju.machine import ProviderMachine |
211 | from juju.providers.common.base import MachineProviderBase |
212 | @@ -16,16 +19,26 @@ |
213 | |
214 | class DummyProvider(MachineProviderBase): |
215 | |
216 | - def __init__(self, second_zookeeeper): |
217 | - self._second_zookeeper = second_zookeeeper |
218 | + def __init__(self, *zookeepers): |
219 | + self._zookeepers = zookeepers |
220 | |
221 | def get_zookeeper_machines(self): |
222 | """ |
223 | Return a pair of possible zookeepers, the first of which is invalid |
224 | """ |
225 | - return succeed([ |
226 | - ProviderMachine("i-havenodns"), |
227 | - self._second_zookeeper]) |
228 | + machines = [ProviderMachine("i-havenodns")] |
229 | + machines.extend(self._zookeepers) |
230 | + return succeed(machines) |
231 | + |
232 | + |
233 | +class NotBootstrappedProvider(MachineProviderBase): |
234 | + """Pretend to be an environment that has not been bootstrapped.""" |
235 | + |
236 | + def __init__(self): |
237 | + pass |
238 | + |
239 | + def get_zookeeper_machines(self): |
240 | + return fail(EnvironmentNotFound("is the environment bootstrapped?")) |
241 | |
242 | |
243 | class ConnectTest(TestCase): |
244 | @@ -35,33 +48,32 @@ |
245 | client.connect("foo.example.com:2181", timeout=30, share=share) |
246 | self.mocker.result(result) |
247 | |
248 | - def assert_connect_error(self, error, expect_type, expect_message): |
249 | - self.mock_connect(False, fail(error)) |
250 | + @inlineCallbacks |
251 | + def test_none_have_dns_at_first(self): |
252 | + """Verify retry of ZK machine that at first doesn't have a DNS name.""" |
253 | + log = self.capture_logging(level=logging.DEBUG) |
254 | + mocked_machine = self.mocker.proxy(ProviderMachine( |
255 | + "i-have-no-dns-at-first")) |
256 | + mocked_machine.dns_name |
257 | + self.mocker.result(None) |
258 | + mocked_machine.dns_name |
259 | + self.mocker.count(0, None) |
260 | + self.mocker.result("foo.example.com") # name assumed by mock_connect |
261 | + |
262 | + # Provide for a valid mocked client once connected |
263 | + client = self.mocker.mock(type=SSHClient) |
264 | + self.mock_connect(True, succeed(client)) |
265 | + client.exists_and_watch("/initialized") |
266 | + self.mocker.result((succeed(True), None)) |
267 | + |
268 | self.mocker.replay() |
269 | |
270 | - provider = DummyProvider(ProviderMachine("i-exist", "foo.example.com")) |
271 | - d = provider.connect() |
272 | - |
273 | - def check_error(error): |
274 | - self.assertEqual(str(error), expect_message) |
275 | - self.assertFailure(d, expect_type) |
276 | - d.addCallback(check_error) |
277 | - return d |
278 | - |
279 | - def test_none_have_dns(self): |
280 | - """ |
281 | - `EnvironmentPending` should be raised if no zookeeper nodes have dns |
282 | - names |
283 | - """ |
284 | - provider = DummyProvider(ProviderMachine("i-havenodnseither")) |
285 | - d = provider.connect() |
286 | - self.assertFailure(d, EnvironmentPending) |
287 | - |
288 | - def check(error): |
289 | - self.assertEquals( |
290 | - str(error), "No machines have addresses assigned yet") |
291 | - d.addCallback(check) |
292 | - return d |
293 | + provider = DummyProvider(mocked_machine) |
294 | + client = yield provider.connect(share=True) |
295 | + |
296 | + self.assertIn( |
297 | + "Retrying connection: No machines have assigned addresses", |
298 | + log.getvalue()) |
299 | |
300 | def test_share_kwarg(self): |
301 | """The `share` kwarg should be passed through to `SSHClient.connect`""" |
302 | @@ -79,26 +91,80 @@ |
303 | d.addCallback(verify) |
304 | return d |
305 | |
306 | + @inlineCallbacks |
307 | def test_no_connection(self): |
308 | - """`NoConnection` errors should become `EnvironmentPending`s""" |
309 | - return self.assert_connect_error( |
310 | - NoConnection("KABOOM!"), EnvironmentPending, |
311 | - "Cannot connect to machine i-exist (perhaps still initializing): " |
312 | - "KABOOM!") |
313 | - |
314 | + """Verify retry of `NoConnection` errors.""" |
315 | + log = self.capture_logging(level=logging.DEBUG) |
316 | + |
317 | + # First connection attempts fails |
318 | + mock_client = self.mocker.patch(SSHClient) |
319 | + mock_client.connect("foo.example.com:2181", timeout=30, share=False) |
320 | + self.mocker.result(fail(NoConnection("KABOOM!"))) |
321 | + |
322 | + # Subsequent connection attempt then succeeds with a valid |
323 | + # (mocked) client that can be waited on. |
324 | + mock_client.connect("foo.example.com:2181", timeout=30, share=False) |
325 | + self.mocker.result(succeed(mock_client)) |
326 | + mock_client.exists_and_watch("/initialized") |
327 | + self.mocker.result((succeed(True), None)) |
328 | + |
329 | + self.mocker.replay() |
330 | + |
331 | + provider = DummyProvider(ProviderMachine("i-exist", "foo.example.com")) |
332 | + yield provider.connect() |
333 | + self.assertIn( |
334 | + "Retrying connection: Cannot connect to environment " |
335 | + "using foo.example.com (perhaps still initializing): KABOOM!", |
336 | + log.getvalue()) |
337 | + |
338 | + @inlineCallbacks |
339 | + def test_not_bootstrapped(self): |
340 | + """Verify failure when the provider has not been bootstrapped.""" |
341 | + provider = NotBootstrappedProvider() |
342 | + e = yield self.assertFailure(provider.connect(), EnvironmentNotFound) |
343 | + self.assertEqual( |
344 | + str(e), |
345 | + "juju environment not found: is the environment bootstrapped?") |
346 | + |
347 | + @inlineCallbacks |
348 | def test_txzookeeper_error(self): |
349 | - """ |
350 | - `ConnectionTimeoutException` errors should become `EnvironmentPending`s |
351 | - """ |
352 | - return self.assert_connect_error( |
353 | - ConnectionTimeoutException("SPLAT!"), EnvironmentPending, |
354 | - "Cannot connect to machine i-exist (perhaps still initializing): " |
355 | - "SPLAT!") |
356 | - |
357 | + """Verify retry of `ConnectionTimeoutException` errors.""" |
358 | + log = self.capture_logging(level=logging.DEBUG) |
359 | + |
360 | + # First connection attempts fails |
361 | + mock_client = self.mocker.patch(SSHClient) |
362 | + mock_client.connect("foo.example.com:2181", timeout=30, share=False) |
363 | + self.mocker.result(fail(ConnectionTimeoutException("SPLAT!"))) |
364 | + |
365 | + # Subsequent connection attempt then succeeds with a valid |
366 | + # (mocked) client that can be waited on. |
367 | + mock_client.connect("foo.example.com:2181", timeout=30, share=False) |
368 | + self.mocker.result(succeed(mock_client)) |
369 | + mock_client.exists_and_watch("/initialized") |
370 | + self.mocker.result((succeed(True), None)) |
371 | + |
372 | + self.mocker.replay() |
373 | + |
374 | + provider = DummyProvider(ProviderMachine("i-exist", "foo.example.com")) |
375 | + yield provider.connect() |
376 | + self.assertIn( |
377 | + "Retrying connection: Cannot connect to environment " |
378 | + "using foo.example.com (perhaps still initializing): SPLAT!", |
379 | + log.getvalue()) |
380 | + |
381 | + @inlineCallbacks |
382 | def test_other_error(self): |
383 | """Other errors should propagate""" |
384 | - return self.assert_connect_error( |
385 | - TypeError("THUD!"), TypeError, "THUD!") |
386 | + log = self.capture_logging() |
387 | + self.mock_connect(False, fail(TypeError("THUD!"))) |
388 | + self.mocker.replay() |
389 | + |
390 | + provider = DummyProvider(ProviderMachine("i-exist", "foo.example.com")) |
391 | + ex = yield self.assertFailure(provider.connect(), TypeError) |
392 | + self.assertEqual(str(ex), "THUD!") |
393 | + self.assertIn( |
394 | + "Cannot connect to environment: THUD!", |
395 | + log.getvalue()) |
396 | |
397 | @inlineCallbacks |
398 | def test_wait_for_initialize(self): |
399 | @@ -107,7 +173,7 @@ |
400 | is not ready, should wait until that state is ready. |
401 | """ |
402 | client = ZookeeperClient() |
403 | - self.client = client # for poke_zk |
404 | + self.client = client # for poke_zk |
405 | self.mock_connect(False, succeed(client)) |
406 | self.mocker.replay() |
407 | |
408 | @@ -131,3 +197,43 @@ |
409 | finally: |
410 | deleteTree("/", client.handle) |
411 | client.close() |
412 | + |
413 | + @inlineCallbacks |
414 | + def test_fast_connection(self): |
415 | + """Verify connection when requirements are available at time of conn. |
416 | + |
417 | + This includes /initialized is already set. In addition, also |
418 | + verifies that if multiple ZKs are available, one is selected |
419 | + via random.choice. |
420 | + """ |
421 | + log = self.capture_logging(level=logging.DEBUG) |
422 | + client = ZookeeperClient() |
423 | + self.mock_connect(False, succeed(client)) |
424 | + |
425 | + def get_choice(lst): |
426 | + for item in lst: |
427 | + if item.dns_name == "foo.example.com": |
428 | + return item |
429 | + raise AssertionError("expected choice not seen") |
430 | + |
431 | + self.patch(random, "choice", get_choice) |
432 | + self.mocker.replay() |
433 | + |
434 | + provider = DummyProvider( |
435 | + ProviderMachine("i-am-picked", "foo.example.com"), |
436 | + ProviderMachine("i-was-not", "bar.example.com")) |
437 | + |
438 | + zookeeper.set_debug_level(0) |
439 | + yield client.connect(get_test_zookeeper_address()) |
440 | + try: |
441 | + yield client.create("/initialized") |
442 | + yield provider.connect() |
443 | + self.assertEqual( |
444 | + "Connecting to environment...\n" |
445 | + "Connecting to environment using foo.example.com...\n" |
446 | + "Environment is initialized.\n" |
447 | + "Connected to environment.\n", |
448 | + log.getvalue()) |
449 | + finally: |
450 | + deleteTree("/", client.handle) |
451 | + client.close() |
452 | |
453 | === removed file 'juju/providers/ec2/tests/test_connect.py' |
454 | --- juju/providers/ec2/tests/test_connect.py 2011-09-15 18:50:23 +0000 |
455 | +++ juju/providers/ec2/tests/test_connect.py 1970-01-01 00:00:00 +0000 |
456 | @@ -1,143 +0,0 @@ |
457 | -from yaml import dump |
458 | - |
459 | -from twisted.internet.defer import inlineCallbacks, succeed, fail |
460 | - |
461 | -import zookeeper |
462 | - |
463 | -from txzookeeper import ZookeeperClient |
464 | -from txzookeeper.client import ConnectionTimeoutException |
465 | -from txzookeeper.tests.utils import deleteTree |
466 | - |
467 | -from juju.lib.testing import TestCase |
468 | - |
469 | -from juju.errors import EnvironmentPending, NoConnection |
470 | -from juju.state.sshclient import SSHClient |
471 | -from juju.providers.ec2.tests.common import EC2TestMixin |
472 | -from juju.tests.common import get_test_zookeeper_address |
473 | - |
474 | - |
475 | -class EC2ConnectTest(EC2TestMixin, TestCase): |
476 | - |
477 | - def mock_find_zookeepers(self, instance): |
478 | - self.s3.get_object(self.env_name, "provider-state") |
479 | - self.mocker.result(succeed(dump( |
480 | - {"zookeeper-instances": ["i-foobar"]}))) |
481 | - self.ec2.describe_instances("i-foobar") |
482 | - self.mocker.result(succeed([instance])) |
483 | - |
484 | - def mock_connect(self, share, instance, result): |
485 | - self.mock_find_zookeepers(instance) |
486 | - client = self.mocker.patch(SSHClient) |
487 | - client.connect("foo.example.com:2181", timeout=30, share=share) |
488 | - self.mocker.result(result) |
489 | - |
490 | - def assert_connect_error(self, error, expect_type, expect_message): |
491 | - instance = self.get_instance("i-foobar", dns_name="foo.example.com") |
492 | - self.mock_connect(False, instance, fail(error)) |
493 | - self.mocker.replay() |
494 | - |
495 | - provider = self.get_provider() |
496 | - d = provider.connect() |
497 | - |
498 | - def check_error(error): |
499 | - self.assertEqual(str(error), expect_message) |
500 | - self.assertFailure(d, expect_type) |
501 | - d.addCallback(check_error) |
502 | - return d |
503 | - |
504 | - def test_no_dns_name(self): |
505 | - """ |
506 | - `EnvironmentPending` should be raised if no zookeeper nodes have dns |
507 | - names |
508 | - """ |
509 | - instance = self.get_instance("i-foobar") |
510 | - self.mock_find_zookeepers(instance) |
511 | - self.mocker.replay() |
512 | - |
513 | - provider = self.get_provider() |
514 | - d = provider.connect() |
515 | - |
516 | - def check_error(error): |
517 | - self.assertEqual( |
518 | - str(error), "No machines have addresses assigned yet") |
519 | - |
520 | - self.assertFailure(d, NoConnection) |
521 | - d.addCallback(check_error) |
522 | - return d |
523 | - |
524 | - def test_provider_connect_forwards_share_option(self): |
525 | - """The `share` kwarg should be passed through to `SSHClient.connect`""" |
526 | - instance = self.get_instance("i-foobar", dns_name="foo.example.com") |
527 | - connected_client = self.mocker.mock(type=SSHClient) |
528 | - self.mock_connect(True, instance, succeed(connected_client)) |
529 | - # We'll test the wait on initialization separately. |
530 | - connected_client.exists_and_watch("/initialized") |
531 | - self.mocker.result((succeed(True), None)) |
532 | - self.mocker.replay() |
533 | - |
534 | - provider = self.get_provider() |
535 | - d = provider.connect(share=True) |
536 | - |
537 | - def verify_result(result): |
538 | - self.assertIdentical(connected_client, result) |
539 | - d.addCallback(verify_result) |
540 | - return d |
541 | - |
542 | - def test_no_connection(self): |
543 | - """`NoConnection` errors should become `EnvironmentPending`s""" |
544 | - return self.assert_connect_error( |
545 | - NoConnection("KABOOM!"), EnvironmentPending, |
546 | - "Cannot connect to machine i-foobar (perhaps still initializing): " |
547 | - "KABOOM!") |
548 | - |
549 | - def test_txzookeeper_error(self): |
550 | - """ |
551 | - `ConnectionTimeoutException` errors should become `EnvironmentPending`s |
552 | - """ |
553 | - return self.assert_connect_error( |
554 | - ConnectionTimeoutException("SPLAT!"), EnvironmentPending, |
555 | - "Cannot connect to machine i-foobar (perhaps still initializing): " |
556 | - "SPLAT!") |
557 | - |
558 | - def test_other_error(self): |
559 | - """Other errors should propagate""" |
560 | - return self.assert_connect_error( |
561 | - TypeError("THUD!"), TypeError, "THUD!") |
562 | - |
563 | - @inlineCallbacks |
564 | - def test_provider_connect_waits_on_initialization(self): |
565 | - """ |
566 | - A connection to a zookeeper that is running, but whose juju state |
567 | - is not ready, should wait until that state is ready. |
568 | - """ |
569 | - # Hand back a real connected client to test the wait on initialization. |
570 | - instance = self.get_instance("i-foobar", dns_name="foo.example.com") |
571 | - connected_client = ZookeeperClient() |
572 | - self.client = connected_client # for poke_zk |
573 | - self.mock_connect(False, instance, succeed(connected_client)) |
574 | - |
575 | - self.mocker.replay() |
576 | - |
577 | - zookeeper.set_debug_level(0) |
578 | - yield connected_client.connect(get_test_zookeeper_address()) |
579 | - |
580 | - client_result = [] |
581 | - |
582 | - provider = self.get_provider() |
583 | - client_deferred = provider.connect() |
584 | - client_deferred.addCallback(client_result.append) |
585 | - |
586 | - # Give it a chance to do it incorrectly. |
587 | - yield self.poke_zk() |
588 | - |
589 | - try: |
590 | - self.assertEquals(client_result, []) |
591 | - |
592 | - yield connected_client.create("/initialized") |
593 | - |
594 | - yield client_deferred |
595 | - self.assertTrue(client_result, client_result) |
596 | - self.assertIdentical(client_result[0], connected_client) |
597 | - finally: |
598 | - deleteTree("/", connected_client.handle) |
599 | - connected_client.close() |
600 | |
601 | === removed file 'juju/providers/orchestra/tests/test_connect.py' |
602 | --- juju/providers/orchestra/tests/test_connect.py 2011-10-17 13:49:47 +0000 |
603 | +++ juju/providers/orchestra/tests/test_connect.py 1970-01-01 00:00:00 +0000 |
604 | @@ -1,111 +0,0 @@ |
605 | -from yaml import dump |
606 | - |
607 | -from twisted.internet.defer import fail, inlineCallbacks, succeed |
608 | - |
609 | -import zookeeper |
610 | - |
611 | -from txzookeeper import ZookeeperClient |
612 | -from txzookeeper.client import ConnectionTimeoutException |
613 | -from txzookeeper.tests.utils import deleteTree |
614 | - |
615 | -from juju.errors import EnvironmentPending, NoConnection |
616 | -from juju.lib.testing import TestCase |
617 | -from juju.state.sshclient import SSHClient |
618 | -from juju.tests.common import get_test_zookeeper_address |
619 | - |
620 | -from .common import OrchestraTestMixin |
621 | - |
622 | - |
623 | -class ConnectTest(TestCase, OrchestraTestMixin): |
624 | - |
625 | - def mock_connect(self, share, result): |
626 | - self.setup_mocks() |
627 | - self.mock_fs_get( |
628 | - "http://somewhe.re/webdav/provider-state", 200, dump( |
629 | - {"zookeeper-instances": ["foo"]})) |
630 | - self.proxy_m.callRemote("get_systems") |
631 | - self.mocker.result(succeed([{ |
632 | - "uid": "foo", "name": "foo.example.com", |
633 | - "mgmt_classes": ["acquired"], "netboot_enabled": True}])) |
634 | - client = self.mocker.patch(SSHClient) |
635 | - client.connect("foo.example.com:2181", timeout=30, share=share) |
636 | - self.mocker.result(result) |
637 | - |
638 | - def assert_connect_error(self, error, expect_type, expect_message): |
639 | - self.mock_connect(False, fail(error)) |
640 | - self.mocker.replay() |
641 | - |
642 | - d = self.get_provider().connect() |
643 | - |
644 | - def check_error(error): |
645 | - self.assertEqual(str(error), expect_message) |
646 | - self.assertFailure(d, expect_type) |
647 | - d.addCallback(check_error) |
648 | - return d |
649 | - |
650 | - def test_share_kwarg(self): |
651 | - """The `share` kwarg should be passed through to `SSHClient.connect`""" |
652 | - client = self.mocker.mock(type=SSHClient) |
653 | - self.mock_connect(True, succeed(client)) |
654 | - client.exists_and_watch("/initialized") |
655 | - self.mocker.result((succeed(True), None)) |
656 | - self.mocker.replay() |
657 | - |
658 | - d = self.get_provider().connect(share=True) |
659 | - |
660 | - def verify(result): |
661 | - self.assertIdentical(result, client) |
662 | - d.addCallback(verify) |
663 | - return d |
664 | - |
665 | - def test_no_connection(self): |
666 | - """`NoConnection` errors should become `EnvironmentPending`s""" |
667 | - return self.assert_connect_error( |
668 | - NoConnection("KABOOM!"), EnvironmentPending, |
669 | - "Cannot connect to machine foo (perhaps still initializing): " |
670 | - "KABOOM!") |
671 | - |
672 | - def test_txzookeeper_error(self): |
673 | - """ |
674 | - `ConnectionTimeoutException` errors should become `EnvironmentPending`s |
675 | - """ |
676 | - return self.assert_connect_error( |
677 | - ConnectionTimeoutException("SPLAT!"), EnvironmentPending, |
678 | - "Cannot connect to machine foo (perhaps still initializing): " |
679 | - "SPLAT!") |
680 | - |
681 | - def test_other_error(self): |
682 | - """Other errors should propagate""" |
683 | - return self.assert_connect_error( |
684 | - TypeError("THUD!"), TypeError, "THUD!") |
685 | - |
686 | - @inlineCallbacks |
687 | - def test_wait_for_initialize(self): |
688 | - """ |
689 | - A connection to a zookeeper that is running, but whose juju state |
690 | - is not ready, should wait until that state is ready. |
691 | - """ |
692 | - client = ZookeeperClient() |
693 | - self.client = client # for poke_zk |
694 | - self.mock_connect(False, succeed(client)) |
695 | - self.mocker.replay() |
696 | - |
697 | - zookeeper.set_debug_level(0) |
698 | - yield client.connect(get_test_zookeeper_address()) |
699 | - |
700 | - d = self.get_provider().connect() |
701 | - client_result = [] |
702 | - d.addCallback(client_result.append) |
703 | - |
704 | - # Give it an opportunity to do it incorrectly. |
705 | - yield self.poke_zk() |
706 | - |
707 | - try: |
708 | - self.assertEquals(client_result, []) |
709 | - yield client.create("/initialized") |
710 | - yield d |
711 | - self.assertTrue(client_result, client_result) |
712 | - self.assertIdentical(client_result[0], client) |
713 | - finally: |
714 | - deleteTree("/", client.handle) |
715 | - client.close() |
Can't see anything to complain about at all. +1.