Merge lp:~jimbaker/pyjuju/robust-zk-connect into lp:pyjuju

Proposed by Jim Baker
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
Reviewer Review Type Date Requested Status
Kapil Thangavelu (community) Approve
William Reade (community) Approve
Review via email: mp+82768@code.launchpad.net

Description of the change

This branch implements the change in the API, per this mailing list discussion:
https://lists.ubuntu.com/archives/juju/2011-November/000990.html

Some things not covered in this branch:

Documentation of the rationale, as requested by https://lists.ubuntu.com/archives/juju/2011-November/001010.html since However, there's not anything documenting this in general, so it's a bit larger in scope. Instead this should be covered in a separate doc branch.

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=DEBUG_ZK).

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.common.tests.test_connect, which is in fact common. So updating these tests to take in account that EnvironmentPending indicates that a retry should be attempted, or address a couple of gaps in testing, didn't make sense.

To post a comment you must log in.
Revision history for this message
William Reade (fwereade) wrote :

Can't see anything to complain about at all. +1.

review: Approve
Revision history for this message
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://paste.ubuntu.com/762451/
normal mode -> http://paste.ubuntu.com/762452/

[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,053:13891(0xb6500b70):ZOO_ERROR@handle_socket_error_msg@1579: Socket [127.0.0.1:35354] zk retcode=-4, errno=111(Connection refused): server refused to accept the client
Unhandled error in Deferred:
Unhandled error in Deferred:
Unhandled Error
Traceback (most recent call last):
Failure: txzookeeper.client.ConnectionTimeoutException: could not connect before timeout

it would be nice if this could be resolved as well in this branch, i
tracked it down to the called_aware_deferred_chain returning
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://pastebin.ubuntu.com/761938/

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

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

Revision history for this message
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.

Revision history for this message
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.

Revision history for this message
Kapil Thangavelu (hazmat) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'juju/lib/tests/test_twistutils.py'
--- juju/lib/tests/test_twistutils.py 2011-09-15 18:50:23 +0000
+++ juju/lib/tests/test_twistutils.py 2011-12-08 20:20:29 +0000
@@ -1,4 +1,5 @@
1import os1import os
2import time
23
3from twisted.internet.defer import (4from twisted.internet.defer import (
4 succeed, fail, Deferred, DeferredList, inlineCallbacks, returnValue)5 succeed, fail, Deferred, DeferredList, inlineCallbacks, returnValue)
@@ -8,7 +9,7 @@
89
9from juju.lib.testing import TestCase10from juju.lib.testing import TestCase
10from juju.lib.twistutils import (11from juju.lib.twistutils import (
11 concurrent_execution_guard, gather_results, get_module_directory)12 concurrent_execution_guard, gather_results, get_module_directory, sleep)
1213
1314
14class Bar(object):15class Bar(object):
@@ -144,3 +145,13 @@
144 self.assertIn("juju", directory)145 self.assertIn("juju", directory)
145 self.assertNotIn("_trial_temp", directory)146 self.assertNotIn("_trial_temp", directory)
146 self.assertTrue(os.path.isdir(directory))147 self.assertTrue(os.path.isdir(directory))
148
149
150class SleepTest(TestCase):
151
152 @inlineCallbacks
153 def test_sleep(self):
154 """Directly test deferred sleep."""
155 start = time.time()
156 yield sleep(0.1)
157 self.assertGreaterEqual(time.time() - start, 0.1)
147158
=== modified file 'juju/lib/twistutils.py'
--- juju/lib/twistutils.py 2011-02-15 15:15:16 +0000
+++ juju/lib/twistutils.py 2011-12-08 20:20:29 +0000
@@ -1,7 +1,9 @@
1import inspect1import inspect
2import os2import os
33
4from twisted.internet.defer import maybeDeferred, succeed, DeferredList4from twisted.internet import reactor
5from twisted.internet.defer import (
6 Deferred, maybeDeferred, succeed, DeferredList)
5from twisted.python.util import mergeFunctionMetadata7from twisted.python.util import mergeFunctionMetadata
68
79
@@ -53,3 +55,15 @@
53 """55 """
54 return os.path.abspath(os.path.dirname(inspect.getabsfile(module)).replace(56 return os.path.abspath(os.path.dirname(inspect.getabsfile(module)).replace(
55 "/_trial_temp", ""))57 "/_trial_temp", ""))
58
59
60def sleep(delay):
61 """Non-blocking sleep.
62
63 :param int delay: time in seconds to sleep.
64 :return: a Deferred that fires after the desired delay.
65 :rtype: :class:`twisted.internet.defer.Deferred`
66 """
67 deferred = Deferred()
68 reactor.callLater(delay, deferred.callback, None)
69 return deferred
5670
=== modified file 'juju/providers/common/base.py'
--- juju/providers/common/base.py 2011-09-28 09:48:30 +0000
+++ juju/providers/common/base.py 2011-12-08 20:20:29 +0000
@@ -148,8 +148,6 @@
148148
149 :raises: :exc:`juju.errors.EnvironmentNotFound` when no zookeepers149 :raises: :exc:`juju.errors.EnvironmentNotFound` when no zookeepers
150 exist150 exist
151 :raises: :exc:`juju.errors.EnvironmentPending` when zookeepers
152 exist but connection attempt fails
153 """151 """
154 return ZookeeperConnect(self).run(share=share)152 return ZookeeperConnect(self).run(share=share)
155153
156154
=== modified file 'juju/providers/common/connect.py'
--- juju/providers/common/connect.py 2011-09-16 19:37:15 +0000
+++ juju/providers/common/connect.py 2011-12-08 20:20:29 +0000
@@ -1,8 +1,11 @@
1import random
2
1from twisted.internet.defer import inlineCallbacks, returnValue3from twisted.internet.defer import inlineCallbacks, returnValue
24
3from txzookeeper.client import ConnectionTimeoutException5from txzookeeper.client import ConnectionTimeoutException
46
5from juju.errors import EnvironmentPending, NoConnection7from juju.errors import EnvironmentNotFound, EnvironmentPending, NoConnection
8from juju.lib.twistutils import sleep
6from juju.state.sshclient import SSHClient9from juju.state.sshclient import SSHClient
710
8from .utils import log11from .utils import log
@@ -15,51 +18,69 @@
1518
16 @inlineCallbacks19 @inlineCallbacks
17 def run(self, share=False):20 def run(self, share=False):
18 """Attempt to connect to a running zookeeper node.21 """Attempt to connect to a running zookeeper node, retrying as needed.
1922
20 :param bool share: where feasible, attempt to share a connection with23 :param bool share: where feasible, attempt to share a connection with
21 other clients24 other clients.
2225
23 :return: an open :class:`txzookeeper.client.ZookeeperClient`26 :return: an open :class:`txzookeeper.client.ZookeeperClient`
24 :rtype: :class:`twisted.internet.defer.Deferred`27 :rtype: :class:`twisted.internet.defer.Deferred`
2528
26 :raises: :exc:`juju.errors.EnvironmentNotFound` when no zookeepers29 :raises: :exc:`juju.errors.EnvironmentNotFound` when no zookeepers
27 exist30 exist
28 :raises: :exc:`juju.errors.EnvironmentPending` when zookeepers31
29 exist but connection attempt fails32 Internally this method catches all
33 :exc:`juju.errors.EnvironmentPending`, since
34 this exception explicitly means that a retry is feasible.
35
36 TODO consider supporting a timeout for this method, instead of
37 any such timeouts being done externally.
30 """38 """
39 log.info("Connecting to environment...")
40 while True:
41 try:
42 client = yield self._internal_connect(share)
43 log.info("Connected to environment.")
44 returnValue(client)
45 except EnvironmentPending as e:
46 log.debug("Retrying connection: %s", e)
47 except EnvironmentNotFound:
48 # Expected if not bootstrapped, simply raise up
49 raise
50 except Exception as e:
51 # Otherwise this is unexpected, log with some details
52 log.exception("Cannot connect to environment: %s", e)
53 raise
54
55 @inlineCallbacks
56 def _internal_connect(self, share):
57 """Attempt connection to one of the ZK nodes."""
31 candidates = yield self._provider.get_zookeeper_machines()58 candidates = yield self._provider.get_zookeeper_machines()
32 chosen = yield self._pick_machine(candidates)59 assigned = [machine for machine in candidates if machine.dns_name]
33 client = yield self._connect_to_machine(chosen, share)60 if not assigned:
61 yield sleep(1) # Momentarily backoff
62 raise EnvironmentPending("No machines have assigned addresses")
63
64 chosen = random.choice(assigned)
65 log.debug("Connecting to environment using %s...", chosen.dns_name)
66 try:
67 client = yield SSHClient().connect(
68 chosen.dns_name + ":2181", timeout=30, share=share)
69 except (NoConnection, ConnectionTimeoutException) as e:
70 raise EnvironmentPending(
71 "Cannot connect to environment using %s "
72 "(perhaps still initializing): %s" % (
73 chosen.dns_name, str(e)))
74
34 yield self.wait_for_initialization(client)75 yield self.wait_for_initialization(client)
35 returnValue(client)76 returnValue(client)
3677
37 def _pick_machine(self, machines):
38 # TODO Should we pick a random entry from the nodes list?
39 for machine in machines:
40 if machine.dns_name:
41 return machine
42 raise EnvironmentPending("No machines have addresses assigned yet")
43
44 def _connect_to_machine(self, machine, share):
45 log.info("Connecting to environment.")
46 result = SSHClient().connect(
47 machine.dns_name + ":2181", timeout=30, share=share)
48 result.addErrback(self._cannot_connect, machine)
49 return result
50
51 def _cannot_connect(self, failure, machine):
52 failure.trap(NoConnection, ConnectionTimeoutException)
53 raise EnvironmentPending(
54 "Cannot connect to machine %s (perhaps still initializing): %s"
55 % (machine.instance_id, str(failure.value)))
56
57 @inlineCallbacks78 @inlineCallbacks
58 def wait_for_initialization(self, client):79 def wait_for_initialization(self, client):
59 exists_d, watch_d = client.exists_and_watch("/initialized")80 exists_d, watch_d = client.exists_and_watch("/initialized")
60 exists = yield exists_d81 exists = yield exists_d
61 if not exists:82 if not exists:
62 log.info("Environment still initializing. Will wait.")83 log.debug("Environment still initializing. Will wait.")
63 yield watch_d84 yield watch_d
64 else:85 else:
65 log.debug("Environment already initialized.")86 log.debug("Environment is initialized.")
6687
=== modified file 'juju/providers/common/tests/test_connect.py'
--- juju/providers/common/tests/test_connect.py 2011-09-15 18:50:23 +0000
+++ juju/providers/common/tests/test_connect.py 2011-12-08 20:20:29 +0000
@@ -1,3 +1,6 @@
1import logging
2import random
3
1from twisted.internet.defer import fail, inlineCallbacks, succeed4from twisted.internet.defer import fail, inlineCallbacks, succeed
25
3import zookeeper6import zookeeper
@@ -6,7 +9,7 @@
6from txzookeeper.client import ConnectionTimeoutException9from txzookeeper.client import ConnectionTimeoutException
7from txzookeeper.tests.utils import deleteTree10from txzookeeper.tests.utils import deleteTree
811
9from juju.errors import EnvironmentPending, NoConnection12from juju.errors import EnvironmentNotFound, NoConnection
10from juju.lib.testing import TestCase13from juju.lib.testing import TestCase
11from juju.machine import ProviderMachine14from juju.machine import ProviderMachine
12from juju.providers.common.base import MachineProviderBase15from juju.providers.common.base import MachineProviderBase
@@ -16,16 +19,26 @@
1619
17class DummyProvider(MachineProviderBase):20class DummyProvider(MachineProviderBase):
1821
19 def __init__(self, second_zookeeeper):22 def __init__(self, *zookeepers):
20 self._second_zookeeper = second_zookeeeper23 self._zookeepers = zookeepers
2124
22 def get_zookeeper_machines(self):25 def get_zookeeper_machines(self):
23 """26 """
24 Return a pair of possible zookeepers, the first of which is invalid27 Return a pair of possible zookeepers, the first of which is invalid
25 """28 """
26 return succeed([29 machines = [ProviderMachine("i-havenodns")]
27 ProviderMachine("i-havenodns"),30 machines.extend(self._zookeepers)
28 self._second_zookeeper])31 return succeed(machines)
32
33
34class NotBootstrappedProvider(MachineProviderBase):
35 """Pretend to be an environment that has not been bootstrapped."""
36
37 def __init__(self):
38 pass
39
40 def get_zookeeper_machines(self):
41 return fail(EnvironmentNotFound("is the environment bootstrapped?"))
2942
3043
31class ConnectTest(TestCase):44class ConnectTest(TestCase):
@@ -35,33 +48,32 @@
35 client.connect("foo.example.com:2181", timeout=30, share=share)48 client.connect("foo.example.com:2181", timeout=30, share=share)
36 self.mocker.result(result)49 self.mocker.result(result)
3750
38 def assert_connect_error(self, error, expect_type, expect_message):51 @inlineCallbacks
39 self.mock_connect(False, fail(error))52 def test_none_have_dns_at_first(self):
53 """Verify retry of ZK machine that at first doesn't have a DNS name."""
54 log = self.capture_logging(level=logging.DEBUG)
55 mocked_machine = self.mocker.proxy(ProviderMachine(
56 "i-have-no-dns-at-first"))
57 mocked_machine.dns_name
58 self.mocker.result(None)
59 mocked_machine.dns_name
60 self.mocker.count(0, None)
61 self.mocker.result("foo.example.com") # name assumed by mock_connect
62
63 # Provide for a valid mocked client once connected
64 client = self.mocker.mock(type=SSHClient)
65 self.mock_connect(True, succeed(client))
66 client.exists_and_watch("/initialized")
67 self.mocker.result((succeed(True), None))
68
40 self.mocker.replay()69 self.mocker.replay()
4170
42 provider = DummyProvider(ProviderMachine("i-exist", "foo.example.com"))71 provider = DummyProvider(mocked_machine)
43 d = provider.connect()72 client = yield provider.connect(share=True)
4473
45 def check_error(error):74 self.assertIn(
46 self.assertEqual(str(error), expect_message)75 "Retrying connection: No machines have assigned addresses",
47 self.assertFailure(d, expect_type)76 log.getvalue())
48 d.addCallback(check_error)
49 return d
50
51 def test_none_have_dns(self):
52 """
53 `EnvironmentPending` should be raised if no zookeeper nodes have dns
54 names
55 """
56 provider = DummyProvider(ProviderMachine("i-havenodnseither"))
57 d = provider.connect()
58 self.assertFailure(d, EnvironmentPending)
59
60 def check(error):
61 self.assertEquals(
62 str(error), "No machines have addresses assigned yet")
63 d.addCallback(check)
64 return d
6577
66 def test_share_kwarg(self):78 def test_share_kwarg(self):
67 """The `share` kwarg should be passed through to `SSHClient.connect`"""79 """The `share` kwarg should be passed through to `SSHClient.connect`"""
@@ -79,26 +91,80 @@
79 d.addCallback(verify)91 d.addCallback(verify)
80 return d92 return d
8193
94 @inlineCallbacks
82 def test_no_connection(self):95 def test_no_connection(self):
83 """`NoConnection` errors should become `EnvironmentPending`s"""96 """Verify retry of `NoConnection` errors."""
84 return self.assert_connect_error(97 log = self.capture_logging(level=logging.DEBUG)
85 NoConnection("KABOOM!"), EnvironmentPending,98
86 "Cannot connect to machine i-exist (perhaps still initializing): "99 # First connection attempts fails
87 "KABOOM!")100 mock_client = self.mocker.patch(SSHClient)
88101 mock_client.connect("foo.example.com:2181", timeout=30, share=False)
102 self.mocker.result(fail(NoConnection("KABOOM!")))
103
104 # Subsequent connection attempt then succeeds with a valid
105 # (mocked) client that can be waited on.
106 mock_client.connect("foo.example.com:2181", timeout=30, share=False)
107 self.mocker.result(succeed(mock_client))
108 mock_client.exists_and_watch("/initialized")
109 self.mocker.result((succeed(True), None))
110
111 self.mocker.replay()
112
113 provider = DummyProvider(ProviderMachine("i-exist", "foo.example.com"))
114 yield provider.connect()
115 self.assertIn(
116 "Retrying connection: Cannot connect to environment "
117 "using foo.example.com (perhaps still initializing): KABOOM!",
118 log.getvalue())
119
120 @inlineCallbacks
121 def test_not_bootstrapped(self):
122 """Verify failure when the provider has not been bootstrapped."""
123 provider = NotBootstrappedProvider()
124 e = yield self.assertFailure(provider.connect(), EnvironmentNotFound)
125 self.assertEqual(
126 str(e),
127 "juju environment not found: is the environment bootstrapped?")
128
129 @inlineCallbacks
89 def test_txzookeeper_error(self):130 def test_txzookeeper_error(self):
90 """131 """Verify retry of `ConnectionTimeoutException` errors."""
91 `ConnectionTimeoutException` errors should become `EnvironmentPending`s132 log = self.capture_logging(level=logging.DEBUG)
92 """133
93 return self.assert_connect_error(134 # First connection attempts fails
94 ConnectionTimeoutException("SPLAT!"), EnvironmentPending,135 mock_client = self.mocker.patch(SSHClient)
95 "Cannot connect to machine i-exist (perhaps still initializing): "136 mock_client.connect("foo.example.com:2181", timeout=30, share=False)
96 "SPLAT!")137 self.mocker.result(fail(ConnectionTimeoutException("SPLAT!")))
97138
139 # Subsequent connection attempt then succeeds with a valid
140 # (mocked) client that can be waited on.
141 mock_client.connect("foo.example.com:2181", timeout=30, share=False)
142 self.mocker.result(succeed(mock_client))
143 mock_client.exists_and_watch("/initialized")
144 self.mocker.result((succeed(True), None))
145
146 self.mocker.replay()
147
148 provider = DummyProvider(ProviderMachine("i-exist", "foo.example.com"))
149 yield provider.connect()
150 self.assertIn(
151 "Retrying connection: Cannot connect to environment "
152 "using foo.example.com (perhaps still initializing): SPLAT!",
153 log.getvalue())
154
155 @inlineCallbacks
98 def test_other_error(self):156 def test_other_error(self):
99 """Other errors should propagate"""157 """Other errors should propagate"""
100 return self.assert_connect_error(158 log = self.capture_logging()
101 TypeError("THUD!"), TypeError, "THUD!")159 self.mock_connect(False, fail(TypeError("THUD!")))
160 self.mocker.replay()
161
162 provider = DummyProvider(ProviderMachine("i-exist", "foo.example.com"))
163 ex = yield self.assertFailure(provider.connect(), TypeError)
164 self.assertEqual(str(ex), "THUD!")
165 self.assertIn(
166 "Cannot connect to environment: THUD!",
167 log.getvalue())
102168
103 @inlineCallbacks169 @inlineCallbacks
104 def test_wait_for_initialize(self):170 def test_wait_for_initialize(self):
@@ -107,7 +173,7 @@
107 is not ready, should wait until that state is ready.173 is not ready, should wait until that state is ready.
108 """174 """
109 client = ZookeeperClient()175 client = ZookeeperClient()
110 self.client = client # for poke_zk176 self.client = client # for poke_zk
111 self.mock_connect(False, succeed(client))177 self.mock_connect(False, succeed(client))
112 self.mocker.replay()178 self.mocker.replay()
113179
@@ -131,3 +197,43 @@
131 finally:197 finally:
132 deleteTree("/", client.handle)198 deleteTree("/", client.handle)
133 client.close()199 client.close()
200
201 @inlineCallbacks
202 def test_fast_connection(self):
203 """Verify connection when requirements are available at time of conn.
204
205 This includes /initialized is already set. In addition, also
206 verifies that if multiple ZKs are available, one is selected
207 via random.choice.
208 """
209 log = self.capture_logging(level=logging.DEBUG)
210 client = ZookeeperClient()
211 self.mock_connect(False, succeed(client))
212
213 def get_choice(lst):
214 for item in lst:
215 if item.dns_name == "foo.example.com":
216 return item
217 raise AssertionError("expected choice not seen")
218
219 self.patch(random, "choice", get_choice)
220 self.mocker.replay()
221
222 provider = DummyProvider(
223 ProviderMachine("i-am-picked", "foo.example.com"),
224 ProviderMachine("i-was-not", "bar.example.com"))
225
226 zookeeper.set_debug_level(0)
227 yield client.connect(get_test_zookeeper_address())
228 try:
229 yield client.create("/initialized")
230 yield provider.connect()
231 self.assertEqual(
232 "Connecting to environment...\n"
233 "Connecting to environment using foo.example.com...\n"
234 "Environment is initialized.\n"
235 "Connected to environment.\n",
236 log.getvalue())
237 finally:
238 deleteTree("/", client.handle)
239 client.close()
134240
=== removed file 'juju/providers/ec2/tests/test_connect.py'
--- juju/providers/ec2/tests/test_connect.py 2011-09-15 18:50:23 +0000
+++ juju/providers/ec2/tests/test_connect.py 1970-01-01 00:00:00 +0000
@@ -1,143 +0,0 @@
1from yaml import dump
2
3from twisted.internet.defer import inlineCallbacks, succeed, fail
4
5import zookeeper
6
7from txzookeeper import ZookeeperClient
8from txzookeeper.client import ConnectionTimeoutException
9from txzookeeper.tests.utils import deleteTree
10
11from juju.lib.testing import TestCase
12
13from juju.errors import EnvironmentPending, NoConnection
14from juju.state.sshclient import SSHClient
15from juju.providers.ec2.tests.common import EC2TestMixin
16from juju.tests.common import get_test_zookeeper_address
17
18
19class EC2ConnectTest(EC2TestMixin, TestCase):
20
21 def mock_find_zookeepers(self, instance):
22 self.s3.get_object(self.env_name, "provider-state")
23 self.mocker.result(succeed(dump(
24 {"zookeeper-instances": ["i-foobar"]})))
25 self.ec2.describe_instances("i-foobar")
26 self.mocker.result(succeed([instance]))
27
28 def mock_connect(self, share, instance, result):
29 self.mock_find_zookeepers(instance)
30 client = self.mocker.patch(SSHClient)
31 client.connect("foo.example.com:2181", timeout=30, share=share)
32 self.mocker.result(result)
33
34 def assert_connect_error(self, error, expect_type, expect_message):
35 instance = self.get_instance("i-foobar", dns_name="foo.example.com")
36 self.mock_connect(False, instance, fail(error))
37 self.mocker.replay()
38
39 provider = self.get_provider()
40 d = provider.connect()
41
42 def check_error(error):
43 self.assertEqual(str(error), expect_message)
44 self.assertFailure(d, expect_type)
45 d.addCallback(check_error)
46 return d
47
48 def test_no_dns_name(self):
49 """
50 `EnvironmentPending` should be raised if no zookeeper nodes have dns
51 names
52 """
53 instance = self.get_instance("i-foobar")
54 self.mock_find_zookeepers(instance)
55 self.mocker.replay()
56
57 provider = self.get_provider()
58 d = provider.connect()
59
60 def check_error(error):
61 self.assertEqual(
62 str(error), "No machines have addresses assigned yet")
63
64 self.assertFailure(d, NoConnection)
65 d.addCallback(check_error)
66 return d
67
68 def test_provider_connect_forwards_share_option(self):
69 """The `share` kwarg should be passed through to `SSHClient.connect`"""
70 instance = self.get_instance("i-foobar", dns_name="foo.example.com")
71 connected_client = self.mocker.mock(type=SSHClient)
72 self.mock_connect(True, instance, succeed(connected_client))
73 # We'll test the wait on initialization separately.
74 connected_client.exists_and_watch("/initialized")
75 self.mocker.result((succeed(True), None))
76 self.mocker.replay()
77
78 provider = self.get_provider()
79 d = provider.connect(share=True)
80
81 def verify_result(result):
82 self.assertIdentical(connected_client, result)
83 d.addCallback(verify_result)
84 return d
85
86 def test_no_connection(self):
87 """`NoConnection` errors should become `EnvironmentPending`s"""
88 return self.assert_connect_error(
89 NoConnection("KABOOM!"), EnvironmentPending,
90 "Cannot connect to machine i-foobar (perhaps still initializing): "
91 "KABOOM!")
92
93 def test_txzookeeper_error(self):
94 """
95 `ConnectionTimeoutException` errors should become `EnvironmentPending`s
96 """
97 return self.assert_connect_error(
98 ConnectionTimeoutException("SPLAT!"), EnvironmentPending,
99 "Cannot connect to machine i-foobar (perhaps still initializing): "
100 "SPLAT!")
101
102 def test_other_error(self):
103 """Other errors should propagate"""
104 return self.assert_connect_error(
105 TypeError("THUD!"), TypeError, "THUD!")
106
107 @inlineCallbacks
108 def test_provider_connect_waits_on_initialization(self):
109 """
110 A connection to a zookeeper that is running, but whose juju state
111 is not ready, should wait until that state is ready.
112 """
113 # Hand back a real connected client to test the wait on initialization.
114 instance = self.get_instance("i-foobar", dns_name="foo.example.com")
115 connected_client = ZookeeperClient()
116 self.client = connected_client # for poke_zk
117 self.mock_connect(False, instance, succeed(connected_client))
118
119 self.mocker.replay()
120
121 zookeeper.set_debug_level(0)
122 yield connected_client.connect(get_test_zookeeper_address())
123
124 client_result = []
125
126 provider = self.get_provider()
127 client_deferred = provider.connect()
128 client_deferred.addCallback(client_result.append)
129
130 # Give it a chance to do it incorrectly.
131 yield self.poke_zk()
132
133 try:
134 self.assertEquals(client_result, [])
135
136 yield connected_client.create("/initialized")
137
138 yield client_deferred
139 self.assertTrue(client_result, client_result)
140 self.assertIdentical(client_result[0], connected_client)
141 finally:
142 deleteTree("/", connected_client.handle)
143 connected_client.close()
1440
=== removed file 'juju/providers/orchestra/tests/test_connect.py'
--- juju/providers/orchestra/tests/test_connect.py 2011-10-17 13:49:47 +0000
+++ juju/providers/orchestra/tests/test_connect.py 1970-01-01 00:00:00 +0000
@@ -1,111 +0,0 @@
1from yaml import dump
2
3from twisted.internet.defer import fail, inlineCallbacks, succeed
4
5import zookeeper
6
7from txzookeeper import ZookeeperClient
8from txzookeeper.client import ConnectionTimeoutException
9from txzookeeper.tests.utils import deleteTree
10
11from juju.errors import EnvironmentPending, NoConnection
12from juju.lib.testing import TestCase
13from juju.state.sshclient import SSHClient
14from juju.tests.common import get_test_zookeeper_address
15
16from .common import OrchestraTestMixin
17
18
19class ConnectTest(TestCase, OrchestraTestMixin):
20
21 def mock_connect(self, share, result):
22 self.setup_mocks()
23 self.mock_fs_get(
24 "http://somewhe.re/webdav/provider-state", 200, dump(
25 {"zookeeper-instances": ["foo"]}))
26 self.proxy_m.callRemote("get_systems")
27 self.mocker.result(succeed([{
28 "uid": "foo", "name": "foo.example.com",
29 "mgmt_classes": ["acquired"], "netboot_enabled": True}]))
30 client = self.mocker.patch(SSHClient)
31 client.connect("foo.example.com:2181", timeout=30, share=share)
32 self.mocker.result(result)
33
34 def assert_connect_error(self, error, expect_type, expect_message):
35 self.mock_connect(False, fail(error))
36 self.mocker.replay()
37
38 d = self.get_provider().connect()
39
40 def check_error(error):
41 self.assertEqual(str(error), expect_message)
42 self.assertFailure(d, expect_type)
43 d.addCallback(check_error)
44 return d
45
46 def test_share_kwarg(self):
47 """The `share` kwarg should be passed through to `SSHClient.connect`"""
48 client = self.mocker.mock(type=SSHClient)
49 self.mock_connect(True, succeed(client))
50 client.exists_and_watch("/initialized")
51 self.mocker.result((succeed(True), None))
52 self.mocker.replay()
53
54 d = self.get_provider().connect(share=True)
55
56 def verify(result):
57 self.assertIdentical(result, client)
58 d.addCallback(verify)
59 return d
60
61 def test_no_connection(self):
62 """`NoConnection` errors should become `EnvironmentPending`s"""
63 return self.assert_connect_error(
64 NoConnection("KABOOM!"), EnvironmentPending,
65 "Cannot connect to machine foo (perhaps still initializing): "
66 "KABOOM!")
67
68 def test_txzookeeper_error(self):
69 """
70 `ConnectionTimeoutException` errors should become `EnvironmentPending`s
71 """
72 return self.assert_connect_error(
73 ConnectionTimeoutException("SPLAT!"), EnvironmentPending,
74 "Cannot connect to machine foo (perhaps still initializing): "
75 "SPLAT!")
76
77 def test_other_error(self):
78 """Other errors should propagate"""
79 return self.assert_connect_error(
80 TypeError("THUD!"), TypeError, "THUD!")
81
82 @inlineCallbacks
83 def test_wait_for_initialize(self):
84 """
85 A connection to a zookeeper that is running, but whose juju state
86 is not ready, should wait until that state is ready.
87 """
88 client = ZookeeperClient()
89 self.client = client # for poke_zk
90 self.mock_connect(False, succeed(client))
91 self.mocker.replay()
92
93 zookeeper.set_debug_level(0)
94 yield client.connect(get_test_zookeeper_address())
95
96 d = self.get_provider().connect()
97 client_result = []
98 d.addCallback(client_result.append)
99
100 # Give it an opportunity to do it incorrectly.
101 yield self.poke_zk()
102
103 try:
104 self.assertEquals(client_result, [])
105 yield client.create("/initialized")
106 yield d
107 self.assertTrue(client_result, client_result)
108 self.assertIdentical(client_result[0], client)
109 finally:
110 deleteTree("/", client.handle)
111 client.close()

Subscribers

People subscribed via source and target branches

to status/vote changes: