Merge lp:~fwereade/pyjuju/webdav-storage into lp:pyjuju

Proposed by William Reade
Status: Merged
Approved by: Gustavo Niemeyer
Approved revision: 297
Merged at revision: 294
Proposed branch: lp:~fwereade/pyjuju/webdav-storage
Merge into: lp:pyjuju
Prerequisite: lp:~fwereade/pyjuju/cobbler-instance-ids
Diff against target: 353 lines (+215/-24)
8 files modified
ensemble/environment/config.py (+3/-1)
ensemble/environment/tests/test_config.py (+2/-1)
ensemble/providers/orchestra/__init__.py (+15/-20)
ensemble/providers/orchestra/files.py (+35/-0)
ensemble/providers/orchestra/tests/test_bootstrap.py (+10/-0)
ensemble/providers/orchestra/tests/test_files.py (+101/-0)
ensemble/providers/orchestra/tests/test_provider.py (+0/-2)
ensemble/providers/orchestra/tests/test_state.py (+49/-0)
To merge this branch: bzr merge lp:~fwereade/pyjuju/webdav-storage
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Review via email: mp+69453@code.launchpad.net

Description of the change

WebDAV FileStorage class now exists, and is hooked up to the orchestra MachineProvider; as a bonus, so are save_state and load_state, because they're trivial now.

NOTE: also depends on lp:~fwereade/ensemble/generic-state-ops

To post a comment you must log in.
lp:~fwereade/pyjuju/webdav-storage updated
291. By William Reade

merge from lp:~fwereade/ensemble/generic-state-ops

292. By William Reade

merge and fix from lp:~fwereade/ensemble/cobbler-instance-ids

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

Very nice code as well, thanks. +1, with one constraint:

[1]

The new entry points in orchestra/__init__.py need some quick
test coverage to ensure they basically work at least.

review: Approve
lp:~fwereade/pyjuju/webdav-storage updated
293. By William Reade

storage-url is no longer mandatory in config; if not present, assume %(orchestra-server)s/webdav

294. By William Reade

merge lp:~fwereade/ensemble/generic-state-ops

295. By William Reade

merge lp:~fwereade/ensemble/cobbler-instance-ids

296. By William Reade

per review, direct verification tests for the happy path of orchestra.MachineProvider's .save_state and .load_state

297. By William Reade

merge trunk

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

William, if you are reading this without a second review, please go ahead and merge it.

The branch looks good, and has been waiting for the second review for long enough.

review: Approve

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 23:32:19 +0000
3+++ ensemble/environment/config.py 2011-08-03 06:46:34 +0000
4@@ -50,7 +50,9 @@
5 "orchestra-pass": String(),
6 "admin-secret": String(),
7 "acquired-mgmt-class": String(),
8- "available-mgmt-class": String()}),
9+ "available-mgmt-class": String(),
10+ "storage-url": String()},
11+ optional=["storage-url"]),
12 "dummy": KeyDict({})}))},
13 optional=["default"])
14
15
16=== modified file 'ensemble/environment/tests/test_config.py'
17--- ensemble/environment/tests/test_config.py 2011-07-21 08:15:51 +0000
18+++ ensemble/environment/tests/test_config.py 2011-08-03 06:46:34 +0000
19@@ -36,6 +36,7 @@
20 admin-secret: garden
21 acquired-mgmt-class: acquired
22 available-mgmt-class: available
23+ storage-url: http://somewhereel.se
24 """
25
26 class EnvironmentsConfigTestBase(TestCase):
27@@ -554,7 +555,7 @@
28 def test_orchestra_schema_requires(self):
29 requires = (
30 "type orchestra-server orchestra-user orchestra-pass "
31- "admin-secret acquired-mgmt-class available-mgmt-class").split()
32+ "admin-secret acquired-mgmt-class available-mgmt-class ").split()
33 for require in requires:
34 config = yaml.load(SAMPLE_ORCHESTRA)
35 del config["environments"]["sample"][require]
36
37=== modified file 'ensemble/providers/orchestra/__init__.py'
38--- ensemble/providers/orchestra/__init__.py 2011-07-20 07:49:20 +0000
39+++ ensemble/providers/orchestra/__init__.py 2011-08-03 06:46:34 +0000
40@@ -1,9 +1,11 @@
41-from twisted.internet.defer import fail, succeed
42+from twisted.internet.defer import fail
43
44 from ensemble.errors import EnvironmentNotFound
45 from ensemble.providers.common.bootstrap import Bootstrap
46+from ensemble.providers.common.state import SaveState, LoadState
47
48 from .cobbler import CobblerConnect
49+from .files import FileStorage
50 from .launch import OrchestraLaunchMachine
51
52
53@@ -70,23 +72,27 @@
54 raise NotImplementedError()
55
56 def save_state(self, state):
57- """
58- Save state to the provider.
59+ """Save state to the provider.
60
61 @param state
62 @type dict
63 """
64- return succeed(None)
65+ serializer = SaveState(self)
66+ return serializer.run(state)
67
68 def load_state(self):
69- """
70- Load state from the provider, returns a dictionary.
71- """
72- raise NotImplementedError()
73+ """Load state from the provider.
74+
75+ @return: a dictionary.
76+ """
77+ deserializer = LoadState(self)
78+ return deserializer.run()
79
80 def get_file_storage(self):
81 """Retrieve the provider C{FileStorage} abstraction."""
82- raise NotImplementedError()
83+ if "storage-url" not in self.config:
84+ return FileStorage("http://%(orchestra-server)s/webdav" % self.config)
85+ return FileStorage(self.config["storage-url"])
86
87 def get_serialization_data(self):
88 """Return a dictionary serialization of the provider configuration.
89@@ -124,14 +130,3 @@
90 @raise: EnvironmentNotFound or EnvironmentPending
91 """
92 return fail(EnvironmentNotFound())
93-
94-
95-class FileStorage(object):
96-
97- def get(self, name):
98- """Returns open file or fails with FileNotFound"""
99- raise NotImplementedError()
100-
101- def put(self, remote_path, local_path):
102- """Copies contents of local_path to remote_path"""
103- raise NotImplementedError()
104
105=== added file 'ensemble/providers/orchestra/files.py'
106--- ensemble/providers/orchestra/files.py 1970-01-01 00:00:00 +0000
107+++ ensemble/providers/orchestra/files.py 2011-08-03 06:46:34 +0000
108@@ -0,0 +1,35 @@
109+from cStringIO import StringIO
110+
111+from twisted.web.client import getPage
112+from twisted.web.error import Error
113+
114+from ensemble.errors import FileNotFound
115+
116+
117+class FileStorage(object):
118+
119+ def __init__(self, base_url):
120+ self._base_url = base_url
121+
122+ def _key_to_url(self, name):
123+ return "%s/%s" % (self._base_url, name)
124+
125+ def get(self, name):
126+ url = self._key_to_url(name)
127+ d = getPage(url)
128+ d.addCallback(StringIO)
129+
130+ def convert_404(failure):
131+ failure.trap(Error)
132+ if failure.value.status == "404":
133+ raise FileNotFound(url)
134+ return failure
135+ d.addErrback(convert_404)
136+ return d
137+
138+ def put(self, remote_path, file_object):
139+ url = self._key_to_url(remote_path)
140+ data = file_object.read()
141+ d = getPage(url, method="PUT", postdata=data)
142+ d.addCallback(lambda _: True)
143+ return d
144
145=== modified file 'ensemble/providers/orchestra/tests/test_bootstrap.py'
146--- ensemble/providers/orchestra/tests/test_bootstrap.py 2011-08-02 08:07:12 +0000
147+++ ensemble/providers/orchestra/tests/test_bootstrap.py 2011-08-03 06:46:34 +0000
148@@ -1,4 +1,5 @@
149 from xmlrpclib import Fault
150+from yaml import dump
151
152 from twisted.internet.defer import fail, succeed
153 from twisted.web.xmlrpc import Proxy
154@@ -14,6 +15,8 @@
155 "orchestra-pass": "pass",
156 "acquired-mgmt-class": "acquired",
157 "available-mgmt-class": "available",
158+ "admin-secret": "SEEKRIT",
159+ "storage-url": "http://somewhe.re/webdav",
160 "admin-secret": "SEEKRIT"}
161
162
163@@ -92,6 +95,12 @@
164 "TOKEN")
165 self.mocker.result(succeed("[some-timestamp]_power"))
166
167+ def mock_save_state(self):
168+ getPage = self.mocker.replace("twisted.web.client.getPage")
169+ getPage("http://somewhe.re/webdav/provider-state",
170+ method="PUT", postdata=dump({"zookeeper-instances": ["winston-uid"]}))
171+ self.mocker.result(succeed(None))
172+
173 def test_no_machines_available(self):
174 self.mock_proxy()
175 self.mock_get_systems(acceptable=False)
176@@ -137,6 +146,7 @@
177 self.mock_acquire_system()
178 self.mock_set_ks_meta()
179 self.mock_start_system()
180+ self.mock_save_state()
181 self.mocker.replay()
182
183 provider = MachineProvider("tetrascape", CONFIG)
184
185=== added file 'ensemble/providers/orchestra/tests/test_files.py'
186--- ensemble/providers/orchestra/tests/test_files.py 1970-01-01 00:00:00 +0000
187+++ ensemble/providers/orchestra/tests/test_files.py 2011-08-03 06:46:34 +0000
188@@ -0,0 +1,101 @@
189+from cStringIO import StringIO
190+
191+from twisted.internet.defer import fail, succeed
192+from twisted.web.error import Error
193+
194+from ensemble.errors import FileNotFound
195+from ensemble.lib.testing import TestCase
196+from ensemble.providers.orchestra import MachineProvider
197+
198+
199+def get_file_storage(with_storage_url=True):
200+ config = {"storage-url": "http://somewhe.re/webdav",
201+ "orchestra-server": "somewhereel.se",
202+ "orchestra-user": "user",
203+ "orchestra-pass": "pass",
204+ "acquired-mgmt-class": "acquired",
205+ "available-mgmt-class": "available"}
206+ if not with_storage_url:
207+ del config["storage-url"]
208+ provider = MachineProvider("blah", config)
209+ return provider.get_file_storage()
210+
211+
212+class FileStorageTest(TestCase):
213+
214+ def test_get_works_no_storage_url(self):
215+ getPage = self.mocker.replace("twisted.web.client.getPage")
216+ getPage("http://somewhereel.se/webdav/rubber/chicken")
217+ self.mocker.result(succeed("pulley"))
218+ self.mocker.replay()
219+
220+ fs = get_file_storage(with_storage_url=False)
221+ d = fs.get("rubber/chicken")
222+
223+ def verify(result):
224+ self.assertEquals(result.read(), "pulley")
225+ d.addCallback(verify)
226+ return d
227+
228+ def test_get_works(self):
229+ getPage = self.mocker.replace("twisted.web.client.getPage")
230+ getPage("http://somewhe.re/webdav/rubber/chicken")
231+ self.mocker.result(succeed("pulley"))
232+ self.mocker.replay()
233+
234+ fs = get_file_storage()
235+ d = fs.get("rubber/chicken")
236+
237+ def verify(result):
238+ self.assertEquals(result.read(), "pulley")
239+ d.addCallback(verify)
240+ return d
241+
242+ def test_get_fails(self):
243+ getPage = self.mocker.replace("twisted.web.client.getPage")
244+ getPage("http://somewhe.re/webdav/rubber/chicken")
245+ self.mocker.result(fail(Error("404")))
246+ self.mocker.replay()
247+
248+ fs = get_file_storage()
249+ d = fs.get("rubber/chicken")
250+ self.assertFailure(d, FileNotFound)
251+ return d
252+
253+ def test_get_errors(self):
254+ getPage = self.mocker.replace("twisted.web.client.getPage")
255+ getPage("http://somewhe.re/webdav/rubber/chicken")
256+ self.mocker.result(fail(Error("500")))
257+ self.mocker.replay()
258+
259+ fs = get_file_storage()
260+ d = fs.get("rubber/chicken")
261+ self.assertFailure(d, Error)
262+ return d
263+
264+ def test_put_works(self):
265+ getPage = self.mocker.replace("twisted.web.client.getPage")
266+ getPage("http://somewhe.re/webdav/rubber/chicken",
267+ method="PUT", postdata="pulley")
268+ self.mocker.result(succeed(None))
269+ self.mocker.replay()
270+
271+ fs = get_file_storage()
272+ d = fs.put("rubber/chicken", StringIO("pulley"))
273+
274+ def verify(result):
275+ self.assertEquals(result, True)
276+ d.addCallback(verify)
277+ return d
278+
279+ def test_put_errors(self):
280+ getPage = self.mocker.replace("twisted.web.client.getPage")
281+ getPage("http://somewhe.re/webdav/rubber/chicken",
282+ method="PUT", postdata="pulley")
283+ self.mocker.result(fail(Error("500")))
284+ self.mocker.replay()
285+
286+ fs = get_file_storage()
287+ d = fs.put("rubber/chicken", StringIO("pulley"))
288+ self.assertFailure(d, Error)
289+ return d
290
291=== modified file 'ensemble/providers/orchestra/tests/test_provider.py'
292--- ensemble/providers/orchestra/tests/test_provider.py 2011-07-30 11:50:10 +0000
293+++ ensemble/providers/orchestra/tests/test_provider.py 2011-08-03 06:46:34 +0000
294@@ -15,5 +15,3 @@
295 "tetrascape", CONFIG)
296 self.assertEquals(provider.environment_name, "tetrascape")
297 self.assertEquals(provider.config, CONFIG)
298-
299-
300
301=== added file 'ensemble/providers/orchestra/tests/test_state.py'
302--- ensemble/providers/orchestra/tests/test_state.py 1970-01-01 00:00:00 +0000
303+++ ensemble/providers/orchestra/tests/test_state.py 2011-08-03 06:46:34 +0000
304@@ -0,0 +1,49 @@
305+from yaml import dump
306+
307+from twisted.internet.defer import succeed
308+
309+from ensemble.lib.testing import TestCase
310+from ensemble.providers.orchestra import MachineProvider
311+
312+
313+def get_provider():
314+ config = {"orchestra-server": "somewhe.re",
315+ "orchestra-user": "user",
316+ "orchestra-pass": "pass",
317+ "acquired-mgmt-class": "acquired",
318+ "available-mgmt-class": "available"}
319+ return MachineProvider("tetrascape", config)
320+
321+
322+class StateTest(TestCase):
323+
324+ def test_save(self):
325+ state = {"foo": "blah blah"}
326+ getPage = self.mocker.replace("twisted.web.client.getPage")
327+ getPage("http://somewhe.re/webdav/provider-state",
328+ method="PUT", postdata=dump(state))
329+ self.mocker.result(succeed(None))
330+ self.mocker.replay()
331+
332+ provider = get_provider()
333+ d = provider.save_state(state)
334+
335+ def verify(result):
336+ self.assertEquals(result, True)
337+ d.addCallback(verify)
338+ return d
339+
340+ def test_load(self):
341+ expect_state = {"foo": "blah blah"}
342+ getPage = self.mocker.replace("twisted.web.client.getPage")
343+ getPage("http://somewhe.re/webdav/provider-state")
344+ self.mocker.result(succeed(dump(expect_state)))
345+ self.mocker.replay()
346+
347+ provider = get_provider()
348+ d = provider.load_state()
349+
350+ def verify(state):
351+ self.assertEquals(state, expect_state)
352+ d.addCallback(verify)
353+ return d

Subscribers

People subscribed via source and target branches

to status/vote changes: