Merge lp:~hazmat/pyjuju/lib-files into lp:pyjuju

Proposed by Kapil Thangavelu
Status: Merged
Approved by: Gustavo Niemeyer
Approved revision: 343
Merged at revision: 347
Proposed branch: lp:~hazmat/pyjuju/lib-files
Merge into: lp:pyjuju
Prerequisite: lp:~hazmat/pyjuju/lib-zookeeper
Diff against target: 722 lines (+247/-187)
8 files modified
ensemble/formula/tests/test_publisher.py (+4/-3)
ensemble/lib/lxc/__init__.py (+85/-54)
ensemble/lib/lxc/data/ensemble-create (+11/-7)
ensemble/lib/lxc/tests/test_lxc.py (+37/-15)
ensemble/providers/common/files.py (+40/-0)
ensemble/providers/common/tests/test_files.py (+62/-0)
ensemble/providers/dummy.py (+3/-38)
ensemble/providers/tests/test_dummy.py (+5/-70)
To merge this branch: bzr merge lp:~hazmat/pyjuju/lib-files
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
William Reade (community) Needs Fixing
Review via email: mp+73444@code.launchpad.net

Description of the change

Both the dummy and local dev provider would like to utilise a local disk backed provider filestorage. The implementation currently in the dummy provider should be moved into provider.common for easier reuse by both.

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

[0]

get_url is missing, and is needed for download_formula.

I'm not sure whether it should return a file:// url (which download_formula does in fact undertand) because I'm not sure whether we intend local dev machines to have access to the whole filesystem; I'd suggest that they probably shouldn't, really... and so, if that's the case, we'll presumably need to run some sort of file server of our own. Thoughts?

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

Excerpts from William Reade's message of Thu Sep 01 08:58:22 UTC 2011:
> Review: Needs Fixing
> [0]
>
> get_url is missing, and is needed for download_formula.
>
> I'm not sure whether it should return a file:// url (which download_formula does in fact undertand) because I'm not sure whether we intend local dev machines to have access to the whole filesystem; I'd suggest that they probably shouldn't, really... and so, if that's the case, we'll presumably need to run some sort of file server of our own. Thoughts?

Thanks for having a look at this.

[0]
Addressed the get_url missing and some other test assumptions.

The machine agent will be running in the host creating lxc containers, and needs
to do so as root. The formulas are specifically downloaded into /formulas on the
container rootfs by the machine agent. ie. using local disk is a very good fit
imo, its the simplest thing that works, and has low overhead. Additional
commonality between separate provider implementations, shouldn't come at the
expense of that.

cheers,

kapil

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

[1]

I'm missing some context about how that's to be used. The LXC container doesn't have access
to a common location in the outside disk, so how does a local disk formula would work in
that case?

Depending on your thinking around this, an alternative could be the Orchestra file storage
provider, that uses webdav. We could look for a trivial webdav provider to set up
automatically in the local machine.

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

Excerpts from Gustavo Niemeyer's message of Sat Sep 03 14:14:22 UTC 2011:
> Review: Needs Information
> [1]
>
> I'm missing some context about how that's to be used. The LXC container doesn't have access
> to a common location in the outside disk, so how does a local disk formula would work in
> that case?
>
> Depending on your thinking around this, an alternative could be the Orchestra file storage
> provider, that uses webdav. We could look for a trivial webdav provider to set up
> automatically in the local machine.

The machine agent which puts the formula into place on the unit, lives outside
the container, and has direct fs access to the provider storage directory and
container rootfs.

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

Sounds good, thanks for the info.

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

Putting it back in review for William to see if the changes look good.

lp:~hazmat/pyjuju/lib-files updated
344. By Kapil Thangavelu

Merged lib-zookeeper into lib-files.

345. By Kapil Thangavelu

Merged lib-zookeeper into lib-files.

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

