Merge lp:~fwereade/pyjuju/cobbler-zk-connect into lp:pyjuju

Proposed by William Reade
Status: Merged
Approved by: Jim Baker
Approved revision: 331
Merged at revision: 331
Proposed branch: lp:~fwereade/pyjuju/cobbler-zk-connect
Merge into: lp:pyjuju
Prerequisite: lp:~fwereade/pyjuju/provider-base
Diff against target: 1884 lines (+551/-353)
50 files modified
ensemble/agents/provision.py (+4/-4)
ensemble/agents/tests/test_provision.py (+3/-3)
ensemble/agents/tests/test_unit.py (+4/-4)
ensemble/control/status.py (+3/-3)
ensemble/control/tests/test_status.py (+0/-2)
ensemble/errors.py (+7/-19)
ensemble/formula/bundle.py (+3/-3)
ensemble/formula/provider.py (+1/-1)
ensemble/formula/tests/test_bundle.py (+8/-4)
ensemble/formula/tests/test_config.py (+6/-4)
ensemble/formula/tests/test_repository.py (+4/-4)
ensemble/hooks/tests/test_arguments.py (+2/-2)
ensemble/hooks/tests/test_cli.py (+5/-2)
ensemble/hooks/tests/test_executor.py (+1/-1)
ensemble/hooks/tests/test_invoker.py (+1/-1)
ensemble/lib/tests/test_schema.py (+1/-1)
ensemble/machine/__init__.py (+1/-3)
ensemble/machine/tests/test_unit_deployment.py (+6/-4)
ensemble/machine/unit.py (+1/-1)
ensemble/providers/common/base.py (+25/-21)
ensemble/providers/common/connect.py (+35/-26)
ensemble/providers/common/findzookeepers.py (+1/-1)
ensemble/providers/common/tests/test_connect.py (+133/-0)
ensemble/providers/common/tests/test_findzookeepers.py (+27/-2)
ensemble/providers/common/tests/test_utils.py (+2/-5)
ensemble/providers/common/utils.py (+3/-1)
ensemble/providers/ec2/__init__.py (+7/-25)
ensemble/providers/ec2/launch.py (+2/-3)
ensemble/providers/ec2/machine.py (+1/-6)
ensemble/providers/ec2/securitygroup.py (+6/-7)
ensemble/providers/ec2/tests/common.py (+0/-10)
ensemble/providers/ec2/tests/test_connect.py (+48/-90)
ensemble/providers/ec2/tests/test_getmachines.py (+2/-3)
ensemble/providers/ec2/tests/test_launch.py (+3/-4)
ensemble/providers/ec2/tests/test_machine.py (+7/-26)
ensemble/providers/ec2/tests/test_provider.py (+4/-1)
ensemble/providers/ec2/tests/test_securitygroup.py (+6/-6)
ensemble/providers/orchestra/__init__.py (+4/-4)
ensemble/providers/orchestra/cobbler.py (+5/-5)
ensemble/providers/orchestra/machine.py (+4/-0)
ensemble/providers/orchestra/tests/test_cobbler.py (+14/-9)
ensemble/providers/orchestra/tests/test_connect.py (+111/-0)
ensemble/providers/orchestra/tests/test_launch.py (+14/-6)
ensemble/providers/orchestra/tests/test_provider.py (+4/-2)
ensemble/state/service.py (+2/-1)
ensemble/state/sshclient.py (+2/-0)
ensemble/state/tests/test_security.py (+1/-1)
ensemble/state/tests/test_service.py (+9/-5)
ensemble/tests/test_errors.py (+6/-16)
ensemble/unit/tests/test_formula.py (+2/-1)
To merge this branch: bzr merge lp:~fwereade/pyjuju/cobbler-zk-connect
Reviewer Review Type Date Requested Status
Jim Baker (community) Approve
Gustavo Niemeyer Approve
Review via email: mp+71734@code.launchpad.net

Description of the change

This branch moves providers.ec2.connect over to providers.common.connect, makes a couple of small changes, and adds a bunch of tests. It should finally close the referenced bug (much of it was actually fixed in lp:~fwereade/ensemble/hide-instances, but it wasn't hooked up to orchestra until this branch).

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Looks good. +1 with these sorted:

[1]

+ raise EnvironmentPending("no machines have addresses assigned yet")

Was already like that, but can you please fix the message with s/no/No/?

[2]

+ def _cannot_connect(self, failure, machine):
+ failure.trap(NoConnection, ConnectionTimeoutException)
+ if not machine.launch_time:
+ raise EnvironmentPending(
+ "machine %s may not be ready (%s)"
+ % (machine.instance_id, str(failure.value)))
+
         five_minutes_ago = datetime.datetime.now() - timedelta(minutes=5)
         if machine.launch_time > five_minutes_ago:
- raise NoConnection("Can't yet connect to started machine: %s" %
- str(failure.value))
+ raise EnvironmentPending(
+ "machine %s is probably not ready (%s)"
+ % (machine.instance_id, str(failure.value)))
         return failure

Feels like a lot of subtlety and complexity for something simple.

Please kill both launch_time checks, and use this error message:

    "Can't connect to machine %s (perhaps still initializing): %s"

As a minor hint for future changes, please note the message
style (capital case, colon before underlying error message).

review: Approve
328. By William Reade

unused import

329. By William Reade

merge parent

330. By William Reade

ProviderMachine no longer has a .launch_time

331. By William Reade

merge trunk

Revision history for this message
Jim Baker (jimbaker) wrote :

+1, looks great once these minor problems are fixed in addition to what Gustavo mentioned.

[1]

+ def connect(self, share=False):
+ """Connect to the zookeeper ensemble running in the machine provider.
+
+ @param share: Requests sharing of the connection with other clients
+ attempting to connect to the same provider, if that's feasible.
+
+ returns an open C{txzookeeper.client.ZookeeperClient} and a
+ C{ensemble.storage.connection.TunnelProtocol}
+ """
+ return ZookeeperConnect(self).run(share=share)
+

Please write new docstrings using RST format.

[2]

     def run(self, share=False):
- client_deferred = self._provider.get_zookeeper_machines()
- client_deferred.addCallback(self._connect_to_machine, share)
- client_deferred.addCallback(self._wait_for_initialization)
- return client_deferred
+ d = self._provider.get_zookeeper_machines()
+ d.addCallback(self._pick_machine)
+ d.addCallback(self._connect_to_machine, share)
+ d.addCallback(self._wait_for_initialization)
+ return d

It's not difficult to follow the chaining here, but I think it's much easier when written using inlineCallbacks. Also, no docstring here.

[3]

+ def get_zookeeper_machines(self):
+ return succeed([
+ ProviderMachine("i-havenodns"),
+ self._second_zookeeper])

Docstring

[4]

+ # Give it some time to do it incorrectly.
+ yield self.sleep(0.1)

Is this sleep necessary, vs roundtripping with a poke?

[5]

 def machine_from_instance(instance):
- launch_time = None
- if instance.launch_time:
- launch_time = datetime.datetime.strptime(
- instance.launch_time[:19], "%Y-%m-%dT%H:%M:%S")
     return EC2ProviderMachine(
         instance.instance_id,
         instance.dns_name,
- instance.private_dns_name,
- launch_time)
+ instance.private_dns_name)

Docstring

[6]

- # XXX Replace the logic above with self.sleep(0.1) when it's merged.
+ yield self.sleep(0.1)

Again, another necessary sleep?

[7]

There are a number of new tests. None of them have docstrings.

review: Approve
Revision history for this message
William Reade (fwereade) wrote :

> [4]
>
> + # Give it some time to do it incorrectly.
> + yield self.sleep(0.1)
>
> Is this sleep necessary, vs roundtripping with a poke?

> [6]
>
> - # XXX Replace the logic above with self.sleep(0.1) when it's merged.
> + yield self.sleep(0.1)
>
> Again, another necessary sleep?

I don't understand your proposed solution: if we yield d, we'll block forever, but if we don't yield d and don't sleep we won't be able to confirm that d actually does wait for /initialized's existence. Am I missing something?

332. By William Reade

fixes per jimbaker's review; mostly dosctrings

333. By William Reade

fix thinko

Revision history for this message
William Reade (fwereade) wrote :

Just waiting on clarification for jimbaker's [4], [6], and on https://code.launchpad.net/~fwereade/ensemble/cobbler-zk-connect-error-messages/+merge/72033

Revision history for this message
William Reade (fwereade) wrote :

> Just waiting on clarification for jimbaker's [4], [6]

Explained on IRC: .poke_zk() is deterministic, and faster, and does exactly what we need.

334. By William Reade

remaining fixes per review

335. By William Reade

merge trunk

336. By William Reade

change dosctrings per review

337. By William Reade

merge trunk

338. By William Reade

merge trunk

339. By William Reade

merge lp:~fwereade/ensemble/cobbler-zk-connect-error-messages (error messages are more rigorously tested, and in some cases better) [f=831883][r=niemeyer,jimbaker]

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ensemble/agents/provision.py'
2--- ensemble/agents/provision.py 2011-08-12 19:28:37 +0000
3+++ ensemble/agents/provision.py 2011-08-26 13:16:39 +0000
4@@ -4,7 +4,7 @@
5 from zookeeper import NoNodeException
6
7 from ensemble.environment.config import EnvironmentsConfig
8-from ensemble.errors import MachinesNotFound, ProviderInteractionError
9+from ensemble.errors import MachinesNotFound, ProviderError
10 from ensemble.lib.twistutils import concurrent_execution_guard
11 from ensemble.state.errors import (
12 MachineStateNotFound, ServiceStateNotFound, ServiceUnitStateNotFound,
13@@ -178,7 +178,7 @@
14 # map of instance_id -> machine
15 try:
16 provider_machines = yield self.provider.get_machines()
17- except ProviderInteractionError:
18+ except ProviderError:
19 log.exception("Cannot get machine list")
20 return
21
22@@ -192,7 +192,7 @@
23 machine_state_id, provider_machines)
24 except (StateChanged,
25 MachineStateNotFound,
26- ProviderInteractionError):
27+ ProviderError):
28 log.exception("Cannot process machine %s", machine_state_id)
29 continue
30 instance_ids.append(instance_id)
31@@ -204,7 +204,7 @@
32 machine = provider_machines[instance_id]
33 try:
34 yield self.provider.shutdown_machine(machine)
35- except ProviderInteractionError:
36+ except ProviderError:
37 log.exception("Cannot shutdown machine %s", instance_id)
38 continue
39
40
41=== modified file 'ensemble/agents/tests/test_provision.py'
42--- ensemble/agents/tests/test_provision.py 2011-08-11 05:59:30 +0000
43+++ ensemble/agents/tests/test_provision.py 2011-08-26 13:16:39 +0000
44@@ -313,7 +313,7 @@
45
46 mock_provider = self.mocker.patch(self.agent.provider)
47 mock_provider.start_machine({"machine-id": 0})
48- self.mocker.result(fail(ProviderInteractionError(OSError("Bad"))))
49+ self.mocker.result(fail(ProviderInteractionError()))
50
51 mock_provider.start_machine({"machine-id": 1})
52 self.mocker.passthrough()
53@@ -339,7 +339,7 @@
54 mock_provider = self.mocker.patch(self.agent.provider)
55
56 mock_provider.shutdown_machine(MATCH_MACHINE)
57- self.mocker.result(fail(ProviderInteractionError("Bad")))
58+ self.mocker.result(fail(ProviderInteractionError()))
59
60 mock_provider.shutdown_machine(MATCH_MACHINE)
61 self.mocker.passthrough()
62@@ -368,7 +368,7 @@
63
64 mock_provider = self.mocker.patch(self.agent.provider)
65 mock_provider.get_machines()
66- self.mocker.result(fail(ProviderInteractionError("Bad")))
67+ self.mocker.result(fail(ProviderInteractionError()))
68
69 mock_provider.get_machines()
70 self.mocker.passthrough()
71
72=== modified file 'ensemble/agents/tests/test_unit.py'
73--- ensemble/agents/tests/test_unit.py 2011-07-29 14:42:04 +0000
74+++ ensemble/agents/tests/test_unit.py 2011-08-26 13:16:39 +0000
75@@ -118,10 +118,10 @@
76 e = self.assertRaises(EnsembleError,
77 self.agent.configure,
78 options)
79- self.assertIn(
80- ("--unit-name must be provided in the command line,"
81- " or $ENSEMBLE_UNIT_NAME in the environment"),
82- str(e))
83+ self.assertEquals(
84+ str(e),
85+ "--unit-name must be provided in the command line, or "
86+ "$ENSEMBLE_UNIT_NAME in the environment")
87
88 def test_agent_unit_name_cli_extraction(self):
89 """The unit agent can parse its unit-name from the cli.
90
91=== modified file 'ensemble/control/status.py'
92--- ensemble/control/status.py 2011-07-20 23:35:48 +0000
93+++ ensemble/control/status.py 2011-08-26 13:16:39 +0000
94@@ -7,7 +7,7 @@
95 from twisted.internet.defer import inlineCallbacks, returnValue
96 import yaml
97
98-from ensemble.errors import ProviderInteractionError
99+from ensemble.errors import ProviderError
100 from ensemble.environment.errors import EnvironmentsConfigError
101 from ensemble.state.errors import UnitRelationStateNotFound
102 from ensemble.state.formula import FormulaStateManager
103@@ -264,10 +264,10 @@
104 try:
105 pm = yield machine_provider.get_machine(instance_id)
106 m["dns-name"] = pm.dns_name
107- except ProviderInteractionError:
108+ except ProviderError:
109 # The provider doesn't have machine information
110 log.error("Machine provider information missing: machine %s" % (
111- machine_state.id))
112+ machine_state.id))
113
114 machine_data[machine_state.id] = m
115
116
117=== modified file 'ensemble/control/tests/test_status.py'
118--- ensemble/control/tests/test_status.py 2011-08-08 21:51:54 +0000
119+++ ensemble/control/tests/test_status.py 2011-08-26 13:16:39 +0000
120@@ -458,7 +458,6 @@
121 self.assertEqual(state["machines"][7]["instance-id"],
122 "pending")
123
124-
125 @inlineCallbacks
126 def test_render_yaml(self):
127 yield self.build_topology()
128@@ -610,7 +609,6 @@
129
130 self.assertIn("namespace:dummy-1", result)
131
132-
133 def test_render_dot_bad_clustering(self):
134 """Test around Bug #792448.
135
136
137=== modified file 'ensemble/errors.py'
138--- ensemble/errors.py 2011-08-17 17:26:19 +0000
139+++ ensemble/errors.py 2011-08-26 13:16:39 +0000
140@@ -48,7 +48,7 @@
141 self.path = path
142
143 def __str__(self):
144- return "File was not found: %s" % (self.path,)
145+ return "File was not found: %r" % (self.path,)
146
147
148 class FormulaError(EnsembleError):
149@@ -59,7 +59,7 @@
150 self.message = message
151
152 def __str__(self):
153- return "Error processing '%s': %s." % (self.path, self.message)
154+ return "Error processing %r: %s" % (self.path, self.message)
155
156
157 class FormulaInvocationError(EnsembleError):
158@@ -70,8 +70,8 @@
159 self.exit_code = exit_code
160
161 def __str__(self):
162- return "Error processing '%s': exit code %s." % (self.path,
163- self.exit_code)
164+ return "Error processing %r: exit code %s." % (
165+ self.path, self.exit_code)
166
167
168 class FileAlreadyExists(EnsembleError):
169@@ -84,7 +84,7 @@
170 self.path = path
171
172 def __str__(self):
173- return "File already exists, won't overwrite: %s" % (self.path,)
174+ return "File already exists, won't overwrite: %r" % (self.path,)
175
176
177 class InvalidEnsembleHeaderValue(EnsembleError):
178@@ -101,7 +101,7 @@
179
180 def __str__(self):
181 return ("Expected a YAML file with an 'ensemble: %s' "
182- "header, found 'ensemble: %s': %s"
183+ "header, found 'ensemble: %s': %r"
184 % (self.expected, self.found, self.path))
185
186
187@@ -130,12 +130,6 @@
188 class EnvironmentPending(NoConnection):
189 """Raised when the ensemble environment is not accessible."""
190
191- def __init__(self, info="no details available"):
192- self._info = info
193-
194- def __str__(self):
195- return "Ensemble environment is not accessible: %s" % self._info
196-
197
198 class ProviderError(EnsembleError):
199 """Raised when an exception occurs in a provider."""
200@@ -154,13 +148,7 @@
201
202
203 class ProviderInteractionError(ProviderError):
204-
205- def __init__(self, error):
206- self.error = error
207-
208- def __str__(self):
209- return "ProviderError: Interaction with machine provider failed: %r" \
210- % self.error
211+ """Raised when an unexpected error occurs interacting with a provider"""
212
213
214 class CannotTerminateMachine(EnsembleError):
215
216=== modified file 'ensemble/formula/bundle.py'
217--- ensemble/formula/bundle.py 2011-06-05 20:19:18 +0000
218+++ ensemble/formula/bundle.py 2011-08-26 13:16:39 +0000
219@@ -18,13 +18,13 @@
220 try:
221 zf = ZipFile(path, 'r')
222 except BadZipfile, exc:
223- raise FormulaError(path, "Must be a ZIP-file (%s)." % str(exc))
224+ raise FormulaError(path, "must be a zip file (%s)" % exc)
225
226 metadata = MetaData()
227 if "metadata.yaml" not in zf.namelist():
228 raise FormulaError(path,
229- "Archive does not contain required file: "
230- "\"metadata.yaml\".")
231+ "archive does not contain required file "
232+ "\"metadata.yaml\"")
233
234 content = zf.read("metadata.yaml")
235 metadata.parse(content)
236
237=== modified file 'ensemble/formula/provider.py'
238--- ensemble/formula/provider.py 2010-11-02 16:15:23 +0000
239+++ ensemble/formula/provider.py 2011-08-26 13:16:39 +0000
240@@ -25,5 +25,5 @@
241 from .directory import FormulaDirectory
242 return FormulaDirectory(specification)
243
244- raise FormulaError(specification, "Unable to process %s into a formula" % (
245+ raise FormulaError(specification, "unable to process %s into a formula" % (
246 specification))
247
248=== modified file 'ensemble/formula/tests/test_bundle.py'
249--- ensemble/formula/tests/test_bundle.py 2011-05-06 04:45:17 +0000
250+++ ensemble/formula/tests/test_bundle.py 2011-08-26 13:16:39 +0000
251@@ -38,8 +38,10 @@
252 try:
253 FormulaBundle(filename)
254 except FormulaError, exc:
255- self.assertIn(filename, str(exc))
256- self.assertIn("zip", str(exc).lower())
257+ self.assertEquals(
258+ str(exc),
259+ ("Error processing %r: " % filename) +
260+ "must be a zip file (File is not a zip file)")
261 return
262
263 self.fail("Expected formula error.")
264@@ -53,8 +55,10 @@
265 try:
266 FormulaBundle(filename)
267 except FormulaError, exc:
268- self.assertIn(filename, str(exc))
269- self.assertIn("metadata.yaml", str(exc).lower())
270+ self.assertEquals(
271+ str(exc),
272+ ("Error processing %r: " % filename) +
273+ "archive does not contain required file \"metadata.yaml\"")
274 return
275
276 self.fail("Expected formula error.")
277
278=== modified file 'ensemble/formula/tests/test_config.py'
279--- ensemble/formula/tests/test_config.py 2011-08-24 00:18:27 +0000
280+++ ensemble/formula/tests/test_config.py 2011-08-26 13:16:39 +0000
281@@ -102,7 +102,7 @@
282
283 error = self.assertRaises(ServiceConfigError,
284 self.config.validate, dict(username="1234"))
285- self.assertIn(str(error), "Invalid value for username: 1234")
286+ self.assertEquals(str(error), "Invalid value for username: 1234")
287
288 data = self.config.validate(dict(username="12345678"))
289 self.assertEqual(data, {"username": "12345678", "title": "My Title"})
290@@ -114,15 +114,17 @@
291 error = self.assertRaises(ServiceConfigError,
292 self.config.parse,
293 local_configuration)
294- self.assertIn("options.fails.validator: required value not found",
295- str(error))
296+ self.assertEquals(
297+ str(error),
298+ "Invalid options specification: options.fails.validator: "
299+ "required value not found")
300
301 def test_validate_types(self):
302 self.config.parse(sample_configuration)
303
304 error = self.assertRaises(ServiceConfigError,
305 self.config.validate, {"skill-level": "NaN"})
306- self.assertIn(str(error), "Invalid value for skill-level: NaN")
307+ self.assertEquals(str(error), "Invalid value for skill-level: NaN")
308
309 data = self.config.validate({"skill-level": "9001"})
310 # its over 9000!
311
312=== modified file 'ensemble/formula/tests/test_repository.py'
313--- ensemble/formula/tests/test_repository.py 2011-07-15 12:41:01 +0000
314+++ ensemble/formula/tests/test_repository.py 2011-08-26 13:16:39 +0000
315@@ -85,11 +85,11 @@
316 self.assertTrue(formula.metadata)
317
318 def test_find_formula_by_name_fails(self):
319- exc = self.assertRaises(
320+ error = self.assertRaises(
321 FormulaNotFound, self.repository1.find, "missing")
322- self.assertIn("missing", str(exc))
323- self.assertIn(self.unbundled_repo_path, str(exc),
324- "Repository path not found in the error message.")
325+ self.assertEquals(str(error),
326+ "Formula 'missing' not found in repository %s"
327+ % self.unbundled_repo_path)
328
329 def test_find_formula_with_multiple_versions_returns_latest(self):
330 # Copy the repository out of the codebase, so that we can hack it.
331
332=== modified file 'ensemble/hooks/tests/test_arguments.py'
333--- ensemble/hooks/tests/test_arguments.py 2011-06-11 02:37:35 +0000
334+++ ensemble/hooks/tests/test_arguments.py 2011-08-26 13:16:39 +0000
335@@ -52,8 +52,8 @@
336 err = self.capture_stream("stderr")
337 os.environ.pop("ENSEMBLE_AGENT_SOCKET", None)
338 error = self.failUnlessRaises(SystemExit, cli.parse_args)
339- self.assertEquals("No ENSEMBLE_AGENT_SOCKET/-s option found",
340- str(error))
341+ self.assertEquals(str(error),
342+ "No ENSEMBLE_AGENT_SOCKET/-s option found")
343 self.assertIn("No ENSEMBLE_AGENT_SOCKET/-s option found",
344 err.getvalue())
345
346
347=== modified file 'ensemble/hooks/tests/test_cli.py'
348--- ensemble/hooks/tests/test_cli.py 2011-06-29 09:02:33 +0000
349+++ ensemble/hooks/tests/test_cli.py 2011-08-26 13:16:39 +0000
350@@ -335,12 +335,15 @@
351 # and check an error condition
352 options = ["content=@missing"]
353 error = self.assertRaises(EnsembleError, parse_keyvalue_pairs, options)
354- self.assertIn("No such file", str(error))
355+ self.assertEquals(
356+ str(error),
357+ "No such file or directory: missing (argument:content)")
358
359 # and check when fed non-kvpairs the error makes sense
360 options = ["foobar"]
361 error = self.assertRaises(EnsembleError, parse_keyvalue_pairs, options)
362- self.assertIn("Expected `option=value`. Found `foobar`", str(error))
363+ self.assertEquals(
364+ str(error), "Expected `option=value`. Found `foobar`")
365
366 def test_parse_port_protocol(self):
367 self.assertEqual(parse_port_protocol("80"), (80, "tcp"))
368
369=== modified file 'ensemble/hooks/tests/test_executor.py'
370--- ensemble/hooks/tests/test_executor.py 2011-07-06 18:00:07 +0000
371+++ ensemble/hooks/tests/test_executor.py 2011-08-26 13:16:39 +0000
372@@ -326,7 +326,7 @@
373 error = yield self.assertFailure(
374 self._executor.run_priority_hook(invoke, "foobar"),
375 AssertionError)
376- self.assertIn("Executor must not be running", str(error))
377+ self.assertEquals(str(error), "Executor must not be running")
378
379 @inlineCallbacks
380 def test_prioritize_with_queued(self):
381
382=== modified file 'ensemble/hooks/tests/test_invoker.py'
383--- ensemble/hooks/tests/test_invoker.py 2011-08-16 17:26:09 +0000
384+++ ensemble/hooks/tests/test_invoker.py 2011-08-26 13:16:39 +0000
385@@ -480,7 +480,7 @@
386 error = self.failUnlessRaises(errors.FormulaError, exe, hook)
387
388 self.assertEqual(error.path, hook)
389- self.assertIn(error.message, "hook is not executable")
390+ self.assertEqual(error.message, "hook is not executable")
391
392 @defer.inlineCallbacks
393 def test_spawn_success(self):
394
395=== modified file 'ensemble/lib/tests/test_schema.py'
396--- ensemble/lib/tests/test_schema.py 2011-07-27 08:54:46 +0000
397+++ ensemble/lib/tests/test_schema.py 2011-08-26 13:16:39 +0000
398@@ -183,7 +183,7 @@
399 exp = "([a-"
400 error = self.assertRaises(
401 SchemaError, Regex().coerce, exp, PATH)
402- self.assertIn("expected regex", str(error))
403+ self.assertEquals(str(error), "<path>: expected regex, got '([a-'")
404
405
406 class ListTest(TestCase):
407
408=== modified file 'ensemble/machine/__init__.py'
409--- ensemble/machine/__init__.py 2011-08-08 17:37:46 +0000
410+++ ensemble/machine/__init__.py 2011-08-26 13:16:39 +0000
411@@ -10,10 +10,8 @@
412 :class:`MachineProvider` api.
413 """
414
415- def __init__(self, instance_id, dns_name=None,
416- private_dns_name=None, launch_time=None):
417+ def __init__(self, instance_id, dns_name=None, private_dns_name=None):
418 self.instance_id = instance_id
419 # ideally this would be ip_address, but txaws doesn't expose it.
420 self.dns_name = dns_name
421 self.private_dns_name = private_dns_name
422- self.launch_time = launch_time
423
424=== modified file 'ensemble/machine/tests/test_unit_deployment.py'
425--- ensemble/machine/tests/test_unit_deployment.py 2011-02-15 15:15:16 +0000
426+++ ensemble/machine/tests/test_unit_deployment.py 2011-08-26 13:16:39 +0000
427@@ -115,8 +115,8 @@
428
429 self.failUnlessFailure(d, UnitDeploymentError)
430
431- def validate_result(result):
432- self.assertIn("No module named magichat", str(result))
433+ def validate_result(error):
434+ self.assertIn("No module named magichat", str(error))
435
436 d.addCallback(validate_result)
437 return d
438@@ -170,7 +170,7 @@
439 error = self.assertRaises(
440 UnitDeploymentError,
441 self.deployment.start, "0", get_test_zookeeper_address())
442- self.assertIn("Formula must be unpacked first.", str(error))
443+ self.assertEquals(str(error), "Formula must be unpacked first.")
444
445 @inlineCallbacks
446 def test_service_unit_destroy(self):
447@@ -269,7 +269,9 @@
448 error = self.assertRaises(
449 UnitDeploymentError,
450 self.deployment.unpack_formula, self.formula)
451- self.assertIn("Invalid formula for deployment:", str(error))
452+ self.assertEquals(
453+ str(error),
454+ "Invalid formula for deployment: %s" % self.formula.path)
455
456 def test_is_running_no_pid_file(self):
457 """
458
459=== modified file 'ensemble/machine/unit.py'
460--- ensemble/machine/unit.py 2011-02-15 15:15:16 +0000
461+++ ensemble/machine/unit.py 2011-08-26 13:16:39 +0000
462@@ -123,6 +123,6 @@
463 """Unpack a formula to the service units directory."""
464 if not isinstance(formula, FormulaBundle):
465 raise UnitDeploymentError(
466- "Invalid formula for deployment: %r" % formula)
467+ "Invalid formula for deployment: %s" % formula.path)
468
469 formula.extract_to(os.path.join(self.directory, "formula"))
470
471=== modified file 'ensemble/providers/common/base.py'
472--- ensemble/providers/common/base.py 2011-08-23 18:47:42 +0000
473+++ ensemble/providers/common/base.py 2011-08-26 13:16:39 +0000
474@@ -4,7 +4,9 @@
475 from twisted.internet.defer import inlineCallbacks, returnValue
476
477 from ensemble.environment.errors import EnvironmentsConfigError
478+
479 from .bootstrap import Bootstrap
480+from .connect import ZookeeperConnect
481 from .findzookeepers import find_zookeepers
482 from .state import SaveState, LoadState
483 from .utils import get_user_authorized_keys
484@@ -16,7 +18,6 @@
485 To write a working subclass, you will need to override the following
486 methods:
487
488- * connect
489 * get_file_storage
490 * start_machine
491 * get_machines
492@@ -57,19 +58,8 @@
493 # Subclasses need to implement their own versions of everything
494 # in the following block
495
496- def connect(self, share=False):
497- """Connect to the zookeeper ensemble running in the machine provider.
498-
499- @param share: Requests sharing of the connection with other clients
500- attempting to connect to the same provider, if that's feasible.
501-
502- returns an open C{txzookeeper.client.ZookeeperClient} and a
503- C{ensemble.storage.connection.TunnelProtocol}
504- """
505- raise NotImplementedError()
506-
507 def get_file_storage(self):
508- """Retrieve the provider C{FileStorage} abstraction."""
509+ """Retrieve the provider FileStorage abstraction."""
510 raise NotImplementedError()
511
512 def start_machine(self, machine_data, master=False):
513@@ -123,10 +113,23 @@
514
515 @return: the first valid instance found as a single element list.
516
517- @raise: EnvironmentNotFound
518+ @raise: `EnvironmentNotFound`
519 """
520 return find_zookeepers(self)
521
522+ def connect(self, share=False):
523+ """Attempt to connect to a running zookeeper node.
524+
525+ @param share: where feasible, attempt to share a connection with other
526+ clients
527+
528+ @return: an open `txzookeeper.client.ZookeeperClient`
529+
530+ @raise: `EnvironmentNotFound` when no zookeepers exist
531+ @raise: `EnvironmentPending` when zookeepers exist but connect fails
532+ """
533+ return ZookeeperConnect(self).run(share=share)
534+
535 def bootstrap(self):
536 """Bootstrap an ensemble server in the provider."""
537 return Bootstrap(self).run()
538@@ -134,9 +137,11 @@
539 def get_machine(self, instance_id):
540 """Retrieve a provider machine by instance id.
541
542- @param instance_id: instance_id of the Machine you want to get.
543-
544- @raise: MachinesNotFound
545+ @param instance_id: instance_id of the `ProviderMachine` you want.
546+
547+ @return: a single `ProviderMachine`
548+
549+ @raise: `MachinesNotFound`
550 """
551 d = self.get_machines([instance_id])
552 d.addCallback(itemgetter(0))
553@@ -145,7 +150,7 @@
554 def shutdown_machine(self, machine):
555 """Terminate one machine associated with this provider.
556
557- @param machine: machine to shut down.
558+ @param machine: ProviderMachine to shut down.
559
560 @raise: MachinesNotFound
561 """
562@@ -166,14 +171,13 @@
563 def save_state(self, state):
564 """Save state to the provider.
565
566- @param state
567- @type dict
568+ @param state: a dictionary to persist
569 """
570 return SaveState(self).run(state)
571
572 def load_state(self):
573 """Load state from the provider.
574
575- @return: a dictionary.
576+ @return: a previously-persisted dictionary, or False
577 """
578 return LoadState(self).run()
579
580=== renamed file 'ensemble/providers/ec2/connect.py' => 'ensemble/providers/common/connect.py'
581--- ensemble/providers/ec2/connect.py 2011-08-17 08:58:51 +0000
582+++ ensemble/providers/common/connect.py 2011-08-26 13:16:39 +0000
583@@ -1,45 +1,55 @@
584-import datetime
585-from datetime import timedelta
586-
587 from twisted.internet.defer import inlineCallbacks, returnValue
588
589+from txzookeeper.client import ConnectionTimeoutException
590+
591 from ensemble.errors import EnvironmentPending, NoConnection
592 from ensemble.state.sshclient import SSHClient
593
594 from .utils import log
595
596
597-class EC2Connect(object):
598+class ZookeeperConnect(object):
599
600 def __init__(self, provider):
601 self._provider = provider
602
603+ @inlineCallbacks
604 def run(self, share=False):
605- client_deferred = self._provider.get_zookeeper_machines()
606- client_deferred.addCallback(self._connect_to_machine, share)
607- client_deferred.addCallback(self._wait_for_initialization)
608- return client_deferred
609-
610- def _connect_to_machine(self, machines, share):
611+ """Attempt to connect to a running zookeeper node.
612+
613+ @param share: where feasible, attempt to share a connection with other
614+ clients
615+
616+ @return: an open txzookeeper.client.ZookeeperClient
617+
618+ @raise: EnvironmentNotFound when no zookeepers exist
619+ @raise: EnvironmentPending when zookeepers exist but connect fails
620+ """
621+ candidates = yield self._provider.get_zookeeper_machines()
622+ chosen = yield self._pick_machine(candidates)
623+ client = yield self._connect_to_machine(chosen, share)
624+ yield self._wait_for_initialization(client)
625+ returnValue(client)
626+
627+ def _pick_machine(self, machines):
628 # TODO Should we pick a random entry from the nodes list?
629+ for machine in machines:
630+ if machine.dns_name:
631+ return machine
632+ raise EnvironmentPending("No machines have addresses assigned yet")
633+
634+ def _connect_to_machine(self, machine, share):
635 log.info("Connecting to environment.")
636- machine = machines[0]
637- if not machine.dns_name:
638- raise EnvironmentPending("Machine %s has no address assigned yet"
639- % machine.instance_id)
640- client = SSHClient()
641- result = client.connect(machine.dns_name + ":2181",
642- timeout=30, share=share)
643- result.addErrback(self._check_machine_age, machine)
644+ result = SSHClient().connect(
645+ machine.dns_name + ":2181", timeout=30, share=share)
646+ result.addErrback(self._cannot_connect, machine)
647 return result
648
649- def _check_machine_age(self, failure, machine):
650- failure.trap(NoConnection)
651- five_minutes_ago = datetime.datetime.now() - timedelta(minutes=5)
652- if machine.launch_time > five_minutes_ago:
653- raise NoConnection("Can't yet connect to started machine: %s" %
654- str(failure.value))
655- return failure
656+ def _cannot_connect(self, failure, machine):
657+ failure.trap(NoConnection, ConnectionTimeoutException)
658+ raise EnvironmentPending(
659+ "Cannot connect to machine %s (perhaps still initializing): %s"
660+ % (machine.instance_id, str(failure.value)))
661
662 @inlineCallbacks
663 def _wait_for_initialization(self, client):
664@@ -50,4 +60,3 @@
665 yield watch_d
666 else:
667 log.debug("Environment already initialized.")
668- returnValue(client)
669
670=== modified file 'ensemble/providers/common/findzookeepers.py'
671--- ensemble/providers/common/findzookeepers.py 2011-08-16 13:24:41 +0000
672+++ ensemble/providers/common/findzookeepers.py 2011-08-26 13:16:39 +0000
673@@ -26,5 +26,5 @@
674
675 if machines:
676 returnValue(machines)
677- raise EnvironmentNotFound("machines are not running (%s)."
678+ raise EnvironmentNotFound("machines are not running (%s)"
679 % ", ".join(missing_instance_ids))
680
681=== added file 'ensemble/providers/common/tests/test_connect.py'
682--- ensemble/providers/common/tests/test_connect.py 1970-01-01 00:00:00 +0000
683+++ ensemble/providers/common/tests/test_connect.py 2011-08-26 13:16:39 +0000
684@@ -0,0 +1,133 @@
685+from twisted.internet.defer import fail, inlineCallbacks, succeed
686+
687+import zookeeper
688+
689+from txzookeeper import ZookeeperClient
690+from txzookeeper.client import ConnectionTimeoutException
691+from txzookeeper.tests.utils import deleteTree
692+
693+from ensemble.errors import EnvironmentPending, NoConnection
694+from ensemble.lib.testing import TestCase
695+from ensemble.machine import ProviderMachine
696+from ensemble.providers.common.base import MachineProviderBase
697+from ensemble.state.sshclient import SSHClient
698+from ensemble.tests.common import get_test_zookeeper_address
699+
700+
701+class DummyProvider(MachineProviderBase):
702+
703+ def __init__(self, second_zookeeeper):
704+ self._second_zookeeper = second_zookeeeper
705+
706+ def get_zookeeper_machines(self):
707+ """
708+ Return a pair of possible zookeepers, the first of which is invalid
709+ """
710+ return succeed([
711+ ProviderMachine("i-havenodns"),
712+ self._second_zookeeper])
713+
714+
715+class ConnectTest(TestCase):
716+
717+ def mock_connect(self, share, result):
718+ client = self.mocker.patch(SSHClient)
719+ client.connect("foo.example.com:2181", timeout=30, share=share)
720+ self.mocker.result(result)
721+
722+ def assert_connect_error(self, error, expect_type, expect_message):
723+ self.mock_connect(False, fail(error))
724+ self.mocker.replay()
725+
726+ provider = DummyProvider(ProviderMachine("i-exist", "foo.example.com"))
727+ d = provider.connect()
728+
729+ def check_error(error):
730+ self.assertEqual(str(error), expect_message)
731+ self.assertFailure(d, expect_type)
732+ d.addCallback(check_error)
733+ return d
734+
735+ def test_none_have_dns(self):
736+ """
737+ `EnvironmentPending` should be raised if no zookeeper nodes have dns
738+ names
739+ """
740+ provider = DummyProvider(ProviderMachine("i-havenodnseither"))
741+ d = provider.connect()
742+ self.assertFailure(d, EnvironmentPending)
743+
744+ def check(error):
745+ self.assertEquals(
746+ str(error), "No machines have addresses assigned yet")
747+ d.addCallback(check)
748+ return d
749+
750+ def test_share_kwarg(self):
751+ """The `share` kwarg should be passed through to `SSHClient.connect`"""
752+ client = self.mocker.mock(type=SSHClient)
753+ self.mock_connect(True, succeed(client))
754+ client.exists_and_watch("/initialized")
755+ self.mocker.result((succeed(True), None))
756+ self.mocker.replay()
757+
758+ provider = DummyProvider(ProviderMachine("i-amok", "foo.example.com"))
759+ d = provider.connect(share=True)
760+
761+ def verify(result):
762+ self.assertIdentical(result, client)
763+ d.addCallback(verify)
764+ return d
765+
766+ def test_no_connection(self):
767+ """`NoConnection` errors should become `EnvironmentPending`s"""
768+ return self.assert_connect_error(
769+ NoConnection("KABOOM!"), EnvironmentPending,
770+ "Cannot connect to machine i-exist (perhaps still initializing): "
771+ "KABOOM!")
772+
773+ def test_txzookeeper_error(self):
774+ """
775+ `ConnectionTimeoutException` errors should become `EnvironmentPending`s
776+ """
777+ return self.assert_connect_error(
778+ ConnectionTimeoutException("SPLAT!"), EnvironmentPending,
779+ "Cannot connect to machine i-exist (perhaps still initializing): "
780+ "SPLAT!")
781+
782+ def test_other_error(self):
783+ """Other errors should propagate"""
784+ return self.assert_connect_error(
785+ TypeError("THUD!"), TypeError, "THUD!")
786+
787+ @inlineCallbacks
788+ def test_wait_for_initialize(self):
789+ """
790+ A connection to a zookeeper that is running, but whose ensemble state
791+ is not ready, should wait until that state is ready.
792+ """
793+ client = ZookeeperClient()
794+ self.client = client # for poke_zk
795+ self.mock_connect(False, succeed(client))
796+ self.mocker.replay()
797+
798+ zookeeper.set_debug_level(0)
799+ yield client.connect(get_test_zookeeper_address())
800+
801+ provider = DummyProvider(ProviderMachine("i-amok", "foo.example.com"))
802+ d = provider.connect()
803+ client_result = []
804+ d.addCallback(client_result.append)
805+
806+ # Give it a chance to do it incorrectly.
807+ yield self.poke_zk()
808+
809+ try:
810+ self.assertEquals(client_result, [])
811+ yield client.create("/initialized")
812+ yield d
813+ self.assertTrue(client_result, client_result)
814+ self.assertIdentical(client_result[0], client)
815+ finally:
816+ deleteTree("/", client.handle)
817+ client.close()
818
819=== modified file 'ensemble/providers/common/tests/test_findzookeepers.py'
820--- ensemble/providers/common/tests/test_findzookeepers.py 2011-08-16 13:44:01 +0000
821+++ ensemble/providers/common/tests/test_findzookeepers.py 2011-08-26 13:16:39 +0000
822@@ -37,12 +37,27 @@
823 provider = self.get_provider(False)
824 d = provider.get_zookeeper_machines()
825 self.assertFailure(d, EnvironmentNotFound)
826+
827+ def check(error):
828+ self.assertEquals(
829+ str(error),
830+ "Ensemble environment not found: is the environment "
831+ "bootstrapped?")
832+ d.addCallback(check)
833 return d
834
835 def test_empty_state(self):
836 provider = self.get_provider({})
837 d = provider.get_zookeeper_machines()
838 self.assertFailure(d, EnvironmentNotFound)
839+
840+ def check(error):
841+ self.assertEquals(
842+ str(error),
843+ "Ensemble environment not found: is the environment "
844+ "bootstrapped?")
845+ d.addCallback(check)
846+ return d
847 return d
848
849 def test_get_machine_error_aborts(self):
850@@ -56,14 +71,24 @@
851 self.assertFailure(d, SomeError)
852 return d
853
854- def test_bad_machine(self):
855- provider = self.get_provider({"zookeeper-instances": ["porter"]})
856+ def test_bad_machines(self):
857+ provider = self.get_provider({
858+ "zookeeper-instances": ["porter", "carter"]})
859 provider.get_machines(["porter"])
860 self.mocker.result(fail(MachinesNotFound(["porter"])))
861+ provider.get_machines(["carter"])
862+ self.mocker.result(fail(MachinesNotFound(["carter"])))
863 self.mocker.replay()
864
865 d = provider.get_zookeeper_machines()
866 self.assertFailure(d, EnvironmentNotFound)
867+
868+ def check(error):
869+ self.assertEquals(
870+ str(error),
871+ "Ensemble environment not found: machines are not running "
872+ "(porter, carter)")
873+ d.addCallback(check)
874 return d
875
876 def test_good_machine(self):
877
878=== modified file 'ensemble/providers/common/tests/test_utils.py'
879--- ensemble/providers/common/tests/test_utils.py 2011-08-11 22:04:38 +0000
880+++ ensemble/providers/common/tests/test_utils.py 2011-08-26 13:16:39 +0000
881@@ -63,9 +63,7 @@
882 OSError("Bad"))
883 self.assertInstance(error, EnsembleError)
884 self.assertEqual(
885- str(error),
886- "ProviderError: Interaction with machine provider failed: "
887- "OSError('Bad',)")
888+ str(error), "Unexpected OSError interacting with provider: Bad")
889
890 def test_convert_unknown_error_ignores_ensemble_error(self):
891 error = self.assertRaises(
892@@ -87,8 +85,7 @@
893 self.assertInstance(failure.value, ProviderInteractionError)
894 self.assertEqual(
895 str(failure.value),
896- "ProviderError: Interaction with machine provider failed: "
897- "OSError('Bad',)")
898+ "Unexpected OSError interacting with provider: Bad")
899
900
901 class FormatCloudInitTest(TestCase):
902
903=== modified file 'ensemble/providers/common/utils.py'
904--- ensemble/providers/common/utils.py 2011-08-03 15:53:38 +0000
905+++ ensemble/providers/common/utils.py 2011-08-26 13:16:39 +0000
906@@ -31,7 +31,9 @@
907 error = failure
908
909 if not isinstance(error, EnsembleError):
910- error = ProviderInteractionError(error)
911+ message = ("Unexpected %s interacting with provider: %s"
912+ % (type(error).__name__, str(error)))
913+ error = ProviderInteractionError(message)
914
915 if isinstance(failure, Failure):
916 return Failure(error)
917
918=== modified file 'ensemble/providers/ec2/__init__.py'
919--- ensemble/providers/ec2/__init__.py 2011-08-23 18:47:42 +0000
920+++ ensemble/providers/ec2/__init__.py 2011-08-26 13:16:39 +0000
921@@ -9,9 +9,7 @@
922 from ensemble.errors import (
923 MachinesNotFound, ProviderError, ProviderInteractionError)
924 from ensemble.providers.common.base import MachineProviderBase
925-from ensemble.providers.common.utils import convert_unknown_error
926
927-from .connect import EC2Connect
928 from .files import FileStorage
929 from .launch import EC2LaunchMachine
930 from .machine import EC2ProviderMachine, machine_from_instance
931@@ -48,25 +46,8 @@
932 data.setdefault("secret-key", os.environ.get("AWS_SECRET_ACCESS_KEY"))
933 return data
934
935- def _run_operation(self, operation, *args, **kw):
936- d = operation.run(*args, **kw)
937- d.addErrback(convert_unknown_error)
938- return d
939-
940- def connect(self, share=False):
941- """Connect to the zookeeper ensemble running in the machine provider.
942-
943- @param share: Requests sharing of the connection with other clients
944- attempting to connect to the same provider, if that's feasible.
945-
946- returns an open C{txzookeeper.client.ZookeeperClient} and a
947- C{ensemble.storage.connection.TunnelProtocol}
948- """
949- connect = EC2Connect(self)
950- return self._run_operation(connect, share=share)
951-
952 def get_file_storage(self):
953- """Retrieve an S3-backed C{FileStorage}."""
954+ """Retrieve an S3-backed `FileStorage`."""
955 return FileStorage(self.s3, self.config["control-bucket"])
956
957 def start_machine(self, machine_data, master=False):
958@@ -75,7 +56,7 @@
959 @param machine_data: a dictionary of data to pass along to the newly
960 launched machine.
961
962- @param master: if True, machine will initialize the ensemble admin
963+ @param master if True, machine will initialize the ensemble admin
964 and run a provisioning agent.
965 """
966 return EC2LaunchMachine(self, master).run(machine_data)
967@@ -84,8 +65,8 @@
968 def get_machines(self, instance_ids=()):
969 """List machines running in the provider.
970
971- @param instance_ids: ids of instances you want to get. Leave blank
972- to list all machines owned by ensemble.
973+ @param instance_ids: ids of instances you want to get. Leave empty
974+ to list all machines owned by this provider.
975
976 @return: a list of EC2ProviderMachines.
977
978@@ -96,13 +77,14 @@
979 instances = yield self.ec2.describe_instances(*instance_ids)
980 except EC2Error as error:
981 code = error.get_error_codes()
982+ message = error.get_error_messages()
983 if code == "InvalidInstanceID.NotFound":
984 message = error.get_error_messages()
985 raise MachinesNotFound(
986 re.findall(r"\bi-[0-9a-f]{3,15}\b", message))
987 raise ProviderInteractionError(
988- "EC2 error when looking up instances %s: %s"
989- % (", ".join(instance_ids), error.get_error_messages()))
990+ "Unexpected EC2Error getting machines %s: %s"
991+ % (", ".join(instance_ids), message))
992
993 machines = []
994 for instance in instances:
995
996=== modified file 'ensemble/providers/ec2/launch.py'
997--- ensemble/providers/ec2/launch.py 2011-08-10 17:23:06 +0000
998+++ ensemble/providers/ec2/launch.py 2011-08-26 13:16:39 +0000
999@@ -123,9 +123,8 @@
1000 log.debug("Cannot delete security group %s: %s",
1001 ensemble_machine_group, e)
1002 raise ProviderInteractionError(
1003- "EC2 error when attempting to delete "
1004- "security group %s: %s" % (
1005- ensemble_machine_group, e))
1006+ "Unexpected EC2Error deleting security group %s: %s"
1007+ % (ensemble_machine_group, e.get_error_messages()))
1008 log.debug("Creating ensemble machine security group %s",
1009 ensemble_machine_group)
1010 yield self._provider.ec2.create_security_group(
1011
1012=== modified file 'ensemble/providers/ec2/machine.py'
1013--- ensemble/providers/ec2/machine.py 2011-08-11 05:59:30 +0000
1014+++ ensemble/providers/ec2/machine.py 2011-08-26 13:16:39 +0000
1015@@ -11,12 +11,7 @@
1016
1017
1018 def machine_from_instance(instance):
1019- launch_time = None
1020- if instance.launch_time:
1021- launch_time = datetime.datetime.strptime(
1022- instance.launch_time[:19], "%Y-%m-%dT%H:%M:%S")
1023 return EC2ProviderMachine(
1024 instance.instance_id,
1025 instance.dns_name,
1026- instance.private_dns_name,
1027- launch_time)
1028+ instance.private_dns_name)
1029
1030=== modified file 'ensemble/providers/ec2/securitygroup.py'
1031--- ensemble/providers/ec2/securitygroup.py 2011-08-10 17:23:06 +0000
1032+++ ensemble/providers/ec2/securitygroup.py 2011-08-26 13:16:39 +0000
1033@@ -38,8 +38,8 @@
1034 port, protocol, machine.instance_id)
1035 except EC2Error, e:
1036 raise ProviderInteractionError(
1037- "EC2 error when attempting to open %s/%s on machine %s: %s" % (
1038- port, protocol, machine.instance_id, e))
1039+ "Unexpected EC2Error opening %s/%s on machine %s: %s"
1040+ % (port, protocol, machine.instance_id, e.get_error_messages()))
1041
1042
1043 @inlineCallbacks
1044@@ -55,8 +55,8 @@
1045 port, protocol, machine.instance_id)
1046 except EC2Error, e:
1047 raise ProviderInteractionError(
1048- "EC2 error when attempting to close %s/%s on machine %s: %s" % (
1049- port, protocol, machine.instance_id, e))
1050+ "Unexpected EC2Error closing %s/%s on machine %s: %s"
1051+ % (port, protocol, machine.instance_id, e.get_error_messages()))
1052
1053
1054 @inlineCallbacks
1055@@ -72,9 +72,8 @@
1056 _get_machine_group_name(provider, machine_id))
1057 except EC2Error, e:
1058 raise ProviderInteractionError(
1059- "EC2 error when attempting to get opened ports "
1060- "on machine %s: %s" % (
1061- machine.instance_id, e))
1062+ "Unexpected EC2Error getting open ports on machine %s: %s"
1063+ % (machine.instance_id, e.get_error_messages()))
1064
1065 opened_ports = set() # made up of (port, protocol) pairs
1066 for ip_permission in security_groups[0].allowed_ips:
1067
1068=== modified file 'ensemble/providers/ec2/tests/common.py'
1069--- ensemble/providers/ec2/tests/common.py 2011-08-22 23:03:03 +0000
1070+++ ensemble/providers/ec2/tests/common.py 2011-08-26 13:16:39 +0000
1071@@ -59,16 +59,6 @@
1072 "<error><Code>1</Code><Message>%s</Message></error>" % message,
1073 code)
1074
1075- def get_wrapped_ec2_error_text(self, entity_id, reason,
1076- format="The instance ID %r does not exist"):
1077- """By convention, `EC2Error` is wrapped as a `ProviderError`"""
1078- message = format % entity_id
1079- return (
1080- "ProviderError: Interaction with machine provider failed: "
1081- "\"EC2 error when attempting to %s %s: "
1082- "Error Message: %s\"" % (
1083- reason, entity_id, message))
1084-
1085 def setUp(self):
1086 # mock out the aws services
1087 service_factory = self.mocker.replace(
1088
1089=== modified file 'ensemble/providers/ec2/tests/test_connect.py'
1090--- ensemble/providers/ec2/tests/test_connect.py 2011-08-17 08:58:51 +0000
1091+++ ensemble/providers/ec2/tests/test_connect.py 2011-08-26 13:16:39 +0000
1092@@ -1,6 +1,3 @@
1093-import datetime
1094-from datetime import timedelta
1095-
1096 from yaml import dump
1097
1098 from twisted.internet.defer import inlineCallbacks, succeed, fail
1099@@ -8,24 +5,17 @@
1100 import zookeeper
1101
1102 from txzookeeper import ZookeeperClient
1103+from txzookeeper.client import ConnectionTimeoutException
1104 from txzookeeper.tests.utils import deleteTree
1105
1106 from ensemble.lib.testing import TestCase
1107
1108-from ensemble.errors import NoConnection
1109+from ensemble.errors import EnvironmentPending, NoConnection
1110 from ensemble.state.sshclient import SSHClient
1111 from ensemble.providers.ec2.tests.common import EC2TestMixin
1112-from ensemble.errors import ProviderInteractionError
1113 from ensemble.tests.common import get_test_zookeeper_address
1114
1115
1116-def _aws_minutes_ago(minutes_ago):
1117- now = datetime.datetime.now()
1118- delta = timedelta(minutes=minutes_ago, seconds=1)
1119- then = now - delta
1120- return then.strftime("%Y-%m-%dT%H:%M:%S.000Z")
1121-
1122-
1123 class EC2ConnectTest(EC2TestMixin, TestCase):
1124
1125 def mock_find_zookeepers(self, instance):
1126@@ -41,7 +31,25 @@
1127 client.connect("foo.example.com:2181", timeout=30, share=share)
1128 self.mocker.result(result)
1129
1130+ def assert_connect_error(self, error, expect_type, expect_message):
1131+ instance = self.get_instance("i-foobar", dns_name="foo.example.com")
1132+ self.mock_connect(False, instance, fail(error))
1133+ self.mocker.replay()
1134+
1135+ provider = self.get_provider()
1136+ d = provider.connect()
1137+
1138+ def check_error(error):
1139+ self.assertEqual(str(error), expect_message)
1140+ self.assertFailure(d, expect_type)
1141+ d.addCallback(check_error)
1142+ return d
1143+
1144 def test_no_dns_name(self):
1145+ """
1146+ `EnvironmentPending` should be raised if no zookeeper nodes have dns
1147+ names
1148+ """
1149 instance = self.get_instance("i-foobar")
1150 self.mock_find_zookeepers(instance)
1151 self.mocker.replay()
1152@@ -51,17 +59,14 @@
1153
1154 def check_error(error):
1155 self.assertEqual(
1156- str(error),
1157- "Ensemble environment is not accessible: Machine i-foobar has "
1158- "no address assigned yet")
1159+ str(error), "No machines have addresses assigned yet")
1160
1161 self.assertFailure(d, NoConnection)
1162 d.addCallback(check_error)
1163 return d
1164
1165 def test_provider_connect_forwards_share_option(self):
1166- """Test connecting to a running ensemble zookeeper server within ec2.
1167- """
1168+ """The `share` kwarg should be passed through to `SSHClient.connect`"""
1169 instance = self.get_instance("i-foobar", dns_name="foo.example.com")
1170 connected_client = self.mocker.mock(type=SSHClient)
1171 self.mock_connect(True, instance, succeed(connected_client))
1172@@ -78,14 +83,37 @@
1173 d.addCallback(verify_result)
1174 return d
1175
1176+ def test_no_connection(self):
1177+ """`NoConnection` errors should become `EnvironmentPending`s"""
1178+ return self.assert_connect_error(
1179+ NoConnection("KABOOM!"), EnvironmentPending,
1180+ "Cannot connect to machine i-foobar (perhaps still initializing): "
1181+ "KABOOM!")
1182+
1183+ def test_txzookeeper_error(self):
1184+ """
1185+ `ConnectionTimeoutException` errors should become `EnvironmentPending`s
1186+ """
1187+ return self.assert_connect_error(
1188+ ConnectionTimeoutException("SPLAT!"), EnvironmentPending,
1189+ "Cannot connect to machine i-foobar (perhaps still initializing): "
1190+ "SPLAT!")
1191+
1192+ def test_other_error(self):
1193+ """Other errors should propagate"""
1194+ return self.assert_connect_error(
1195+ TypeError("THUD!"), TypeError, "THUD!")
1196+
1197 @inlineCallbacks
1198 def test_provider_connect_waits_on_initialization(self):
1199 """
1200- Test connecting to a running ensemble zookeeper server within ec2.
1201+ A connection to a zookeeper that is running, but whose ensemble state
1202+ is not ready, should wait until that state is ready.
1203 """
1204 # Hand back a real connected client to test the wait on initialization.
1205 instance = self.get_instance("i-foobar", dns_name="foo.example.com")
1206 connected_client = ZookeeperClient()
1207+ self.client = connected_client # for poke_zk
1208 self.mock_connect(False, instance, succeed(connected_client))
1209
1210 self.mocker.replay()
1211@@ -99,13 +127,8 @@
1212 client_deferred = provider.connect()
1213 client_deferred.addCallback(client_result.append)
1214
1215- # Give it some time to do it incorrectly.
1216- from twisted.internet.defer import Deferred
1217- from twisted.internet import reactor
1218- sleep_deferred = Deferred()
1219- reactor.callLater(0.1, sleep_deferred.callback, None)
1220- yield sleep_deferred
1221- # XXX Replace the logic above with self.sleep(0.1) when it's merged.
1222+ # Give it a chance to do it incorrectly.
1223+ yield self.poke_zk()
1224
1225 try:
1226 self.assertEquals(client_result, [])
1227@@ -118,68 +141,3 @@
1228 finally:
1229 deleteTree("/", connected_client.handle)
1230 connected_client.close()
1231-
1232- def test_provider_connection_fails_with_recent_machine(self):
1233- """
1234- If we can't connect to a recently bootstrapped machine,
1235- we show an error indicating the machine might still be
1236- initializing.
1237- """
1238- instance = self.get_instance(
1239- "i-foobar", dns_name="foo.example.com",
1240- launch_time=_aws_minutes_ago(1))
1241- self.mock_connect(False, instance, fail(NoConnection("BADABOOM!")))
1242- self.mocker.replay()
1243-
1244- provider = self.get_provider()
1245- d = provider.connect()
1246-
1247- def check_error(error):
1248- self.assertEqual(
1249- str(error), "Can't yet connect to started machine: BADABOOM!")
1250-
1251- self.assertFailure(d, NoConnection)
1252- d.addCallback(check_error)
1253- return d
1254-
1255- def test_provider_connection_fails_with_old_machine(self):
1256- """
1257- If the machine has started for a while, though, we won't
1258- tweak the error message in any way.
1259- """
1260- instance = self.get_instance(
1261- "i-foobar", dns_name="foo.example.com",
1262- launch_time=_aws_minutes_ago(5))
1263- self.mock_connect(False, instance, fail(NoConnection("BADABOOM!")))
1264- self.mocker.replay()
1265-
1266- provider = self.get_provider()
1267- d = provider.connect()
1268-
1269- def check_error(error):
1270- self.assertEqual(str(error), "BADABOOM!")
1271-
1272- self.assertFailure(d, NoConnection)
1273- d.addCallback(check_error)
1274- return d
1275-
1276- def test_recent_machine_error_tweaking_works_with_no_conn_only(self):
1277- """
1278- If the error raised is not NoConnection, then it should not
1279- be tweaked even if it is a recent machine.
1280- """
1281- instance = self.get_instance(
1282- "i-foobar", dns_name="foo.example.com",
1283- launch_time=_aws_minutes_ago(1))
1284- self.mock_connect(False, instance, fail(ValueError("BADAPOFT!")))
1285- self.mocker.replay()
1286-
1287- provider = self.get_provider()
1288- d = provider.connect()
1289-
1290- def check_error(error):
1291- self.assertEqual(str(error.error), "BADAPOFT!")
1292-
1293- self.assertFailure(d, ProviderInteractionError)
1294- d.addCallback(check_error)
1295- return d
1296
1297=== modified file 'ensemble/providers/ec2/tests/test_getmachines.py'
1298--- ensemble/providers/ec2/tests/test_getmachines.py 2011-08-17 08:58:51 +0000
1299+++ ensemble/providers/ec2/tests/test_getmachines.py 2011-08-26 13:16:39 +0000
1300@@ -132,9 +132,8 @@
1301 def verify(error):
1302 self.assertEquals(
1303 str(error),
1304- "ProviderError: Interaction with machine provider failed: "
1305- "\"EC2 error when looking up instances i-brokeit, i-msorry: "
1306- "unhelpful noises ('splat! kerpow!')\"")
1307+ "Unexpected EC2Error getting machines i-brokeit, i-msorry: "
1308+ "unhelpful noises ('splat! kerpow!')")
1309 d.addCallback(verify)
1310 return d
1311
1312
1313=== modified file 'ensemble/providers/ec2/tests/test_launch.py'
1314--- ensemble/providers/ec2/tests/test_launch.py 2011-08-16 13:55:11 +0000
1315+++ ensemble/providers/ec2/tests/test_launch.py 2011-08-26 13:16:39 +0000
1316@@ -141,10 +141,9 @@
1317 ProviderInteractionError)
1318 self.assertEqual(
1319 str(ex),
1320- self.get_wrapped_ec2_error_text(
1321- "ensemble-moon-machine-1", "delete security group",
1322- "There are active instances using security group %r"
1323- ))
1324+ "Unexpected EC2Error deleting security group "
1325+ "ensemble-moon-machine-1: There are active instances using "
1326+ "security group 'ensemble-moon-machine-1'")
1327
1328 def test_provider_type_machine_variable(self):
1329 """The provider type is available via cloud-init."""
1330
1331=== modified file 'ensemble/providers/ec2/tests/test_machine.py'
1332--- ensemble/providers/ec2/tests/test_machine.py 2011-07-20 07:10:30 +0000
1333+++ ensemble/providers/ec2/tests/test_machine.py 2011-08-26 13:16:39 +0000
1334@@ -1,5 +1,3 @@
1335-import datetime
1336-
1337 from txaws.ec2.model import Instance
1338
1339 from ensemble.lib.testing import TestCase
1340@@ -13,27 +11,10 @@
1341 instance = Instance(
1342 "i-foobar", "ignored",
1343 dns_name="public",
1344- private_dns_name="private",
1345- launch_time="2009-04-27T02:23:18.000Z")
1346-
1347- machine = machine_from_instance(instance)
1348- self.assertTrue(isinstance(machine, EC2ProviderMachine))
1349- self.assertEquals(machine.instance_id, instance.instance_id)
1350- self.assertEquals(machine.dns_name, instance.dns_name)
1351- self.assertEquals(machine.private_dns_name, instance.private_dns_name)
1352- self.assertEquals(machine.launch_time,
1353- datetime.datetime(2009, 4, 27, 2, 23, 18))
1354-
1355- def test_machine_from_instance_no_launch_time(self):
1356- instance = Instance(
1357- "i-foobar", "ignored",
1358- dns_name="public",
1359- private_dns_name="private",
1360- launch_time="")
1361-
1362- machine = machine_from_instance(instance)
1363- self.assertTrue(isinstance(machine, EC2ProviderMachine))
1364- self.assertEquals(machine.instance_id, instance.instance_id)
1365- self.assertEquals(machine.dns_name, instance.dns_name)
1366- self.assertEquals(machine.private_dns_name, instance.private_dns_name)
1367- self.assertEquals(machine.launch_time, None)
1368+ private_dns_name="private")
1369+
1370+ machine = machine_from_instance(instance)
1371+ self.assertTrue(isinstance(machine, EC2ProviderMachine))
1372+ self.assertEquals(machine.instance_id, instance.instance_id)
1373+ self.assertEquals(machine.dns_name, instance.dns_name)
1374+ self.assertEquals(machine.private_dns_name, instance.private_dns_name)
1375
1376=== modified file 'ensemble/providers/ec2/tests/test_provider.py'
1377--- ensemble/providers/ec2/tests/test_provider.py 2011-08-11 19:49:32 +0000
1378+++ ensemble/providers/ec2/tests/test_provider.py 2011-08-26 13:16:39 +0000
1379@@ -127,4 +127,7 @@
1380 config["authorized-keys-path"] = "File path"
1381 error = self.assertRaises(EnvironmentsConfigError,
1382 MachineProvider, "some-env-name", config)
1383- self.assertIn("authorized-keys", str(error))
1384+ self.assertEquals(
1385+ str(error),
1386+ "Environment config cannot define both authorized-keys and "
1387+ "authorized-keys-path. Pick one!")
1388
1389=== modified file 'ensemble/providers/ec2/tests/test_securitygroup.py'
1390--- ensemble/providers/ec2/tests/test_securitygroup.py 2011-08-10 17:19:09 +0000
1391+++ ensemble/providers/ec2/tests/test_securitygroup.py 2011-08-26 13:16:39 +0000
1392@@ -89,8 +89,8 @@
1393 ProviderInteractionError)
1394 self.assertEqual(
1395 str(ex),
1396- self.get_wrapped_ec2_error_text(
1397- "i-foobar", "open 80/tcp on machine"))
1398+ "Unexpected EC2Error opening 80/tcp on machine i-foobar: "
1399+ "The instance ID 'i-foobar' does not exist")
1400
1401 @inlineCallbacks
1402 def test_close_provider_port_unknown_instance(self):
1403@@ -108,8 +108,8 @@
1404 ProviderInteractionError)
1405 self.assertEqual(
1406 str(ex),
1407- self.get_wrapped_ec2_error_text(
1408- "i-foobar", "close 80/tcp on machine"))
1409+ "Unexpected EC2Error closing 80/tcp on machine i-foobar: "
1410+ "The instance ID 'i-foobar' does not exist")
1411
1412 @inlineCallbacks
1413 def test_get_provider_opened_ports_unknown_instance(self):
1414@@ -125,5 +125,5 @@
1415 ProviderInteractionError)
1416 self.assertEqual(
1417 str(ex),
1418- self.get_wrapped_ec2_error_text(
1419- "i-foobar", "get opened ports on machine"))
1420+ "Unexpected EC2Error getting open ports on machine i-foobar: "
1421+ "The instance ID 'i-foobar' does not exist")
1422
1423=== modified file 'ensemble/providers/orchestra/__init__.py'
1424--- ensemble/providers/orchestra/__init__.py 2011-08-25 16:11:49 +0000
1425+++ ensemble/providers/orchestra/__init__.py 2011-08-26 13:16:39 +0000
1426@@ -20,7 +20,7 @@
1427 self.cobbler = CobblerClient(config)
1428
1429 def get_file_storage(self):
1430- """Retrieve the provider C{FileStorage} abstraction."""
1431+ """Return a WebDAV-backed FileStorage abstraction."""
1432 if "storage-url" not in self.config:
1433 return FileStorage(
1434 "http://%(orchestra-server)s/webdav" % self.config)
1435@@ -41,10 +41,10 @@
1436 def get_machines(self, instance_ids=()):
1437 """List machines running in the provider.
1438
1439- @param instance_ids: ids of instances you want to get. Leave blank
1440- to list all machines owned by ensemble.
1441+ @param instance_ids: ids of instances you want to get. Leave empty
1442+ to list all machines owned by this provider.
1443
1444- @return: a list of EC2ProviderMachines.
1445+ @return: a list of OrchestraMachines.
1446
1447 @raise: MachinesNotFound
1448 """
1449
1450=== modified file 'ensemble/providers/orchestra/cobbler.py'
1451--- ensemble/providers/orchestra/cobbler.py 2011-08-22 13:01:02 +0000
1452+++ ensemble/providers/orchestra/cobbler.py 2011-08-26 13:16:39 +0000
1453@@ -3,8 +3,7 @@
1454 from twisted.internet.defer import inlineCallbacks, returnValue
1455 from twisted.web.xmlrpc import Proxy
1456
1457-from ensemble.errors import (
1458- MachinesNotFound, ProviderError, ProviderInteractionError)
1459+from ensemble.errors import MachinesNotFound, ProviderError
1460
1461
1462 class CobblerCaller(object):
1463@@ -62,7 +61,7 @@
1464 def check(actual):
1465 if actual != expect:
1466 raise ProviderError(
1467- "Bad result from call to %s with %s (got %r, expected %r)"
1468+ "Bad result from call to %s with %s: got %r, expected %r"
1469 % (name, args, actual, expect))
1470 return actual
1471
1472@@ -84,8 +83,9 @@
1473
1474 def extract_name(systems):
1475 if len(systems) > 1:
1476- raise ProviderInteractionError(
1477- "Got multiple instances with id %s" % instance_id)
1478+ raise ProviderError(
1479+ "Got multiple names for machine %s: %s"
1480+ % (instance_id, ", ".join(systems)))
1481 if not systems:
1482 raise MachinesNotFound([instance_id])
1483 return systems[0]
1484
1485=== modified file 'ensemble/providers/orchestra/machine.py'
1486--- ensemble/providers/orchestra/machine.py 2011-08-11 04:30:55 +0000
1487+++ ensemble/providers/orchestra/machine.py 2011-08-26 13:16:39 +0000
1488@@ -6,4 +6,8 @@
1489
1490
1491 def machine_from_dict(d):
1492+ """Convert a `dict` into an `OrchestraMachine`.
1493+
1494+ Expects dicts as returned by `CobblerClient.get_system[s]`
1495+ """
1496 return OrchestraMachine(d["uid"], d["name"], d["name"])
1497
1498=== modified file 'ensemble/providers/orchestra/tests/test_cobbler.py'
1499--- ensemble/providers/orchestra/tests/test_cobbler.py 2011-08-12 21:35:34 +0000
1500+++ ensemble/providers/orchestra/tests/test_cobbler.py 2011-08-26 13:16:39 +0000
1501@@ -3,8 +3,7 @@
1502 from twisted.internet.defer import fail, succeed
1503 from twisted.web.xmlrpc import Proxy
1504
1505-from ensemble.errors import (
1506- MachinesNotFound, ProviderError, ProviderInteractionError)
1507+from ensemble.errors import MachinesNotFound, ProviderError
1508 from ensemble.lib.testing import TestCase
1509 from ensemble.providers.orchestra.cobbler import CobblerCaller, CobblerClient
1510
1511@@ -243,11 +242,14 @@
1512 d.addCallback(verify)
1513 return d
1514
1515- def check_bad_call(self, d, method):
1516+ def check_bad_call(self, d, method, args):
1517 self.assertFailure(d, ProviderError)
1518
1519 def verify(error):
1520- self.assertIn("Bad result from call to %s" % method, str(error))
1521+ self.assertEquals(
1522+ str(error),
1523+ "Bad result from call to %s with %s: got False, expected True"
1524+ % (method, args))
1525 d.addCallback(verify)
1526 return d
1527
1528@@ -261,11 +263,13 @@
1529 self.mock_get_name(succeed(["some-name", "another-name"]))
1530 self.mocker.replay()
1531 d = self.call_cobbler_method(method_name, *args)
1532- self.assertFailure(d, ProviderInteractionError)
1533+ self.assertFailure(d, ProviderError)
1534
1535 def check_error(error):
1536- self.assertIn("Got multiple instances with id some-uid",
1537- str(error))
1538+ self.assertEquals(
1539+ str(error),
1540+ "Got multiple names for machine some-uid: some-name, "
1541+ "another-name")
1542 d.addCallback(check_error)
1543 return d
1544
1545@@ -301,7 +305,8 @@
1546 self.mock_modify_system(succeed(False), key, value)
1547 self.mocker.replay()
1548 d = self.call_cobbler_method(method_name, *args)
1549- return self.check_bad_call(d, "modify_system")
1550+ return self.check_bad_call(
1551+ d, "modify_system", ("some-handle", key, value))
1552
1553 def check_modify_system_error(self, key, value, method_name, *args):
1554 self.mock_modify_system(fail(SomeError()), key, value)
1555@@ -314,7 +319,7 @@
1556 self.mock_save_system(succeed(False))
1557 self.mocker.replay()
1558 d = self.call_cobbler_method(method_name, *args)
1559- return self.check_bad_call(d, "save_system")
1560+ return self.check_bad_call(d, "save_system", ("some-handle",))
1561
1562 def check_save_system_error(self, method_name, *args):
1563 self.mock_save_system(fail(SomeError()))
1564
1565=== added file 'ensemble/providers/orchestra/tests/test_connect.py'
1566--- ensemble/providers/orchestra/tests/test_connect.py 1970-01-01 00:00:00 +0000
1567+++ ensemble/providers/orchestra/tests/test_connect.py 2011-08-26 13:16:39 +0000
1568@@ -0,0 +1,111 @@
1569+from yaml import dump
1570+
1571+from twisted.internet.defer import fail, inlineCallbacks, succeed
1572+
1573+import zookeeper
1574+
1575+from txzookeeper import ZookeeperClient
1576+from txzookeeper.client import ConnectionTimeoutException
1577+from txzookeeper.tests.utils import deleteTree
1578+
1579+from ensemble.errors import EnvironmentPending, NoConnection
1580+from ensemble.lib.testing import TestCase
1581+from ensemble.state.sshclient import SSHClient
1582+from ensemble.tests.common import get_test_zookeeper_address
1583+
1584+from .common import OrchestraTestMixin
1585+
1586+
1587+class ConnectTest(TestCase, OrchestraTestMixin):
1588+
1589+ def mock_connect(self, share, result):
1590+ self.setup_mocks()
1591+ self.getPage("http://somewhe.re/webdav/provider-state")
1592+ self.mocker.result(succeed(dump(
1593+ {"zookeeper-instances": ["foo"]})))
1594+ self.proxy_m.callRemote("get_systems")
1595+ self.mocker.result(succeed([{"uid": "foo",
1596+ "name": "foo.example.com",
1597+ "mgmt_classes": ["acquired"]}]))
1598+ client = self.mocker.patch(SSHClient)
1599+ client.connect("foo.example.com:2181", timeout=30, share=share)
1600+ self.mocker.result(result)
1601+
1602+ def assert_connect_error(self, error, expect_type, expect_message):
1603+ self.mock_connect(False, fail(error))
1604+ self.mocker.replay()
1605+
1606+ d = self.get_provider().connect()
1607+
1608+ def check_error(error):
1609+ self.assertEqual(str(error), expect_message)
1610+ self.assertFailure(d, expect_type)
1611+ d.addCallback(check_error)
1612+ return d
1613+
1614+ def test_share_kwarg(self):
1615+ """The `share` kwarg should be passed through to `SSHClient.connect`"""
1616+ client = self.mocker.mock(type=SSHClient)
1617+ self.mock_connect(True, succeed(client))
1618+ client.exists_and_watch("/initialized")
1619+ self.mocker.result((succeed(True), None))
1620+ self.mocker.replay()
1621+
1622+ d = self.get_provider().connect(share=True)
1623+
1624+ def verify(result):
1625+ self.assertIdentical(result, client)
1626+ d.addCallback(verify)
1627+ return d
1628+
1629+ def test_no_connection(self):
1630+ """`NoConnection` errors should become `EnvironmentPending`s"""
1631+ return self.assert_connect_error(
1632+ NoConnection("KABOOM!"), EnvironmentPending,
1633+ "Cannot connect to machine foo (perhaps still initializing): "
1634+ "KABOOM!")
1635+
1636+ def test_txzookeeper_error(self):
1637+ """
1638+ `ConnectionTimeoutException` errors should become `EnvironmentPending`s
1639+ """
1640+ return self.assert_connect_error(
1641+ ConnectionTimeoutException("SPLAT!"), EnvironmentPending,
1642+ "Cannot connect to machine foo (perhaps still initializing): "
1643+ "SPLAT!")
1644+
1645+ def test_other_error(self):
1646+ """Other errors should propagate"""
1647+ return self.assert_connect_error(
1648+ TypeError("THUD!"), TypeError, "THUD!")
1649+
1650+ @inlineCallbacks
1651+ def test_wait_for_initialize(self):
1652+ """
1653+ A connection to a zookeeper that is running, but whose ensemble state
1654+ is not ready, should wait until that state is ready.
1655+ """
1656+ client = ZookeeperClient()
1657+ self.client = client # for poke_zk
1658+ self.mock_connect(False, succeed(client))
1659+ self.mocker.replay()
1660+
1661+ zookeeper.set_debug_level(0)
1662+ yield client.connect(get_test_zookeeper_address())
1663+
1664+ d = self.get_provider().connect()
1665+ client_result = []
1666+ d.addCallback(client_result.append)
1667+
1668+ # Give it an opportunity to do it incorrectly.
1669+ yield self.poke_zk()
1670+
1671+ try:
1672+ self.assertEquals(client_result, [])
1673+ yield client.create("/initialized")
1674+ yield d
1675+ self.assertTrue(client_result, client_result)
1676+ self.assertIdentical(client_result[0], client)
1677+ finally:
1678+ deleteTree("/", client.handle)
1679+ client.close()
1680
1681=== modified file 'ensemble/providers/orchestra/tests/test_launch.py'
1682--- ensemble/providers/orchestra/tests/test_launch.py 2011-08-11 23:27:45 +0000
1683+++ ensemble/providers/orchestra/tests/test_launch.py 2011-08-26 13:16:39 +0000
1684@@ -30,9 +30,13 @@
1685
1686 d = self.get_provider().start_machine({"machine-id": "32767"})
1687 self.assertFailure(d, ProviderError)
1688- self.assertIn("Could not find any Cobbler systems marked as available "
1689- "and configured for network boot.",
1690- str(d))
1691+
1692+ def verify(error):
1693+ self.assertEquals(
1694+ str(error),
1695+ "Could not find any Cobbler systems marked as available and "
1696+ "configured for network boot.")
1697+ d.addCallback(verify)
1698 return d
1699
1700 def test_no_acceptable_machines(self):
1701@@ -43,9 +47,13 @@
1702
1703 d = self.get_provider().start_machine({"machine-id": "32767"})
1704 self.assertFailure(d, ProviderError)
1705- self.assertIn("All available Cobbler systems were also marked as "
1706- "acquired (instances: bad-system-uid).",
1707- str(d))
1708+
1709+ def verify(error):
1710+ self.assertEquals(
1711+ str(error),
1712+ "All available Cobbler systems were also marked as acquired "
1713+ "(instances: bad-system-uid).")
1714+ d.addCallback(verify)
1715 return d
1716
1717 def test_actually_launch(self):
1718
1719=== modified file 'ensemble/providers/orchestra/tests/test_provider.py'
1720--- ensemble/providers/orchestra/tests/test_provider.py 2011-08-23 16:07:09 +0000
1721+++ ensemble/providers/orchestra/tests/test_provider.py 2011-08-26 13:16:39 +0000
1722@@ -53,7 +53,10 @@
1723 config["authorized-keys-path"] = "File path"
1724 error = self.assertRaises(EnvironmentsConfigError,
1725 MachineProvider, "some-env-name", config)
1726- self.assertIn("authorized-keys", str(error))
1727+ self.assertEquals(
1728+ str(error),
1729+ "Environment config cannot define both authorized-keys and "
1730+ "authorized-keys-path. Pick one!")
1731
1732 @inlineCallbacks
1733 def test_open_port(self):
1734@@ -75,4 +78,3 @@
1735 yield MachineProvider("blah", CONFIG).get_opened_ports(None, None)
1736 self.assertIn(
1737 "Firewalling is not yet implemented in Orchestra", log.getvalue())
1738-
1739
1740=== modified file 'ensemble/state/service.py'
1741--- ensemble/state/service.py 2011-08-08 03:22:15 +0000
1742+++ ensemble/state/service.py 2011-08-26 13:16:39 +0000
1743@@ -855,7 +855,8 @@
1744 cleared.
1745 """
1746 if not isinstance(hook_names, (list, tuple)):
1747- raise AssertionError("hook names must be a list %r" % hook_names)
1748+ raise AssertionError("Hook names must be a list: got %r"
1749+ % hook_names)
1750
1751 if "*" in hook_names and len(hook_names) > 1:
1752 msg = "Ambigious to debug all hooks and named hooks %r" % (
1753
1754=== modified file 'ensemble/state/sshclient.py'
1755--- ensemble/state/sshclient.py 2011-08-15 00:41:54 +0000
1756+++ ensemble/state/sshclient.py 2011-08-26 13:16:39 +0000
1757@@ -193,6 +193,8 @@
1758 else:
1759 returnValue(self)
1760 self.close()
1761+ # we raise ConnectionTimeoutException (rather than one of our own, with
1762+ # the same meaning) to maintain ZookeeperClient interface
1763 raise ConnectionTimeoutException(
1764 "could not connect before timeout after %d retries" % num_retries)
1765
1766
1767=== modified file 'ensemble/state/tests/test_security.py'
1768--- ensemble/state/tests/test_security.py 2011-08-05 22:55:52 +0000
1769+++ ensemble/state/tests/test_security.py 2011-08-26 13:16:39 +0000
1770@@ -316,7 +316,7 @@
1771 principal = Principal("zebra", "zoo")
1772 error = yield self.assertFailure(self.db.get(principal.name),
1773 PrincipalNotFound)
1774- self.assertIn("zebra", str(error))
1775+ self.assertEquals(str(error), "Principal 'zebra' not found")
1776
1777
1778 class PolicyTest(TestCase):
1779
1780=== modified file 'ensemble/state/tests/test_service.py'
1781--- ensemble/state/tests/test_service.py 2011-08-22 23:02:38 +0000
1782+++ ensemble/state/tests/test_service.py 2011-08-26 13:16:39 +0000
1783@@ -1111,8 +1111,10 @@
1784 error = yield self.assertFailure(
1785 unit_state.enable_hook_debug(["*", "db-relation-changed"]),
1786 ValueError)
1787- self.assertIn("Ambigious to debug all hooks and named hooks",
1788- str(error))
1789+ self.assertEquals(
1790+ str(error),
1791+ "Ambigious to debug all hooks and named hooks "
1792+ "['*', 'db-relation-changed']")
1793
1794 @inlineCallbacks
1795 def test_enable_debug_requires_sequence(self):
1796@@ -1123,7 +1125,7 @@
1797 error = yield self.assertFailure(
1798 unit_state.enable_hook_debug(None),
1799 AssertionError)
1800- self.assertIn("hook names must be a list", str(error))
1801+ self.assertEquals(str(error), "Hook names must be a list: got None")
1802
1803 @inlineCallbacks
1804 def test_enable_named_debug_hook(self):
1805@@ -1144,8 +1146,10 @@
1806 """
1807 unit_state = yield self.get_unit_state()
1808 yield unit_state.enable_hook_debug(["*"])
1809- yield self.assertFailure(unit_state.enable_hook_debug(["*"]),
1810- ServiceUnitDebugAlreadyEnabled)
1811+ error = yield self.assertFailure(unit_state.enable_hook_debug(["*"]),
1812+ ServiceUnitDebugAlreadyEnabled)
1813+ self.assertEquals(
1814+ str(error), "Service unit 'wordpress/0' is already in debug mode.")
1815
1816 @inlineCallbacks
1817 def test_enable_debug_hook_lifetime(self):
1818
1819=== modified file 'ensemble/tests/test_errors.py'
1820--- ensemble/tests/test_errors.py 2011-08-17 17:26:19 +0000
1821+++ ensemble/tests/test_errors.py 2011-08-26 13:16:39 +0000
1822@@ -23,20 +23,20 @@
1823
1824 def test_FileNotFound(self):
1825 error = FileNotFound("/path")
1826- self.assertEquals(str(error), "File was not found: /path")
1827+ self.assertEquals(str(error), "File was not found: '/path'")
1828 self.assertIsEnsembleError(error)
1829
1830 def test_FileAlreadyExists(self):
1831 error = FileAlreadyExists("/path")
1832 self.assertEquals(str(error),
1833- "File already exists, won't overwrite: /path")
1834+ "File already exists, won't overwrite: '/path'")
1835 self.assertIsEnsembleError(error)
1836
1837 def test_InvalidEnsembleHeaderValue(self):
1838 error = InvalidEnsembleHeaderValue("/path", "xpctd", "fnd")
1839 self.assertEquals(str(error),
1840 "Expected a YAML file with an 'ensemble: xpctd' "
1841- "header, found 'ensemble: fnd': /path")
1842+ "header, found 'ensemble: fnd': '/path'")
1843 self.assertIsEnsembleError(error)
1844
1845 def test_NoConnection(self):
1846@@ -62,12 +62,9 @@
1847 self.assertIsEnsembleError(error)
1848
1849 def test_ProviderInteractionError(self):
1850- error = ProviderInteractionError(OSError("Bad Stuff"))
1851+ error = ProviderInteractionError("Bad Stuff")
1852 self.assertIsEnsembleError(error)
1853- self.assertEquals(
1854- str(error),
1855- "ProviderError: Interaction with machine provider failed: "
1856- "OSError('Bad Stuff',)")
1857+ self.assertEquals(str(error), "Bad Stuff")
1858
1859 def test_CannotTerminateMachine(self):
1860 error = CannotTerminateMachine(0, "environment would be destroyed")
1861@@ -101,11 +98,4 @@
1862
1863 def testEnvironmentPendingWithInfo(self):
1864 error = EnvironmentPending("problem")
1865- self.assertEquals(str(error),
1866- "Ensemble environment is not accessible: problem")
1867-
1868- def testEnvironmentPendingNoInfo(self):
1869- error = EnvironmentPending()
1870- self.assertEquals(str(error),
1871- "Ensemble environment is not accessible: no "
1872- "details available")
1873+ self.assertEquals(str(error), "problem")
1874
1875=== modified file 'ensemble/unit/tests/test_formula.py'
1876--- ensemble/unit/tests/test_formula.py 2011-08-18 21:33:55 +0000
1877+++ ensemble/unit/tests/test_formula.py 2011-08-26 13:16:39 +0000
1878@@ -191,4 +191,5 @@
1879 self.client, "local:mickey-21", formula_directory),
1880 FormulaStateNotFound)
1881
1882- self.assertIn("Formula 'local:mickey-21' was not found", str(error))
1883+ self.assertEquals(str(error),
1884+ "Formula 'local:mickey-21' was not found")

Subscribers

People subscribed via source and target branches

to status/vote changes: