Merge lp:~fwereade/pyjuju/cobbler-connect-production into lp:pyjuju
- cobbler-connect-production
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kapil Thangavelu (community) | Approve | ||
Gustavo Niemeyer | Approve | ||
Review via email: mp+68706@code.launchpad.net |
Commit message
Description of the change
* added schema for orchestra provider
* added CobblerConnect for communicating with cobbler
* added OrchestraLaunch
- 315. By William Reade
-
merge parent
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!
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-
- 316. By William Reade
-
whitespace
- 317. By William Reade
-
merge from trunk; also changes per niemeyer's review
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.
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
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/
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:/
[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.
Kapil Thangavelu (hazmat) : | # |
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:/
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
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 | + |
Looks nice. Just a few minors, and +1.
[1]
+ @property class(self) : "acquired- mgmt-class" ] class(self) : "available- mgmt-class" ] config[ "orchestra- user"], "orchestra- pass"])
+ def _acquired_
+ return self._config[
+
+ @property
+ def _available_
+ return self._config[
+
+ @property
+ def _credentials(self):
+ return (self._
+ self._config[
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" ] available_ class = self._config[ "available- mgmt-class" ] "orchestra- user"] "orchestra- pass"]
self._
self._user = self._config[
self._pass = self._config[
[2]
+ if "invalid token" not in failure. getErrorMessage (): ck(lambda unused: call())
+ return failure
+ login = self._login()
+ login.addCallba
Looks nice.
[3]
+ failure.trap(Fault) getErrorMessage ():
+ if "invalid token" not in failure.
+ 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() get_systems( ) acquire_ system( ) set_ks_ meta() start_system( )
+ self.mock_
+ self.mock_
+ self.mock_
+ self.mock_
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.