Please go ahead and merge this Kapil.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ensemble/formula/tests/test_publisher.py'
2--- ensemble/formula/tests/test_publisher.py 2011-08-17 14:56:42 +0000
3+++ ensemble/formula/tests/test_publisher.py 2011-09-13 23:16:26 +0000
4@@ -54,7 +54,8 @@
5 self.formula_key, self.formula.get_sha256())
6
7 self.client = ZookeeperClient(get_test_zookeeper_address())
8- self.storage = FileStorage(self.makeDir())
9+ self.storage_dir = self.makeDir()
10+ self.storage = FileStorage(self.storage_dir)
11 self.publisher = FormulaPublisher(self.client, self.storage)
12
13 yield self.client.connect()
14@@ -79,7 +80,7 @@
15
16 self.assertEqual(
17 result[0].bundle_url, "file://%s/formulas/%s" % (
18- self.storage.path, self.formula_storage_key))
19+ self.storage_dir, self.formula_storage_key))
20
21 @inlineCallbacks
22 def test_published_formula_sans_unicode(self):
23@@ -142,7 +143,7 @@
24 yaml.load(content)["sha256"],
25 self.formula_storage_key.split(":")[-1])
26
27- stored_files = os.listdir(os.path.join(self.storage.path, "formulas"))
28+ stored_files = os.listdir(os.path.join(self.storage_dir, "formulas"))
29 self.assertIn(self.formula_storage_key, stored_files)
30
31 # and the binary bits where stored
32
33=== modified file 'ensemble/lib/lxc/__init__.py'
34--- ensemble/lib/lxc/__init__.py 2011-09-13 23:16:26 +0000
35+++ ensemble/lib/lxc/__init__.py 2011-09-13 23:16:26 +0000
36@@ -1,37 +1,38 @@
37 import os
38 import pipes
39-import shutil
40 import subprocess
41 import sys
42 import tempfile
43
44-from twisted.internet.defer import inlineCallbacks
45+from twisted.internet.defer import inlineCallbacks, returnValue
46 from twisted.internet.threads import deferToThread
47
48 from ensemble.errors import EnsembleError
49
50-BASE_PATH = os.path.normpath(
51- os.path.abspath(
52- os.path.join(os.path.dirname(__file__), "data")))
53+DATA_PATH = os.path.abspath(
54+ os.path.join(os.path.dirname(__file__), "data"))
55
56
57 class LXCError(EnsembleError):
58 """Indicates a low level error with an LXC container"""
59- pass
60
61
62 def _cmd(args):
63- p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
64- env=dict(os.environ))
65- r = p.wait()
66+ p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
67+ stdout_data, _ = p.communicate()
68+ r = p.returncode
69 if r != 0:
70 # read the stdout/err streams and show the user
71- print >>sys.stderr, p.stderr.read()
72- raise LXCError(p.stdout.read())
73- return r
74-
75-
76-def _lxc_create(container_name, template="ubuntu", config_file=None, release="oneiric"):
77+ print >>sys.stderr, stdout_data
78+ raise LXCError(stdout_data)
79+ return (r, stdout_data)
80+
81+
82+# Wrapped lxc cli primitives
83+def _lxc_create(
84+ container_name, template="ubuntu", config_file=None, release="oneiric"):
85+ # the -- argument indicates the last parameters are passed
86+ # to the template and not handled by lxc-create
87 args = ["sudo", "lxc-create",
88 "-n", container_name,
89 "-t", template,
90@@ -50,9 +51,7 @@
91
92
93 def _lxc_stop(container_name):
94- subprocess.Popen(["sudo", "lxc-stop", "-n",
95- container_name],
96- stderr=subprocess.PIPE).wait()
97+ _cmd(["sudo", "lxc-stop", "-n", container_name])
98
99
100 def _lxc_destroy(container_name):
101@@ -60,8 +59,7 @@
102
103
104 def _lxc_ls():
105- output = subprocess.Popen(["lxc-ls"],
106- stdout=subprocess.PIPE).communicate()[0]
107+ _, output = _cmd(["lxc-ls"])
108 output = output.replace("\n", " ")
109 return set([c for c in output.split(" ") if c])
110
111@@ -70,7 +68,7 @@
112 """Wait for container to be in a given state RUNNING|STOPPED."""
113
114 def wait(container_name):
115- rc = _cmd(["sudo", "lxc-wait",
116+ rc, _ = _cmd(["sudo", "lxc-wait",
117 "-n", container_name,
118 "-s", state])
119 return rc == 0
120@@ -83,27 +81,52 @@
121 raise LXCError("Expect container root directory: %s" %
122 container_root)
123 # write the container scripts into the container
124- base_script = os.path.basename(customize_script)
125- in_path = os.path.join(container_root, "tmp", base_script)
126-
127- shutil.copy(customize_script, in_path)
128- os.chmod(in_path, 0770)
129-
130- args = ["sudo", "chroot",
131- container_root,
132- os.path.join("/tmp", base_script)]
133+ fd, in_path = tempfile.mkstemp(prefix=os.path.basename(customize_script),
134+ dir=os.path.join(container_root, "tmp"))
135+
136+ os.write(fd, open(customize_script, "r").read())
137+ os.close(fd)
138+ os.chmod(in_path, 0755)
139+
140+ args = ["sudo", "chroot", container_root,
141+ os.path.join("/tmp", os.path.basename(in_path))]
142 return _cmd(args)
143
144
145 def validate_path(pathname):
146 if not os.access(pathname, os.R_OK):
147- raise LXCError("Invalid file %s" % pathname)
148+ raise LXCError("Invalid or unreadable file: %s" % pathname)
149+
150+
151+@inlineCallbacks
152+def get_containers(prefix):
153+ """Return a dictionary of containers key names to runtime boolean value.
154+
155+ :param prefix: Optionally specify a prefix that the container should
156+ match any returned containers.
157+ """
158+ _, output = yield deferToThread(_cmd, ["lxc-ls"])
159+
160+ containers = {}
161+ for i in filter(None, output.split("\n")):
162+ if i in containers:
163+ containers[i] = True
164+ else:
165+ containers[i] = False
166+
167+ if prefix:
168+ remove = [k for k in containers.keys() if not
169+ k.startswith(prefix)]
170+ map(containers.pop, remove)
171+
172+ returnValue(containers)
173
174
175 class LXCContainer(object):
176- def __init__(self, container_name, configuration=None, network_name="virbr0",
177+ def __init__(self,
178+ container_name, configuration=None, network_name="virbr0",
179 customize_script=None):
180- """Create an LXC container
181+ """Create an LXCContainer
182
183 :param container_name: should be unique within the system
184
185@@ -118,17 +141,15 @@
186 """
187
188 self.container_name = container_name
189- # open the template file and create a new temp processed
190- # version
191- self.lxc_config = self._make_lxc_config(network_name)
192
193 if customize_script is None:
194- customize_script = os.path.join(BASE_PATH,
195+ customize_script = os.path.join(DATA_PATH,
196 "ensemble-create")
197 self.customize_script = customize_script
198 validate_path(self.customize_script)
199
200 self.configuration = configuration
201+ self.network_name = network_name
202 self.running = False
203
204 @property
205@@ -141,18 +162,19 @@
206 return os.path.join(self.rootfs, path)
207
208 def _make_lxc_config(self, network_name):
209- lxc_config = os.path.join(BASE_PATH, "lxc.conf")
210- template = open(lxc_config, "r").read()
211-
212- fd, output_fn = tempfile.mkstemp(suffix=".conf")
213- output_config = open(output_fn, "w")
214- output_config.write(template % {"network_name": network_name})
215- output_config.close()
216-
217- validate_path(output_fn)
218- return output_fn
219-
220- def _injectConfiguration(self):
221+ lxc_config = os.path.join(DATA_PATH, "lxc.conf")
222+ with open(lxc_config, "r") as fh:
223+ template = fh.read()
224+ fd, output_fn = tempfile.mkstemp(suffix=".conf")
225+ output_config = open(output_fn, "w")
226+ output_config.write(template % {"network_name": network_name})
227+ output_config.close()
228+
229+ validate_path(output_fn)
230+ return output_fn
231+
232+ @inlineCallbacks
233+ def _customize_container(self):
234 config_dir = self._p("etc/ensemble")
235 if not os.path.exists(config_dir):
236 _cmd(["sudo", "mkdir", config_dir])
237@@ -168,16 +190,26 @@
238 _cmd(["sudo", "mv", fn,
239 self._p("etc/ensemble/ensemble.conf")])
240
241+ yield deferToThread(
242+ _customize_container, self.customize_script, self.rootfs)
243+
244 @inlineCallbacks
245- def _customize_container(self):
246- self._injectConfiguration()
247- yield deferToThread(_customize_container, self.customize_script, self.rootfs)
248+ def execute(self, args):
249+ if not isinstance(args, (list, tuple)):
250+ args = [args, ]
251+
252+ args = ["sudo", "chroot", self.rootfs] + args
253+ yield deferToThread(_cmd, args)
254
255 @inlineCallbacks
256 def create(self):
257+ # open the template file and create a new temp processed
258+ # version
259+ lxc_config = self._make_lxc_config(self.network_name)
260 yield deferToThread(_lxc_create, self.container_name,
261- config_file=self.lxc_config)
262+ config_file=lxc_config)
263 yield self._customize_container()
264+ os.unlink(lxc_config)
265
266 @inlineCallbacks
267 def run(self):
268@@ -197,4 +229,3 @@
269 def destroy(self):
270 yield self.stop()
271 yield deferToThread(_lxc_destroy, self.container_name)
272- os.unlink(self.lxc_config)
273
274=== modified file 'ensemble/lib/lxc/data/ensemble-create'
275--- ensemble/lib/lxc/data/ensemble-create 2011-09-13 23:16:26 +0000
276+++ ensemble/lib/lxc/data/ensemble-create 2011-09-13 23:16:26 +0000
277@@ -13,21 +13,25 @@
278
279 if [ $ENSEMBLE_ORIGIN = "ppa" ]; then
280 echo "Using Ensemble PPA for container"
281- ENSEMBLE_SOURCE="ppa:ensemble/ppa"
282- elif [ $ENSEMBLE_ORIGIN = "lp" ]; then
283- echo "Using Ensemble Branch $ensemble_source"
284+ elif [ $ENSEMBLE_ORIGIN = "branch" ]; then
285+ echo "Using Ensemble Branch $ENSEMBLE_SOURCE"
286 elif [ $ENSEMBLE_ORIGIN = "distro" ]; then
287 echo "Using Ensemble distribution packages"
288+ else
289+ echo "Unknown Ensemble origin policy $ENSEMBLE_ORIGIN: expected [ppa|branch|disto]"
290+ exit 1
291 fi
292
293 echo "Setting up ensemble in container"
294 apt-get install --force-yes -y bzr tmux
295
296 if [ $ENSEMBLE_ORIGIN = "ppa" ]; then
297- apt-add-repository $ENSEMBLE_SOURCE
298+ # the echo forces an enter to get around the interactive
299+ # prompt in apt-add-repository
300+ echo y | apt-add-repository ppa:ensemble/ppa
301 apt-get update
302 apt-get install --force-yes -y ensemble python-txzookeeper
303- elif [ $ENSEMBLE_ORIGIN = "lp" ]; then
304+ elif [ $ENSEMBLE_ORIGIN = "branch" ]; then
305 apt-get install --force-yes -y python-txzookeeper
306 mkdir /usr/lib/ensemble
307 bzr branch $ENSEMBLE_SOURCE /usr/lib/ensemble/ensemble
308@@ -45,8 +49,8 @@
309 ENSEMBLE_ORIGIN=distro
310 fi
311
312-if [ -z "$ZOOKEEPER_ADDRESS" ]; then
313- echo "'ZOOKEEPER_ADDRESS' is required"
314+if [ -z "$ENSEMBLE_ZOOKEEPER" ]; then
315+ echo "'ENSEMBLE_ZOOKEEPER' is required"
316 exit 1
317 fi
318
319
320=== modified file 'ensemble/lib/lxc/tests/test_lxc.py'
321--- ensemble/lib/lxc/tests/test_lxc.py 2011-09-13 23:16:26 +0000
322+++ ensemble/lib/lxc/tests/test_lxc.py 2011-09-13 23:16:26 +0000
323@@ -1,11 +1,12 @@
324 import os
325+import tempfile
326
327 from twisted.internet.defer import inlineCallbacks
328 from twisted.internet.threads import deferToThread
329
330 from ensemble.lib.lxc import (_lxc_start, _lxc_stop, _lxc_create,
331 _lxc_wait, _lxc_ls, _lxc_destroy,
332- LXCContainer)
333+ LXCContainer, get_containers)
334 from ensemble.lib.testing import TestCase, get_test_zookeeper_address
335
336
337@@ -15,9 +16,8 @@
338 return "TEST_LXC=1 to include lxc tests"
339
340
341-BASE_PATH = os.path.normpath(
342- os.path.abspath(
343- os.path.join(os.path.dirname(__file__), "..", "data")))
344+DATA_PATH = os.path.abspath(
345+ os.path.join(os.path.dirname(__file__), "..", "data"))
346
347
348 DEFAULT_CONTAINER = "lxc_test"
349@@ -27,6 +27,25 @@
350 timeout = 240
351 skip = run_lxc_tests()
352
353+ def setUp(self):
354+ self.config = self.make_config()
355+
356+ @self.addCleanup
357+ def remove_config():
358+ if os.path.exists(self.config):
359+ os.unlink(self.config)
360+
361+ def make_config(self, network_name="virbr0"):
362+ lxc_config = os.path.join(DATA_PATH, "lxc.conf")
363+ template = open(lxc_config, "r").read()
364+
365+ fd, output_fn = tempfile.mkstemp(suffix=".conf")
366+ output_config = open(output_fn, "w")
367+ output_config.write(template % {"network_name": network_name})
368+ output_config.close()
369+
370+ return output_fn
371+
372 def cleanContainer(self, container_name):
373 if os.path.exists("/var/lib/lxc/%s" % container_name):
374 _lxc_stop(container_name)
375@@ -35,8 +54,7 @@
376 def test_lxc_create(self):
377 self.addCleanup(self.cleanContainer, DEFAULT_CONTAINER)
378
379- _lxc_create(DEFAULT_CONTAINER,
380- config_file=os.path.join(BASE_PATH, "lxc.conf"))
381+ _lxc_create(DEFAULT_CONTAINER, config_file=self.config)
382
383 # verify we can find the container
384 output = _lxc_ls()
385@@ -50,8 +68,7 @@
386 def test_lxc_start(self):
387 self.addCleanup(self.cleanContainer, DEFAULT_CONTAINER)
388
389- _lxc_create(DEFAULT_CONTAINER,
390- config_file=os.path.join(BASE_PATH, "lxc.conf"))
391+ _lxc_create(DEFAULT_CONTAINER, config_file=self.config)
392
393 _lxc_start(DEFAULT_CONTAINER)
394 _lxc_stop(DEFAULT_CONTAINER)
395@@ -59,8 +76,7 @@
396 @inlineCallbacks
397 def test_lxc_deferred(self):
398 self.addCleanup(self.cleanContainer, DEFAULT_CONTAINER)
399- yield deferToThread(_lxc_create, DEFAULT_CONTAINER,
400- config_file=os.path.join(BASE_PATH, "lxc.conf"))
401+ yield deferToThread(_lxc_create, DEFAULT_CONTAINER, config_file=self.config)
402 yield deferToThread(_lxc_start, DEFAULT_CONTAINER)
403
404 @inlineCallbacks
405@@ -68,7 +84,7 @@
406 self.addCleanup(self.cleanContainer, DEFAULT_CONTAINER)
407
408 c = LXCContainer(DEFAULT_CONTAINER,
409- dict(zookeeper_address=get_test_zookeeper_address(),
410+ dict(ensemble_zookeeper=get_test_zookeeper_address(),
411 ensemble_origin="distro"))
412 self.assertFalse(c.running)
413 yield c.run()
414@@ -85,7 +101,7 @@
415 #network = open(os.path.join(c.rootfs, "..", "config"), "r").read()
416 #self.assertIn("lxc.network.link=virbr0", network)
417
418- # check that some of the customise modifications were made
419+ # check that some of the customize modifications were made
420 self.assertTrue(os.path.exists(os.path.join(
421 c.rootfs, "etc", "ensemble")))
422 self.assertTrue(os.path.exists(os.path.join(
423@@ -93,12 +109,19 @@
424
425 # verify that we have shell variables in rootfs/etc/ensemble.conf
426 config = open(os.path.join(c.rootfs, "etc", "ensemble", "ensemble.conf"), "r").read()
427- self.assertIn("ZOOKEEPER_ADDRESS=%s" % get_test_zookeeper_address(), config)
428+ self.assertIn("ENSEMBLE_ZOOKEEPER=%s" % get_test_zookeeper_address(), config)
429+
430+ # verify that we are in containers
431+ containers = yield get_containers(None)
432+ self.assertEqual(containers[DEFAULT_CONTAINER], True)
433
434 # tear it down
435 yield c.destroy()
436 self.assertFalse(c.running)
437
438+ containers = yield get_containers(None)
439+ self.assertNotIn(DEFAULT_CONTAINER, containers)
440+
441 # and its gone
442 output = _lxc_ls()
443 self.assertNotIn(DEFAULT_CONTAINER, output)
444@@ -107,8 +130,7 @@
445 def test_lxc_wait(self):
446 self.addCleanup(self.cleanContainer, DEFAULT_CONTAINER)
447
448- _lxc_create(DEFAULT_CONTAINER,
449- config_file=os.path.join(BASE_PATH, "lxc.conf"))
450+ _lxc_create(DEFAULT_CONTAINER, config_file=self.config)
451
452 _lxc_start(DEFAULT_CONTAINER)
453
454
455=== added file 'ensemble/providers/common/files.py'
456--- ensemble/providers/common/files.py 1970-01-01 00:00:00 +0000
457+++ ensemble/providers/common/files.py 2011-09-13 23:16:26 +0000
458@@ -0,0 +1,40 @@
459+"""
460+Directory based file storage (for lxc and dummy).
461+"""
462+
463+import os
464+
465+from twisted.internet.defer import fail, succeed
466+from ensemble.errors import FileNotFound
467+
468+
469+class FileStorage(object):
470+
471+ def __init__(self, path):
472+ self._path = path
473+
474+ def get(self, name):
475+ file_path = os.path.join(
476+ self._path, *filter(None, name.split("/")))
477+ if os.path.exists(file_path):
478+ return succeed(open(file_path))
479+ return fail(FileNotFound(file_path))
480+
481+ def put(self, remote_path, file_object):
482+ store_path = os.path.join(
483+ self._path, *filter(None, remote_path.split("/")))
484+ store_path = os.path.abspath(store_path)
485+ if not store_path.startswith(self._path):
486+ return fail(AssertionError("Invalid Remote Path %s" % remote_path))
487+
488+ parent_store_path = os.path.dirname(store_path)
489+ if not os.path.exists(parent_store_path):
490+ os.makedirs(parent_store_path)
491+ with open(store_path, "wb") as f:
492+ f.write(file_object.read())
493+ return succeed(True)
494+
495+ def get_url(self, name):
496+ file_path = os.path.abspath(os.path.join(
497+ self._path, *filter(None, name.split("/"))))
498+ return "file://%s" % file_path
499
500=== added file 'ensemble/providers/common/tests/test_files.py'
501--- ensemble/providers/common/tests/test_files.py 1970-01-01 00:00:00 +0000
502+++ ensemble/providers/common/tests/test_files.py 2011-09-13 23:16:26 +0000
503@@ -0,0 +1,62 @@
504+import os
505+from StringIO import StringIO
506+
507+from twisted.internet.defer import inlineCallbacks
508+
509+from ensemble.lib.testing import TestCase
510+from ensemble.providers.common.files import FileStorage
511+
512+from ensemble.errors import FileNotFound
513+
514+
515+class FileStorageTest(TestCase):
516+
517+ def setUp(self):
518+ super(FileStorageTest, self).setUp()
519+ self.storage_dir = self.makeDir()
520+ self.storage = FileStorage(self.storage_dir)
521+
522+ def test_get_file_non_existent(self):
523+ return self.failUnlessFailure(self.storage.get("/abc"), FileNotFound)
524+
525+ def test_get_url(self):
526+ url = self.storage.get_url("/abc.txt")
527+ self.assertEqual(url, "file://%s/abc.txt" % self.storage_dir)
528+
529+ @inlineCallbacks
530+ def test_get_file(self):
531+ path = os.path.join(self.storage_dir, "abc.txt")
532+ self.makeFile("content", path=path)
533+ fh = yield self.storage.get("/abc.txt")
534+ self.assertEqual(fh.read(), "content")
535+
536+ @inlineCallbacks
537+ def test_put_and_get_file(self):
538+ file_obj = StringIO("rabbits")
539+ yield self.storage.put("/magic/beans.txt", file_obj)
540+ fh = yield self.storage.get("/magic/beans.txt")
541+ self.assertEqual(fh.read(), "rabbits")
542+
543+ @inlineCallbacks
544+ def test_put_same_path_multiple(self):
545+ file_obj = StringIO("rabbits")
546+ yield self.storage.put("/magic/beans.txt", file_obj)
547+ file_obj = StringIO("elephant")
548+ yield self.storage.put("/magic/beans.txt", file_obj)
549+ fh = yield self.storage.get("/magic/beans.txt")
550+ self.assertEqual(fh.read(), "elephant")
551+
552+ @inlineCallbacks
553+ def test_put_file_relative_path(self):
554+ file_obj = StringIO("moon")
555+ yield self.storage.put("zebra/../zoo/reptiles/snakes.txt", file_obj)
556+ fh = yield self.storage.get("/zoo/reptiles/snakes.txt")
557+ self.assertEqual(fh.read(), "moon")
558+
559+ def test_put_file_invalid_relative_path(self):
560+ """Relative paths work as long as their contained in the storage path.
561+ """
562+ file_obj = StringIO("moon")
563+ return self.failUnlessFailure(
564+ self.storage.put("../../etc/profile.txt", file_obj),
565+ AssertionError)
566
567=== modified file 'ensemble/providers/dummy.py'
568--- ensemble/providers/dummy.py 2011-08-23 12:32:25 +0000
569+++ ensemble/providers/dummy.py 2011-09-13 23:16:26 +0000
570@@ -7,8 +7,10 @@
571 from txzookeeper import ZookeeperClient
572
573 from ensemble.errors import (
574- EnvironmentNotFound, FileNotFound, MachinesNotFound, ProviderError)
575+ EnvironmentNotFound, MachinesNotFound, ProviderError)
576+
577 from ensemble.machine import ProviderMachine
578+from ensemble.providers.common.files import FileStorage
579
580 log = logging.getLogger("ensemble.providers")
581
582@@ -179,40 +181,3 @@
583 if self._machines:
584 return succeed(self._machines[:1])
585 return fail(EnvironmentNotFound("not bootstrapped"))
586-
587-
588-class FileStorage(object):
589-
590- def __init__(self, path):
591- self._path = path
592-
593- @property
594- def path(self):
595- # not part of the api, just for test cleanup
596- return self._path
597-
598- def get_url(self, name):
599- file_path = os.path.abspath(os.path.join(
600- self._path, *filter(None, name.split("/"))))
601- return "file://%s" % file_path
602-
603- def get(self, name):
604- file_path = os.path.join(
605- self._path, *filter(None, name.split("/")))
606- if os.path.exists(file_path):
607- return succeed(open(file_path))
608- return fail(FileNotFound(file_path))
609-
610- def put(self, remote_path, file_object):
611- store_path = os.path.join(
612- self._path, *filter(None, remote_path.split("/")))
613- store_path = os.path.abspath(store_path)
614- if not store_path.startswith(self._path):
615- return fail(AssertionError("Invalid Remote Path %s" % remote_path))
616-
617- parent_store_path = os.path.dirname(store_path)
618- if not os.path.exists(parent_store_path):
619- os.makedirs(parent_store_path)
620- with open(store_path, "wb") as f:
621- f.write(file_object.read())
622- return succeed(True)
623
624=== modified file 'ensemble/providers/tests/test_dummy.py'
625--- ensemble/providers/tests/test_dummy.py 2011-08-22 23:03:03 +0000
626+++ ensemble/providers/tests/test_dummy.py 2011-09-13 23:16:26 +0000
627@@ -1,10 +1,10 @@
628 from cStringIO import StringIO
629-import os
630+
631 import zookeeper
632
633 from twisted.internet.defer import inlineCallbacks
634
635-from ensemble.errors import FileNotFound, ProviderError
636+from ensemble.errors import ProviderError
637 from ensemble.machine import ProviderMachine
638 from ensemble.providers.dummy import MachineProvider, DummyMachine
639
640@@ -152,79 +152,14 @@
641 self.assertEqual(exposed_ports,
642 set([(53, 'udp'), (80, 'tcp'), (443, 'tcp')]))
643
644-
645-class DummyProviderFileStorageTest(TestCase):
646-
647- def setUp(self):
648- super(DummyProviderFileStorageTest, self).setUp()
649- directory = self.makeDir()
650- self.provider = MachineProvider(
651- "foo", {"storage-directory": directory})
652- self.storage = self.provider.get_file_storage()
653-
654- def test_get_file_non_existent(self):
655- return self.failUnlessFailure(self.storage.get("/abc"), FileNotFound)
656-
657+ @inlineCallbacks
658 def test_file_storage_returns_same_storage(self):
659 """Multiple invocations of MachineProvider.get_file_storage use the
660 same path.
661 """
662- provider = MachineProvider("foo", {})
663- storage1 = provider.get_file_storage()
664- storage2 = provider.get_file_storage()
665- self.assertEqual(storage1.path, storage2.path)
666-
667- @inlineCallbacks
668- def test_file_storage_uses_configured_path(self):
669 file_obj = StringIO("rabbits")
670- yield self.storage.put("/magic/beans.txt", file_obj)
671+ storage = self.provider.get_file_storage()
672+ yield storage.put("/magic/beans.txt", file_obj)
673 storage2 = self.provider.get_file_storage()
674 fh = yield storage2.get("/magic/beans.txt")
675 self.assertEqual(fh.read(), "rabbits")
676-
677- def test_get_url(self):
678- url = self.storage.get_url("/abc.txt")
679- self.assertEqual(url, "file://%s/abc.txt" % self.storage.path)
680-
681- @inlineCallbacks
682- def test_get_file(self):
683- path = os.path.join(self.storage.path, "abc.txt")
684- self.makeFile("content", path=path)
685- fh = yield self.storage.get("/abc.txt")
686- self.assertEqual(fh.read(), "content")
687-
688- @inlineCallbacks
689- def test_put_and_get_file(self):
690- file_obj = StringIO("rabbits")
691- yield self.storage.put("/magic/beans.txt", file_obj)
692- fh = yield self.storage.get("/magic/beans.txt")
693- self.assertEqual(fh.read(), "rabbits")
694-
695- @inlineCallbacks
696- def test_put_same_path_multiple(self):
697- file_obj = StringIO("rabbits")
698- yield self.storage.put("/magic/beans.txt", file_obj)
699- file_obj = StringIO("elephant")
700- yield self.storage.put("/magic/beans.txt", file_obj)
701- fh = yield self.storage.get("/magic/beans.txt")
702- self.assertEqual(fh.read(), "elephant")
703-
704- @inlineCallbacks
705- def test_put_file_relative_path(self):
706- file_obj = StringIO("moon")
707- yield self.storage.put("zebra/../zoo/reptiles/snakes.txt", file_obj)
708- fh = yield self.storage.get("/zoo/reptiles/snakes.txt")
709- self.assertEqual(fh.read(), "moon")
710-
711- def test_put_file_invalid_relative_path(self):
712- """Relative paths work as long as their contained in the storage path.
713- """
714- file_obj = StringIO("moon")
715- return self.failUnlessFailure(
716- self.storage.put("../../etc/profile.txt", file_obj),
717- AssertionError)
718-
719- def test_provider_random_storage_dir(self):
720- provider = MachineProvider("foo", {})
721- storage = provider.get_file_storage()
722- self.assertTrue(os.path.exists(storage.path))

Subscribers

People subscribed via source and target branches

to status/vote changes: