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

Proposed by William Reade
Status: Merged
Approved by: Kapil Thangavelu
Approved revision: 318
Merged at revision: 291
Proposed branch: lp:~fwereade/pyjuju/cobbler-connect-production
Merge into: lp:pyjuju
Prerequisite: lp:~fwereade/pyjuju/config-selectdict
Diff against target: 468 lines (+352/-11)
9 files modified
ensemble/environment/config.py (+6/-0)
ensemble/environment/tests/test_config.py (+34/-0)
ensemble/lib/testing.py (+2/-0)
ensemble/providers/orchestra/__init__.py (+22/-8)
ensemble/providers/orchestra/cobbler.py (+107/-0)
ensemble/providers/orchestra/launch.py (+20/-0)
ensemble/providers/orchestra/machine.py (+6/-0)
ensemble/providers/orchestra/tests/test_bootstrap.py (+143/-0)
ensemble/providers/orchestra/tests/test_provider.py (+12/-3)
To merge this branch: bzr merge lp:~fwereade/pyjuju/cobbler-connect-production
Reviewer Review Type Date Requested Status
Kapil Thangavelu (community) Approve
Gustavo Niemeyer Approve
Review via email: mp+68706@code.launchpad.net

Description of the change

* added schema for orchestra provider
* added CobblerConnect for communicating with cobbler
* added OrchestraLaunchMachine, which can now actually launch a machine(which won't do anything)

To post a comment you must log in.
315. By William Reade

merge parent

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Looks nice. Just a few minors, and +1.

[1]

+ @property
+ def _acquired_class(self):
+ return self._config["acquired-mgmt-class"]
+
+ @property
+ def _available_class(self):
+ return self._config["available-mgmt-class"]
+
+ @property
+ def _credentials(self):
+ return (self._config["orchestra-user"],
+ self._config["orchestra-pass"])

Not a big deal, but just as some feedback, this feels like
adding more complexity than necessary for the problem at
hand. Compare with this version:

 self._acquired_class = self._config["acquired-mgmt-class"]
 self._available_class = self._config["available-mgmt-class"]
 self._user = self._config["orchestra-user"]
 self._pass = self._config["orchestra-pass"]

[2]

+ if "invalid token" not in failure.getErrorMessage():
+ return failure
+ login = self._login()
+ login.addCallback(lambda unused: call())

Looks nice.

[3]

+ failure.trap(Fault)
+ if "invalid token" not in failure.getErrorMessage():
+ return failure

None of that logic is covered by tests, though.

[4]

+ def check(actual):
+ if actual != expect:
+ raise ProviderError(
+ "Bad result from call to %s with %s (got %r, expected %r)"
+ % (name, args, actual, expect))
+ return actual

Also untouched by tests.

[5]

+ self.mock_proxy()
+ self.mock_get_systems()
+ self.mock_acquire_system()
+ self.mock_set_ks_meta()
+ self.mock_start_system()

I guess there's no better way to test that interaction with the external system,
but I have to say I'm a bit sad about the amount of mocking here.

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

Quick responses:

[1] I guess something in my past has caused me to become fanatical about having a single point of truth for every possible piece of data in the system. What if someone changes config underneath me!? (Yeah, I'll change it.)

[2] Cheers :-).

[3] Yes it is: test_bootstrap.py, lines 43-48. I'm open to the argument that testing a single call that happens to follow this path is a little weak, but I'm reluctant to add further complexity to the already excessive mocking here. See below...

[4] Thanks, nice catch. I have a comment I'll come back to in the light of...

[5] It'll only get worse in future branches :-(. On the bright side, I think this is a good opportunity for a philosophical ramble :-p.

I'd categorise your favoured testing style as "integration" rather than "unit", which I've tended to neglect in the past, but I'm reevaluating that: factoring the generic code out of providers.ec2 was *much* easier than I expected, largely thanks to this testing style.

My own approach has historically tended to favour ludicrously tight unit testing, in which *everything* that isn't part of the SUT is mocked out; the obvious drawback is that the burden of significant refactorings can be much heavier (and it's very possible to miss tests which expect another object to have an outdated interface), but I've generally had good experiences when my unit tests are backed up by relatively few high-level functional tests: 9 times out of 10, I've found, an end-to-end test of the happy path will flush out the inconsistencies.

In the end, I think, it's a choice between testing (1) the ultimate external effects of an interaction with the SUT or (2) the precise interactions with the SUT's dependencies; which path is better in the long term depends on the interactions' relative susceptibility to change. I do feel that only (2) really qualifies as "unit" testing, but that's frankly irrelevant: reliable coverage matters a lot more than the names we happen to use for the techniques we employ.

I'm not advocating a change of approach here, but I would say that we're we're pretty close to the line: the current strategy means that a tweak to CobblerConnect's interaction with the server will lead to failures in an arbitrary number of tests, none of which are related to the cause of the failure (except coincidentally). We'll have to see how it shakes out here; I think the fact that the cobbler API is much chattier than EC2's may eventually justify a different approach locally, but it hasn't hurt me enough to seriously complain. Yet :-).

[3/4 redux] In [3], I'm comfortable testing only one interaction with the "invalid token" code, because I "know" everything goes through ._call(). In [4], there are 3 distinct operations that can hit ._check_call(), and under the current approach I can't see any option but to test both happy and sad paths each one individually... but this makes me uncomfortable, because a minor tweak to that code would entail fixing at least 6 tests. I'll fix it anyway; I just had an urge to register my misgivings ;-).

Er, I really did intend these to be quick responses. Thanks for the thorough review!

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

[4] Further thought: since all uses of _check_call() are actually in _set_on_system, I intend to write a relatively small number of tests that exercise that code, giving us a similar imperfect-but-wieldy level of testing to that in [3]. Tolerable?

316. By William Reade

whitespace

317. By William Reade

merge from trunk; also changes per niemeyer's review

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

[3]

Try taking the commented lines out and run test_bootstrap.

[5]

Not entirely. It is possible to do good unit testing without mocking, but I agree
that in some cases good unit testing moves toward the integration side of things,
and that sounds quite alright in most cases.

The test should be as unity as possible before it starts to get fake and invasive.
Mocking is silly in the sense that it forces you to repeat the same logic twice
(e.g. call foo() in the test, and call foo() in the code), giving a false sense
of confidence in the logic. The outcome is something that does not guarantee
correctness, because you were worried about "how-to" when you wrote the test rather
than the outcome, and that very fact binds the implementation to the test in a
tight way that makes it hard to refactor, as you noticed by counter example.

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

[3] Bah. Thank you, nice catch; on it.

[5] I think we agree on the problem, at least ;).

318. By William Reade

*properly* fixed testing per niemeyer's review

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

I can't speak at all to what this means in terms of cobbler/orchestra interaction but code style/syntax looks good.

[0] This bit appears unused and extraneous.

ensemble/lib/testing.py

74 + client_got = False
75 +

[1] Just a naming thing, but i'd change CobblerConnect to CobblerConnection

[2] random thoughts.

For launching machines is there anyway to distinguish size/hardware capabilities? I'm just thinking that in the future, we'll want to have some abstracted notion of machine size so we can utilize them for deploying services that need additional hardware capabilities available to a machine provider.

[3] Some pep8/syntax items that came up captured in diff to fix.
https://pastebin.canonical.com/50619/

[4] I'm curious what the ensemble late command stuff is.. it might be nice to have some documentation of the orchestration integration when all of this said and done.

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

> [1] Just a naming thing, but i'd change CobblerConnect to CobblerConnection

Hm, it's not really a *connection* as such, but I agree it's not a very helpful name; I think CobblerClient fits better for now.

> [2] random thoughts.
>
> For launching machines is there anyway to distinguish size/hardware
> capabilities? I'm just thinking that in the future, we'll want to have some
> abstracted notion of machine size so we can utilize them for deploying
> services that need additional hardware capabilities available to a machine
> provider.

Not yet; agree we'll need it at some stage.

> [3] Some pep8/syntax items that came up captured in diff to fix.
> https://pastebin.canonical.com/50619/

Thanks :).

> [4] I'm curious what the ensemble late command stuff is.. it might be nice to
> have some documentation of the orchestration integration when all of this said
> and done.

It's basically just writing the user-data/meta-data files for cloud-init. I agree that we'll want documentation for the Orchestra provider, but there are still a fair number of unanswered questions, so I'm not sure it's quite the right time yet.

319. By William Reade

merge trunk

320. By William Reade

naming, pep8, redundant code (per hazmat's review)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ensemble/environment/config.py'
2--- ensemble/environment/config.py 2011-07-20 22:49:19 +0000
3+++ ensemble/environment/config.py 2011-08-01 20:04:31 +0000
4@@ -45,6 +45,12 @@
5 optional=["access-key", "secret-key",
6 "default-instance-type", "default-ami",
7 "region", "ec2-uri", "s3-uri"]),
8+ "orchestra": KeyDict({"orchestra-server": String(),
9+ "orchestra-user": String(),
10+ "orchestra-pass": String(),
11+ "admin-secret": String(),
12+ "acquired-mgmt-class": String(),
13+ "available-mgmt-class": String()}),
14 "dummy": KeyDict({})}))},
15 optional=["default"])
16
17
18=== modified file 'ensemble/environment/tests/test_config.py'
19--- ensemble/environment/tests/test_config.py 2011-07-20 22:49:19 +0000
20+++ ensemble/environment/tests/test_config.py 2011-08-01 20:04:31 +0000
21@@ -24,6 +24,19 @@
22 nofoo: 1
23 """
24
25+SAMPLE_ORCHESTRA = """
26+ensemble: environments
27+
28+environments:
29+ sample:
30+ type: orchestra
31+ orchestra-server: somewhe.re
32+ orchestra-user: user
33+ orchestra-pass: pass
34+ admin-secret: garden
35+ acquired-mgmt-class: acquired
36+ available-mgmt-class: available
37+"""
38
39 class EnvironmentsConfigTestBase(TestCase):
40
41@@ -537,3 +550,24 @@
42 else:
43 self.fail("Did not properly require "
44 "control-buckets when type == 'ec2'")
45+
46+ def test_orchestra_schema_requires(self):
47+ requires = (
48+ "type orchestra-server orchestra-user orchestra-pass "
49+ "admin-secret acquired-mgmt-class available-mgmt-class").split()
50+ for require in requires:
51+ config = yaml.load(SAMPLE_ORCHESTRA)
52+ del config["environments"]["sample"][require]
53+ self.write_config(yaml.dump(config), other_path=True)
54+
55+ try:
56+ self.config.load(self.other_path)
57+ except EnvironmentsConfigError as error:
58+ self.assertEquals(str(error),
59+ "Environments configuration error: %s: "
60+ "environments.sample.%s: "
61+ "required value not found"
62+ % (self.other_path, require))
63+ else:
64+ self.fail("Did not properly require %s when type == orchestra"
65+ % require)
66
67=== modified file 'ensemble/lib/testing.py'
68--- ensemble/lib/testing.py 2011-06-14 16:07:14 +0000
69+++ ensemble/lib/testing.py 2011-08-01 20:04:31 +0000
70@@ -135,6 +135,8 @@
71 yield self.client.exists("/zookeeper")
72 returnValue(True)
73
74+ client_got = False
75+
76 def get_zookeeper_client(self):
77 client = ZookeeperClient(
78 get_test_zookeeper_address(), session_timeout=1000)
79
80=== renamed directory 'ensemble/providers/cobbler' => 'ensemble/providers/orchestra'
81=== modified file 'ensemble/providers/orchestra/__init__.py'
82--- ensemble/providers/cobbler/__init__.py 2011-07-20 07:10:30 +0000
83+++ ensemble/providers/orchestra/__init__.py 2011-08-01 20:04:31 +0000
84@@ -1,16 +1,20 @@
85-from ensemble.machine import ProviderMachine
86-
87-
88-class CobblerMachine(ProviderMachine):
89- """Cobbler-specific provider machine implementation"""
90- pass
91+from twisted.internet.defer import fail, succeed
92+
93+from ensemble.errors import EnvironmentNotFound
94+from ensemble.providers.common.bootstrap import Bootstrap
95+
96+from .cobbler import CobblerConnect
97+from .launch import OrchestraLaunchMachine
98
99
100 class MachineProvider(object):
101
102+ launch_machine_class = OrchestraLaunchMachine
103+
104 def __init__(self, environment_name, config):
105 self.environment_name = environment_name
106 self.config = config
107+ self.cobbler = CobblerConnect(config)
108
109 def connect(self, share=False):
110 """
111@@ -50,7 +54,8 @@
112 """
113 Bootstrap an ensemble server in the provider.
114 """
115- raise NotImplementedError()
116+ bootstrap = Bootstrap(self)
117+ return bootstrap.run()
118
119 def shutdown(self):
120 """
121@@ -71,7 +76,7 @@
122 @param state
123 @type dict
124 """
125- raise NotImplementedError()
126+ return succeed(None)
127
128 def load_state(self):
129 """
130@@ -111,6 +116,15 @@
131 """
132 raise NotImplementedError()
133
134+ def get_zookeeper_machines(self):
135+ """Find running zookeeper instances.
136+
137+ @return: the first valid instance found as a single element list.
138+
139+ @raise: EnvironmentNotFound or EnvironmentPending
140+ """
141+ return fail(EnvironmentNotFound())
142+
143
144 class FileStorage(object):
145
146
147=== added file 'ensemble/providers/orchestra/cobbler.py'
148--- ensemble/providers/orchestra/cobbler.py 1970-01-01 00:00:00 +0000
149+++ ensemble/providers/orchestra/cobbler.py 2011-08-01 20:04:31 +0000
150@@ -0,0 +1,107 @@
151+from xmlrpclib import Fault
152+
153+from twisted.internet.defer import inlineCallbacks, returnValue
154+from twisted.web.xmlrpc import Proxy
155+
156+from ensemble.errors import ProviderError
157+
158+
159+def _acceptable_system(systems, available_class):
160+ for system in systems:
161+ if available_class not in system["mgmt_classes"]:
162+ continue
163+ if not system["netboot_enabled"]:
164+ continue
165+ return system
166+ raise ProviderError("no acceptable systems available")
167+
168+
169+class CobblerConnect(object):
170+ """Handles the details of communicating with a Cobbler server"""
171+
172+ def __init__(self, config):
173+ self._user = config["orchestra-user"]
174+ self._pass = config["orchestra-pass"]
175+ self._token = ""
176+ self._acquired_class = config["acquired-mgmt-class"]
177+ self._available_class = config["available-mgmt-class"]
178+ self._proxy = Proxy("http://%(orchestra-server)s/cobbler_api" % config)
179+
180+ def _login(self):
181+ login = self._call("login", (self._user, self._pass))
182+ login.addCallback(self._set_token)
183+ return login
184+
185+ def _set_token(self, token):
186+ self._token = token
187+
188+ def _call(self, name, args=(), auth=False):
189+
190+ def call():
191+ call_args = args
192+ if auth:
193+ call_args += (self._token,)
194+ return self._proxy.callRemote(name, *call_args)
195+
196+ def login_retry(failure):
197+ # Login tokens expire after an hour: it seems more sensible
198+ # to assume we always have a valid one, and to relogin and
199+ # retry if it fails, than to try to maintain validity state.
200+ # NOTE: some methods, such as get_system_handle, expect
201+ # tokens but appear not to check validity.
202+ failure.trap(Fault)
203+ if "invalid token" not in failure.getErrorMessage():
204+ return failure
205+ login = self._login()
206+ login.addCallback(lambda unused: call())
207+ return login
208+
209+ result = call()
210+ if auth:
211+ result.addErrback(login_retry)
212+ return result
213+
214+ def _check_call(self, name, args=(), auth=False, expect=None):
215+
216+ def check(actual):
217+ if actual != expect:
218+ raise ProviderError(
219+ "Bad result from call to %s with %s (got %r, expected %r)"
220+ % (name, args, actual, expect))
221+ return actual
222+
223+ call = self._call(name, args, auth=auth)
224+ call.addCallback(check)
225+ return call
226+
227+ @inlineCallbacks
228+ def _set_on_system(self, name, key, value):
229+ handle = yield self._call("get_system_handle", (name,), auth=True)
230+ yield self._check_call("modify_system", (handle, key, value),
231+ auth=True, expect=True)
232+ yield self._check_call("save_system", (handle,), auth=True, expect=True)
233+
234+ @inlineCallbacks
235+ def acquire_system(self):
236+ """Find a system marked as available and mark it as acquired.
237+
238+ @return: the name of the acquired system.
239+
240+ @raise: ProviderError if no suitable system can be found.
241+ """
242+ systems = yield self._call("get_systems")
243+ system = _acceptable_system(systems, self._available_class)
244+ name = system["name"]
245+ # TODO surely we should preserve unrelated management classes?
246+ yield self._set_on_system(name, "mgmt_classes", [self._acquired_class])
247+ returnValue(name)
248+
249+ def set_system_ks_meta(self, name, ks_meta):
250+ return self._set_on_system(name, "ks_meta", ks_meta)
251+
252+ @inlineCallbacks
253+ def start_system(self, name):
254+ yield self._set_on_system(name, "netboot_enabled", True)
255+ yield self._call("background_power_system",
256+ ({"power": "on", "systems": [name]},),
257+ auth=True)
258
259=== added file 'ensemble/providers/orchestra/launch.py'
260--- ensemble/providers/orchestra/launch.py 1970-01-01 00:00:00 +0000
261+++ ensemble/providers/orchestra/launch.py 2011-08-01 20:04:31 +0000
262@@ -0,0 +1,20 @@
263+from twisted.internet.defer import inlineCallbacks, returnValue
264+
265+from ensemble.providers.common.launch import LaunchMachine
266+from ensemble.providers.orchestra.machine import OrchestraMachine
267+
268+
269+class OrchestraLaunchMachine(LaunchMachine):
270+
271+ def get_instance_id_command(self):
272+ return "this doesn't work yet"
273+
274+ @inlineCallbacks
275+ def start_machine(self, variables):
276+ name = yield self._provider.cobbler.acquire_system()
277+ ks_meta = 'ENSEMBLE_LATE_COMMAND="true"'
278+
279+ yield self._provider.cobbler.set_system_ks_meta(name, ks_meta)
280+ yield self._provider.cobbler.start_system(name)
281+
282+ returnValue([OrchestraMachine(name, name)])
283
284=== added file 'ensemble/providers/orchestra/machine.py'
285--- ensemble/providers/orchestra/machine.py 1970-01-01 00:00:00 +0000
286+++ ensemble/providers/orchestra/machine.py 2011-08-01 20:04:31 +0000
287@@ -0,0 +1,6 @@
288+from ensemble.machine import ProviderMachine
289+
290+
291+class OrchestraMachine(ProviderMachine):
292+ """Orchestra-specific provider machine implementation"""
293+ pass
294
295=== added file 'ensemble/providers/orchestra/tests/test_bootstrap.py'
296--- ensemble/providers/orchestra/tests/test_bootstrap.py 1970-01-01 00:00:00 +0000
297+++ ensemble/providers/orchestra/tests/test_bootstrap.py 2011-08-01 20:04:31 +0000
298@@ -0,0 +1,143 @@
299+from xmlrpclib import Fault
300+
301+from twisted.internet.defer import fail, succeed
302+from twisted.web.xmlrpc import Proxy
303+
304+from ensemble.errors import ProviderError
305+from ensemble.lib.mocker import MATCH
306+from ensemble.lib.testing import TestCase
307+from ensemble.providers.orchestra import MachineProvider
308+from ensemble.providers.orchestra.machine import OrchestraMachine
309+
310+CONFIG = {"orchestra-server": "somewhe.re",
311+ "orchestra-user": "user",
312+ "orchestra-pass": "pass",
313+ "acquired-mgmt-class": "acquired",
314+ "available-mgmt-class": "available",
315+ "admin-secret": "SEEKRIT"}
316+
317+
318+class OrchestraBootstrapTest(TestCase):
319+
320+ def mock_proxy(self):
321+ self.proxy_m = self.mocker.mock(Proxy)
322+ Proxy_m = self.mocker.replace(Proxy, spec=None)
323+ Proxy_m("http://somewhe.re/cobbler_api")
324+ self.mocker.result(self.proxy_m)
325+
326+ def mock_get_systems(self, acceptable=True):
327+ systems = [
328+ {"mgmt_classes": ["unknown"]}, # skipped: wrong class
329+ {"mgmt_classes": ["available"], # skipped: cannot remote boot
330+ "netboot_enabled": False}]
331+ if acceptable:
332+ systems.append(
333+ {"mgmt_classes": ["available"], # relevant
334+ "netboot_enabled": True,
335+ "name": "winston"})
336+
337+ self.proxy_m.callRemote("get_systems")
338+ self.mocker.result(succeed(systems))
339+
340+ def mock_acquire_system(self, unexpected_auth_error=None):
341+ self.proxy_m.callRemote("get_system_handle", "winston", "")
342+ if unexpected_auth_error is not None:
343+ self.mocker.result(fail(unexpected_auth_error))
344+ return
345+ self.mocker.result(fail(Fault("blah", "blah invalid token blah")))
346+ self.proxy_m.callRemote("login", "user", "pass")
347+ self.mocker.result(succeed("TOKEN"))
348+ self.proxy_m.callRemote("get_system_handle", "winston", "TOKEN")
349+ self.mocker.result(succeed("smith"))
350+ self.proxy_m.callRemote(
351+ "modify_system", "smith", "mgmt_classes", ["acquired"], "TOKEN")
352+ self.mocker.result(succeed(True))
353+ self.proxy_m.callRemote("save_system", "smith", "TOKEN")
354+ self.mocker.result(succeed(True))
355+
356+ def mock_set_ks_meta(self, fail_modify=False, fail_save=False):
357+ self.proxy_m.callRemote("get_system_handle", "winston", "TOKEN")
358+ self.mocker.result(succeed("smith"))
359+ # TODO: parameterise this match?
360+ match_ks_meta = MATCH(lambda s: s.startswith('ENSEMBLE_LATE_COMMAND="'))
361+ self.proxy_m.callRemote(
362+ "modify_system", "smith", "ks_meta", match_ks_meta, "TOKEN")
363+ if fail_modify:
364+ self.mocker.result(succeed(False))
365+ return
366+ self.mocker.result(succeed(True))
367+ self.proxy_m.callRemote("save_system", "smith", "TOKEN")
368+ if fail_save:
369+ self.mocker.result(succeed(False))
370+ return
371+ self.mocker.result(succeed(True))
372+
373+ def mock_start_system(self):
374+ self.proxy_m.callRemote("get_system_handle", "winston", "TOKEN")
375+ self.mocker.result(succeed("smith"))
376+ self.proxy_m.callRemote(
377+ "modify_system", "smith", "netboot_enabled", True, "TOKEN")
378+ self.mocker.result(succeed(True))
379+ self.proxy_m.callRemote("save_system", "smith", "TOKEN")
380+ self.mocker.result(succeed(True))
381+ self.proxy_m.callRemote(
382+ "background_power_system",
383+ {"power": "on", "systems": ["winston"]},
384+ "TOKEN")
385+ self.mocker.result(succeed("[some-timestamp]_power"))
386+
387+ def test_no_machines_available(self):
388+ self.mock_proxy()
389+ self.mock_get_systems(acceptable=False)
390+ self.mocker.replay()
391+ provider = MachineProvider("tetrascape", CONFIG)
392+ d = provider.bootstrap()
393+ self.assertFailure(d, ProviderError)
394+
395+ def verify_auth_error(self, error):
396+ self.mock_proxy()
397+ self.mock_get_systems()
398+ self.mock_acquire_system(error)
399+ self.mocker.replay()
400+ provider = MachineProvider("tetrascape", CONFIG)
401+ d = provider.bootstrap()
402+ self.assertFailure(d, type(error))
403+
404+ def test_non_auth_fault(self):
405+ return self.verify_auth_error(Fault("blah", "some random error"))
406+
407+ def test_non_auth_error(self):
408+ return self.verify_auth_error(Exception("fiddlesticks"))
409+
410+ def verify_change_failures(self, **kwargs):
411+ self.mock_proxy()
412+ self.mock_get_systems()
413+ self.mock_acquire_system()
414+ self.mock_set_ks_meta(**kwargs)
415+ self.mocker.replay()
416+ provider = MachineProvider("tetrascape", CONFIG)
417+ d = provider.bootstrap()
418+ self.assertFailure(d, ProviderError)
419+
420+ def test_cannot_modify_machine(self):
421+ self.verify_change_failures(fail_modify=True)
422+
423+ def test_cannot_save_machine(self):
424+ self.verify_change_failures(fail_save=True)
425+
426+ def test_launch_available_machine(self):
427+ self.mock_proxy()
428+ self.mock_get_systems()
429+ self.mock_acquire_system()
430+ self.mock_set_ks_meta()
431+ self.mock_start_system()
432+ self.mocker.replay()
433+
434+ provider = MachineProvider("tetrascape", CONFIG)
435+ d = provider.bootstrap()
436+ def verify_machines(machines):
437+ (machine,) = machines
438+ self.assertTrue(isinstance(machine, OrchestraMachine))
439+ self.assertEquals(machine.instance_id, "winston")
440+ d.addCallback(verify_machines)
441+ return d
442
443=== modified file 'ensemble/providers/orchestra/tests/test_provider.py'
444--- ensemble/providers/cobbler/tests/test_provider.py 2011-07-20 07:10:30 +0000
445+++ ensemble/providers/orchestra/tests/test_provider.py 2011-08-01 20:04:31 +0000
446@@ -1,10 +1,19 @@
447 from ensemble.lib.testing import TestCase
448-from ensemble.providers.cobbler import MachineProvider
449+from ensemble.providers.orchestra import MachineProvider
450+
451+CONFIG = {"orchestra-server": "somewhe.re",
452+ "orchestra-user": "henricus",
453+ "orchestra-pass": "barbatus",
454+ "acquired-mgmt-class": "acquired",
455+ "available-mgmt-class": "available"}
456
457
458 class ProviderTestCase(TestCase):
459
460 def test_create_provider(self):
461- provider = MachineProvider("tetrascape", {"cheese": "edam"})
462+ provider = MachineProvider(
463+ "tetrascape", CONFIG)
464 self.assertEquals(provider.environment_name, "tetrascape")
465- self.assertEquals(provider.config, {"cheese": "edam"})
466+ self.assertEquals(provider.config, CONFIG)
467+
468+

Subscribers

People subscribed via source and target branches

to status/vote changes: