Merge lp:~therve/landscape-client/sources-list-plugin into lp:~landscape/landscape-client/trunk
- sources-list-plugin
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Kevin McDermott |
Approved revision: | 335 |
Merged at revision: | 324 |
Proposed branch: | lp:~therve/landscape-client/sources-list-plugin |
Merge into: | lp:~landscape/landscape-client/trunk |
Diff against target: |
548 lines (+516/-2) 4 files modified
landscape/manager/aptsources.py (+155/-0) landscape/manager/config.py (+1/-1) landscape/manager/tests/test_aptsources.py (+359/-0) landscape/manager/tests/test_config.py (+1/-1) |
To merge this branch: | bzr merge lp:~therve/landscape-client/sources-list-plugin |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Kevin McDermott (community) | Approve | ||
Free Ekanayaka (community) | Approve | ||
Review via email: mp+57481@code.launchpad.net |
Commit message
Description of the change
Here it is. I think it's a fairly simple branch. It imports the gpg keys, and manages the sources.list. Nothing too fancy here. It relies on the full key being available in the server, so it means probably connecting to keys.ubuntu.com in Landscape. I think it's better than expected the clients to be able to open that weird port.
There is also no invalidation of keys. If someone feels it's important we can try to do it in a later branch (I think it implies creating a table to keep track of the added keys, so it's a little bit more involved).
- 329. By Thomas Herve
-
Add the plugin in all plugin list
- 330. By Thomas Herve
-
Remove temporary files
- 331. By Thomas Herve
-
Remove temporary files after failures
- 332. By Thomas Herve
-
Handle comments not starting the line
- 333. By Thomas Herve
-
Rename message type
- 334. By Thomas Herve
-
Rename plugin
Thomas Herve (therve) wrote : | # |
Thanks a lot for the careful review Free. I've handled 1 to 3, but I'll postpone 4 until we have a usecase.
- 335. By Thomas Herve
-
Run the reporter at the end of the message handling
Kevin McDermott (bigkevmcd) wrote : | # |
Ok, looks fine to me
+1
Preview Diff
1 | === added file 'landscape/manager/aptsources.py' |
2 | --- landscape/manager/aptsources.py 1970-01-01 00:00:00 +0000 |
3 | +++ landscape/manager/aptsources.py 2011-04-15 08:07:29 +0000 |
4 | @@ -0,0 +1,155 @@ |
5 | +import glob |
6 | +import os |
7 | +import tempfile |
8 | + |
9 | +from twisted.internet.defer import succeed |
10 | +from twisted.internet.utils import getProcessOutputAndValue |
11 | + |
12 | +from landscape.manager.plugin import ManagerPlugin, SUCCEEDED, FAILED |
13 | +from landscape.package.reporter import find_reporter_command |
14 | + |
15 | + |
16 | +class ProcessError(Exception): |
17 | + """Exception raised when running a process fails.""" |
18 | + |
19 | + |
20 | +class AptSources(ManagerPlugin): |
21 | + """A plugin managing sources.list content.""" |
22 | + |
23 | + SOURCES_LIST = "/etc/apt/sources.list" |
24 | + SOURCES_LIST_D = "/etc/apt/sources.list.d" |
25 | + |
26 | + def register(self, registry): |
27 | + super(AptSources, self).register(registry) |
28 | + registry.register_message( |
29 | + "apt-sources-replace", self._wrap_handle_repositories) |
30 | + |
31 | + def run_process(self, command, args): |
32 | + """ |
33 | + Run the process in an asynchronous fashion, to be overriden in tests. |
34 | + """ |
35 | + return getProcessOutputAndValue(command, args) |
36 | + |
37 | + def _wrap_handle_repositories(self, message): |
38 | + """ |
39 | + Wrap C{_handle_repositories} to generate an activity result based on |
40 | + the returned value. |
41 | + """ |
42 | + deferred = self._handle_repositories(message) |
43 | + |
44 | + operation_result = {"type": "operation-result", |
45 | + "operation-id": message["operation-id"]} |
46 | + |
47 | + def success(ignored): |
48 | + operation_result["status"] = SUCCEEDED |
49 | + return operation_result |
50 | + |
51 | + def fail(failure): |
52 | + operation_result["status"] = FAILED |
53 | + text = "%s: %s" % (failure.type.__name__, failure.value) |
54 | + operation_result["result-text"] = text |
55 | + return operation_result |
56 | + |
57 | + deferred.addCallbacks(success, fail) |
58 | + deferred.addBoth(lambda result: |
59 | + self.manager.broker.send_message(result, urgent=True)) |
60 | + |
61 | + def _handle_process_error(self, result): |
62 | + """ |
63 | + Turn a failed process command (code != 0) to a C{ProcessError}. |
64 | + """ |
65 | + out, err, code = result |
66 | + if code: |
67 | + raise ProcessError("%s\n%s" % (out, err)) |
68 | + |
69 | + def _handle_process_failure(self, failure): |
70 | + """ |
71 | + Turn a signaled process command to a C{ProcessError}. |
72 | + """ |
73 | + if not failure.check(ProcessError): |
74 | + out, err, signal = failure.value |
75 | + raise ProcessError("%s\n%s" % (out, err)) |
76 | + else: |
77 | + return failure |
78 | + |
79 | + def _remove_and_continue(self, passthrough, path): |
80 | + """ |
81 | + Remove the temporary file created for the process, and forward the |
82 | + result. |
83 | + """ |
84 | + os.unlink(path) |
85 | + return passthrough |
86 | + |
87 | + def _handle_repositories(self, message): |
88 | + """ |
89 | + Handle a list of repositories to set on the machine. |
90 | + |
91 | + The format is the following: |
92 | + |
93 | + {"sources": [ |
94 | + {"name": "repository-name", |
95 | + "content": |
96 | + "deb http://archive.ubuntu.com/ubuntu/ maverick main\n\ |
97 | + "deb-src http://archive.ubuntu.com/ubuntu/ maverick main"} |
98 | + {"name": "repository-name-dev", |
99 | + "content": |
100 | + "deb http://archive.ubuntu.com/ubuntu/ maverick universe\n\ |
101 | + "deb-src http://archive.ubuntu.com/ubuntu/ maverick universe"}], |
102 | + "gpg-keys": ["-----BEGIN PGP PUBLIC KEY BLOCK-----\n\ |
103 | + XXXX |
104 | + -----END PGP PUBLIC KEY BLOCK-----", |
105 | + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n\ |
106 | + YYY |
107 | + -----END PGP PUBLIC KEY BLOCK-----"]} |
108 | + """ |
109 | + deferred = succeed(None) |
110 | + for key in message["gpg-keys"]: |
111 | + fd, path = tempfile.mkstemp() |
112 | + os.close(fd) |
113 | + key_file = file(path, "w") |
114 | + key_file.write(key) |
115 | + key_file.close() |
116 | + deferred.addCallback( |
117 | + lambda ignore, path=path: |
118 | + self.run_process("/usr/bin/apt-key", ["add", path])) |
119 | + deferred.addCallback(self._handle_process_error) |
120 | + deferred.addBoth(self._remove_and_continue, path) |
121 | + deferred.addErrback(self._handle_process_failure) |
122 | + return deferred.addCallback( |
123 | + self._handle_sources, message["sources"]) |
124 | + |
125 | + def _handle_sources(self, ignored, sources): |
126 | + """Handle sources repositories.""" |
127 | + fd, path = tempfile.mkstemp() |
128 | + os.close(fd) |
129 | + new_sources = file(path, "w") |
130 | + for line in file(self.SOURCES_LIST): |
131 | + stripped_line = line.strip() |
132 | + if not stripped_line or stripped_line.startswith("#"): |
133 | + new_sources.write(line) |
134 | + else: |
135 | + new_sources.write("#%s" % line) |
136 | + new_sources.close() |
137 | + os.rename(path, self.SOURCES_LIST) |
138 | + |
139 | + for filename in glob.glob(os.path.join(self.SOURCES_LIST_D, "*.list")): |
140 | + os.rename(filename, "%s.save" % filename) |
141 | + |
142 | + for source in sources: |
143 | + filename = os.path.join(self.SOURCES_LIST_D, |
144 | + "landscape-%s.list" % source["name"]) |
145 | + sources_file = file(filename, "w") |
146 | + sources_file.write(source["content"]) |
147 | + sources_file.close() |
148 | + return self._run_reporter() |
149 | + |
150 | + def _run_reporter(self): |
151 | + """Once the repositories are modified, trigger a reporter run.""" |
152 | + reporter = find_reporter_command() |
153 | + |
154 | + # Force a smart-update run, because the sources.list has changed |
155 | + args = ["--force-smart-update"] |
156 | + |
157 | + if self.registry.config.config is not None: |
158 | + args.append("--config=%s" % self.registry.config.config) |
159 | + return self.run_process(reporter, args) |
160 | |
161 | === modified file 'landscape/manager/config.py' |
162 | --- landscape/manager/config.py 2010-08-06 17:57:42 +0000 |
163 | +++ landscape/manager/config.py 2011-04-15 08:07:29 +0000 |
164 | @@ -5,7 +5,7 @@ |
165 | |
166 | |
167 | ALL_PLUGINS = ["ProcessKiller", "PackageManager", "UserManager", |
168 | - "ShutdownManager", "Eucalyptus"] |
169 | + "ShutdownManager", "Eucalyptus", "AptSources"] |
170 | |
171 | |
172 | class ManagerConfiguration(Configuration): |
173 | |
174 | === added file 'landscape/manager/tests/test_aptsources.py' |
175 | --- landscape/manager/tests/test_aptsources.py 1970-01-01 00:00:00 +0000 |
176 | +++ landscape/manager/tests/test_aptsources.py 2011-04-15 08:07:29 +0000 |
177 | @@ -0,0 +1,359 @@ |
178 | +import os |
179 | + |
180 | +from twisted.internet.defer import Deferred |
181 | + |
182 | +from landscape.manager.aptsources import AptSources |
183 | +from landscape.manager.plugin import SUCCEEDED, FAILED |
184 | + |
185 | +from landscape.lib.twisted_util import gather_results |
186 | +from landscape.tests.helpers import LandscapeTest, ManagerHelper |
187 | +from landscape.package.reporter import find_reporter_command |
188 | + |
189 | + |
190 | +class AptSourcesTests(LandscapeTest): |
191 | + helpers = [ManagerHelper] |
192 | + |
193 | + def setUp(self): |
194 | + super(AptSourcesTests, self).setUp() |
195 | + self.sourceslist = AptSources() |
196 | + self.sources_path = self.makeDir() |
197 | + self.sourceslist.SOURCES_LIST = os.path.join(self.sources_path, |
198 | + "sources.list") |
199 | + sources_d = os.path.join(self.sources_path, "sources.list.d") |
200 | + os.mkdir(sources_d) |
201 | + self.sourceslist.SOURCES_LIST_D = sources_d |
202 | + self.manager.add(self.sourceslist) |
203 | + |
204 | + sources = file(self.sourceslist.SOURCES_LIST, "w") |
205 | + sources.write("\n") |
206 | + sources.close() |
207 | + |
208 | + service = self.broker_service |
209 | + service.message_store.set_accepted_types(["operation-result"]) |
210 | + |
211 | + self.sourceslist.run_process = lambda cmd, args: None |
212 | + |
213 | + def test_comment_sources_list(self): |
214 | + """ |
215 | + When getting a repository message, L{AptSources} comments the whole |
216 | + sources.list file. |
217 | + """ |
218 | + sources = file(self.sourceslist.SOURCES_LIST, "w") |
219 | + sources.write("oki\n\ndoki\n#comment\n # other comment\n") |
220 | + sources.close() |
221 | + |
222 | + self.manager.dispatch_message( |
223 | + {"type": "apt-sources-replace", "sources": [], "gpg-keys": [], |
224 | + "operation-id": 1}) |
225 | + |
226 | + self.assertEqual( |
227 | + "#oki\n\n#doki\n#comment\n # other comment\n", |
228 | + file(self.sourceslist.SOURCES_LIST).read()) |
229 | + |
230 | + service = self.broker_service |
231 | + self.assertMessages(service.message_store.get_pending_messages(), |
232 | + [{"type": "operation-result", |
233 | + "status": SUCCEEDED, "operation-id": 1}]) |
234 | + |
235 | + def test_random_failures(self): |
236 | + """ |
237 | + If a failure happens during the manipulation of sources, the activity |
238 | + is reported as FAILED with the error message. |
239 | + """ |
240 | + self.sourceslist.SOURCES_LIST = "/doesntexist" |
241 | + |
242 | + self.manager.dispatch_message( |
243 | + {"type": "apt-sources-replace", "sources": [], "gpg-keys": [], |
244 | + "operation-id": 1}) |
245 | + |
246 | + msg = "IOError: [Errno 2] No such file or directory: '/doesntexist'" |
247 | + service = self.broker_service |
248 | + self.assertMessages(service.message_store.get_pending_messages(), |
249 | + [{"type": "operation-result", |
250 | + "result-text": msg, "status": FAILED, |
251 | + "operation-id": 1}]) |
252 | + |
253 | + def test_rename_sources_list_d(self): |
254 | + """ |
255 | + The sources files in sources.list.d are renamed to .save when a message |
256 | + is received. |
257 | + """ |
258 | + sources1 = file( |
259 | + os.path.join(self.sourceslist.SOURCES_LIST_D, "file1.list"), "w") |
260 | + sources1.write("ok\n") |
261 | + sources1.close() |
262 | + |
263 | + sources2 = file( |
264 | + os.path.join(self.sourceslist.SOURCES_LIST_D, |
265 | + "file2.list.save"), "w") |
266 | + sources2.write("ok\n") |
267 | + sources2.close() |
268 | + |
269 | + self.manager.dispatch_message( |
270 | + {"type": "apt-sources-replace", "sources": [], "gpg-keys": [], |
271 | + "operation-id": 1}) |
272 | + |
273 | + self.assertFalse( |
274 | + os.path.exists( |
275 | + os.path.join(self.sourceslist.SOURCES_LIST_D, "file1.list"))) |
276 | + |
277 | + self.assertTrue( |
278 | + os.path.exists( |
279 | + os.path.join(self.sourceslist.SOURCES_LIST_D, |
280 | + "file1.list.save"))) |
281 | + |
282 | + self.assertTrue( |
283 | + os.path.exists( |
284 | + os.path.join(self.sourceslist.SOURCES_LIST_D, |
285 | + "file2.list.save"))) |
286 | + |
287 | + def test_create_landscape_sources(self): |
288 | + """ |
289 | + For every sources listed in the sources field of the message, |
290 | + C{AptSources} creates a file with the content in sources.list.d. |
291 | + """ |
292 | + sources = [{"name": "dev", "content": "oki\n"}, |
293 | + {"name": "lucid", "content": "doki\n"}] |
294 | + self.manager.dispatch_message( |
295 | + {"type": "apt-sources-replace", "sources": sources, "gpg-keys": [], |
296 | + "operation-id": 1}) |
297 | + |
298 | + dev_file = os.path.join(self.sourceslist.SOURCES_LIST_D, |
299 | + "landscape-dev.list") |
300 | + self.assertTrue(os.path.exists(dev_file)) |
301 | + self.assertEqual("oki\n", file(dev_file).read()) |
302 | + |
303 | + lucid_file = os.path.join(self.sourceslist.SOURCES_LIST_D, |
304 | + "landscape-lucid.list") |
305 | + self.assertTrue(os.path.exists(lucid_file)) |
306 | + self.assertEqual("doki\n", file(lucid_file).read()) |
307 | + |
308 | + def test_import_gpg_keys(self): |
309 | + """ |
310 | + C{AptSources} runs a process with apt-key for every keys in the |
311 | + message. |
312 | + """ |
313 | + deferred = Deferred() |
314 | + |
315 | + def run_process(command, args): |
316 | + self.assertEqual("/usr/bin/apt-key", command) |
317 | + self.assertEqual("add", args[0]) |
318 | + filename = args[1] |
319 | + self.assertEqual("Some key content", file(filename).read()) |
320 | + deferred.callback(("ok", "", 0)) |
321 | + return deferred |
322 | + |
323 | + self.sourceslist.run_process = run_process |
324 | + |
325 | + self.manager.dispatch_message( |
326 | + {"type": "apt-sources-replace", "sources": [], |
327 | + "gpg-keys": ["Some key content"], "operation-id": 1}) |
328 | + |
329 | + return deferred |
330 | + |
331 | + def test_import_delete_temporary_files(self): |
332 | + """ |
333 | + The files created to be imported by C{apt-key} are removed after the |
334 | + import. |
335 | + """ |
336 | + deferred = Deferred() |
337 | + filenames = [] |
338 | + |
339 | + def run_process(command, args): |
340 | + if not filenames: |
341 | + filenames.append(args[1]) |
342 | + deferred.callback(("ok", "", 0)) |
343 | + return deferred |
344 | + |
345 | + self.sourceslist.run_process = run_process |
346 | + |
347 | + self.manager.dispatch_message( |
348 | + {"type": "apt-sources-replace", "sources": [], |
349 | + "gpg-keys": ["Some key content"], "operation-id": 1}) |
350 | + |
351 | + self.assertFalse(os.path.exists(filenames[0])) |
352 | + |
353 | + return deferred |
354 | + |
355 | + def test_failed_import_delete_temporary_files(self): |
356 | + """ |
357 | + The files created to be imported by C{apt-key} are removed after the |
358 | + import, even if there is a failure. |
359 | + """ |
360 | + deferred = Deferred() |
361 | + filenames = [] |
362 | + |
363 | + def run_process(command, args): |
364 | + filenames.append(args[1]) |
365 | + deferred.callback(("error", "", 1)) |
366 | + return deferred |
367 | + |
368 | + self.sourceslist.run_process = run_process |
369 | + |
370 | + self.manager.dispatch_message( |
371 | + {"type": "apt-sources-replace", "sources": [], |
372 | + "gpg-keys": ["Some key content"], "operation-id": 1}) |
373 | + |
374 | + self.assertFalse(os.path.exists(filenames[0])) |
375 | + |
376 | + return deferred |
377 | + |
378 | + def test_failed_import_reported(self): |
379 | + """ |
380 | + If the C{apt-key} command failed for some reasons, the output of the |
381 | + command is reported and the activity fails. |
382 | + """ |
383 | + deferred = Deferred() |
384 | + |
385 | + def run_process(command, args): |
386 | + deferred.callback(("nok", "some error", 1)) |
387 | + return deferred |
388 | + |
389 | + self.sourceslist.run_process = run_process |
390 | + |
391 | + self.manager.dispatch_message( |
392 | + {"type": "apt-sources-replace", "sources": [], "gpg-keys": ["key"], |
393 | + "operation-id": 1}) |
394 | + |
395 | + service = self.broker_service |
396 | + msg = "ProcessError: nok\nsome error" |
397 | + self.assertMessages(service.message_store.get_pending_messages(), |
398 | + [{"type": "operation-result", |
399 | + "result-text": msg, "status": FAILED, |
400 | + "operation-id": 1}]) |
401 | + return deferred |
402 | + |
403 | + def test_signaled_import_reported(self): |
404 | + """ |
405 | + If the C{apt-key} fails with a signal, the output of the command is |
406 | + reported and the activity fails. |
407 | + """ |
408 | + deferred = Deferred() |
409 | + |
410 | + def run_process(command, args): |
411 | + deferred.errback(("nok", "some error", 1)) |
412 | + return deferred |
413 | + |
414 | + self.sourceslist.run_process = run_process |
415 | + |
416 | + self.manager.dispatch_message( |
417 | + {"type": "apt-sources-replace", "sources": [], "gpg-keys": ["key"], |
418 | + "operation-id": 1}) |
419 | + |
420 | + service = self.broker_service |
421 | + msg = "ProcessError: nok\nsome error" |
422 | + self.assertMessages(service.message_store.get_pending_messages(), |
423 | + [{"type": "operation-result", |
424 | + "result-text": msg, "status": FAILED, |
425 | + "operation-id": 1}]) |
426 | + return deferred |
427 | + |
428 | + def test_failed_import_no_changes(self): |
429 | + """ |
430 | + If the C{apt-key} command failed for some reasons, the current |
431 | + repositories aren't changed. |
432 | + """ |
433 | + deferred = Deferred() |
434 | + |
435 | + def run_process(command, args): |
436 | + deferred.callback(("nok", "some error", 1)) |
437 | + return deferred |
438 | + |
439 | + self.sourceslist.run_process = run_process |
440 | + |
441 | + sources = file(self.sourceslist.SOURCES_LIST, "w") |
442 | + sources.write("oki\n\ndoki\n#comment\n") |
443 | + sources.close() |
444 | + |
445 | + self.manager.dispatch_message( |
446 | + {"type": "apt-sources-replace", "sources": [], "gpg-keys": ["key"], |
447 | + "operation-id": 1}) |
448 | + |
449 | + self.assertEqual( |
450 | + "oki\n\ndoki\n#comment\n", |
451 | + file(self.sourceslist.SOURCES_LIST).read()) |
452 | + |
453 | + return deferred |
454 | + |
455 | + def test_multiple_import_sequential(self): |
456 | + """ |
457 | + If multiple keys are specified, the imports run sequentially, not in |
458 | + parallel. |
459 | + """ |
460 | + deferred1 = Deferred() |
461 | + deferred2 = Deferred() |
462 | + deferreds = [deferred1, deferred2] |
463 | + |
464 | + def run_process(command, args): |
465 | + if not deferreds: |
466 | + return None |
467 | + return deferreds.pop(0) |
468 | + |
469 | + self.sourceslist.run_process = run_process |
470 | + |
471 | + self.manager.dispatch_message( |
472 | + {"type": "apt-sources-replace", "sources": [], |
473 | + "gpg-keys": ["key1", "key2"], "operation-id": 1}) |
474 | + |
475 | + self.assertEqual(1, len(deferreds)) |
476 | + deferred1.callback(("ok", "", 0)) |
477 | + |
478 | + self.assertEqual(0, len(deferreds)) |
479 | + deferred2.callback(("ok", "", 0)) |
480 | + |
481 | + service = self.broker_service |
482 | + self.assertMessages(service.message_store.get_pending_messages(), |
483 | + [{"type": "operation-result", |
484 | + "status": SUCCEEDED, "operation-id": 1}]) |
485 | + return gather_results(deferreds) |
486 | + |
487 | + def test_multiple_import_failure(self): |
488 | + """ |
489 | + If multiple keys are specified, and that the first one fails, the error |
490 | + is correctly reported. |
491 | + """ |
492 | + deferred1 = Deferred() |
493 | + deferred2 = Deferred() |
494 | + deferreds = [deferred1, deferred2] |
495 | + |
496 | + def run_process(command, args): |
497 | + return deferreds.pop(0) |
498 | + |
499 | + self.sourceslist.run_process = run_process |
500 | + |
501 | + self.manager.dispatch_message( |
502 | + {"type": "apt-sources-replace", "sources": [], |
503 | + "gpg-keys": ["key1", "key2"], "operation-id": 1}) |
504 | + |
505 | + deferred1.callback(("error", "", 1)) |
506 | + deferred2.callback(("error", "", 1)) |
507 | + |
508 | + msg = "ProcessError: error\n" |
509 | + service = self.broker_service |
510 | + self.assertMessages(service.message_store.get_pending_messages(), |
511 | + [{"type": "operation-result", |
512 | + "result-text": msg, "status": FAILED, |
513 | + "operation-id": 1}]) |
514 | + return gather_results(deferreds) |
515 | + |
516 | + def test_run_reporter(self): |
517 | + """ |
518 | + After receiving a message, L{AptSources} triggers a reporter run to |
519 | + have the new packages reported to the server. |
520 | + """ |
521 | + deferred = Deferred() |
522 | + |
523 | + def run_process(command, args): |
524 | + self.assertEqual(find_reporter_command(), command) |
525 | + self.assertEqual(["--force-smart-update", "--config=%s" % |
526 | + self.manager.config.config], args) |
527 | + deferred.callback(("ok", "", 0)) |
528 | + return deferred |
529 | + |
530 | + self.sourceslist.run_process = run_process |
531 | + |
532 | + self.manager.dispatch_message( |
533 | + {"type": "apt-sources-replace", "sources": [], "gpg-keys": [], |
534 | + "operation-id": 1}) |
535 | + |
536 | + return deferred |
537 | |
538 | === modified file 'landscape/manager/tests/test_config.py' |
539 | --- landscape/manager/tests/test_config.py 2010-08-06 17:57:42 +0000 |
540 | +++ landscape/manager/tests/test_config.py 2011-04-15 08:07:29 +0000 |
541 | @@ -12,7 +12,7 @@ |
542 | def test_plugin_factories(self): |
543 | """By default all plugins are enabled.""" |
544 | self.assertEqual(["ProcessKiller", "PackageManager", "UserManager", |
545 | - "ShutdownManager", "Eucalyptus"], |
546 | + "ShutdownManager", "Eucalyptus", "AptSources"], |
547 | ALL_PLUGINS) |
548 | self.assertEqual(ALL_PLUGINS, self.config.plugin_factories) |
549 |
Nice work, nothing really blocking so feel free to address what you think is worth. +1
[1]
+ deferred. addCallback( self._handle_ process_ error) addCallback( lambda ignore, path=path: os.unlink(path))
+ deferred.
Probably not I huge deal, but I suspect that the key file is not removed if the process fails?
[2]
+ if not line.strip() or line.startswith ("#"):
I think comments can start also after some blanks, so maybe:
+ if not line.strip() or line.strip( ).startswith( "#"):
[3]
+class SourcesList( ManagerPlugin) :
and
+ "repositories", self._wrap_ handle_ repositories)
It would be nice for the plugin and message name to match a bit more (that's often the cases for other plugins/messages). I'd suggest "class AptSources" and "apt-sources- replace" or something. The name "reposiories" sounds a bit generic, in case we want to add other operations later. This not blocking though.
[4]
+class ProcessError( Exception) :
+ """Exception raised when running a process fails."""
and
+ def run_process(self, command, args):
This seems generic enough that it could be made a function under landscape. lib.process, like:
class ProcessError( Exception) :
"""Exception raised when running a process fails."""
def __init__(self, out, err):
super( ProcessError, self)._ _init__ ("%s\n% s" % (out, err))
def run_process(self, command, args):
"""Run the process in an asynchronous fashion."""
def handle_ process_ error(result) :
"""
Turn a failed process command (code != 0) to a C{ProcessError}.
"""
out, err, code = result
if code:
raise ProcessError(out, err)
else:
return result
def handle_ process_ failure( failure) :
"""
Turn a signaled process command to a C{ProcessError}.
"""
out, err, signal = failure.value
raise ProcessError(out, err)
deferred = getProcessOutpu tAndValue( command, args) addCallback( handle_ process_ error) addErrback( handle_ process_ failure)
deferred.
deferred.
return deferred
You could test run_process separately, and have the tests for SourcesList simply shows that ProcessError failures are handled correctly:
def test_failed_ import_ no_changes( self):
repositories aren't changed.
"""
If the C{apt-key} command failed for some reasons, the current
"""
def run_process( command, args): or("nok" , "some error"))
return fail(ProcessErr
sources = file(self. sourceslist. SOURCES_ LIST, "w")
sources. write(" oki\n\ndoki\ n#comment\ n")
sources. close()
service = self.broker_service
self.assertMes sages(service. message_ store.get_ pending_ messages( ),
[{"type" : "operation-result",
'result- te...