Merge lp:~hazmat/pyjuju/lib-files into lp:pyjuju
- lib-files
- Merge into trunk
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 | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gustavo Niemeyer | Approve | ||
William Reade (community) | Needs Fixing | ||
Review via email: mp+73444@code.launchpad.net |
Commit message
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.
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
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.
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.
Gustavo Niemeyer (niemeyer) wrote : | # |
Sounds good, thanks for the info.
Gustavo Niemeyer (niemeyer) wrote : | # |
Putting it back in review for William to see if the changes look good.
- 344. By Kapil Thangavelu
-
Merged lib-zookeeper into lib-files.
- 345. By Kapil Thangavelu
-
Merged lib-zookeeper into lib-files.
Gustavo Niemeyer (niemeyer) wrote : | # |
Please go ahead and merge this Kapil.
Preview Diff
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)) |
[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?