Merge lp:~javier.collado/utah/bug1101186 into lp:utah
- bug1101186
- Merge into dev
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Approved by: | Javier Collado | ||||||||
Approved revision: | 869 | ||||||||
Merged at revision: | 846 | ||||||||
Proposed branch: | lp:~javier.collado/utah/bug1101186 | ||||||||
Merge into: | lp:utah | ||||||||
Diff against target: |
894 lines (+364/-145) 16 files modified
debian/changelog (+1/-0) debian/control (+1/-1) docs/source/reference.rst (+9/-0) examples/run_install_test.py (+5/-1) examples/run_test_bamboo_feeder.py (+5/-1) examples/run_test_cobbler.py (+5/-1) examples/run_test_vm.py (+5/-1) examples/run_utah_tests.py (+3/-0) utah/cleanup.py (+210/-0) utah/process.py (+56/-11) utah/provisioning/baremetal/bamboofeeder.py (+6/-5) utah/provisioning/baremetal/cobbler.py (+11/-10) utah/provisioning/baremetal/power.py (+3/-2) utah/provisioning/provisioning.py (+39/-109) utah/provisioning/vm/vm.py (+3/-3) utah/url.py (+2/-0) |
||||||||
To merge this branch: | bzr merge lp:~javier.collado/utah/bug1101186 | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andy Doan (community) | Approve | ||
Max Brustkern (community) | Approve | ||
Javier Collado (community) | Needs Resubmitting | ||
Review via email:
|
Commit message
Description of the change
In this branch:
- url_argument adds temporary directories to be cleaned up later
- cleanup code has been moved to utah.cleanup module (needed by url_argument)
- Machine._runargs method has been moved to utah.process.
Aside from this, I've changed the way cleanup is run. In particular, it no
longer relies on __del__ method since in my tests I had some problems like os
module not being available at cleanup time. Hence, I decided to use a simple
try/finally block in the run scripts which I find more readable because it's
explicit.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
@Max
Thanks for your comments. Please find my answer to them below.
I don't think we should catch any exception in ProcessRunner unless we know we
want to handle any possible exception in the same way and that pretty much
depends on the command being executed, so that probably should be handled on a
case by case basis by the caller.
Regarding keeping a _runargs method in the Machine class, I'm not sure if
there's a use case for that. There's already a run method that is used only for
SSH commands, but I guess it can be expanded to terminal commands or whatever.
The way I see it, ProcessRunner is just a facility to execute commands locally
in the system where the server process is running, but it's not intended to
execute remote commands in the machine being provisioned.
Using atexit to register cleanup methods should work fine, it's just that I
like to see a explicit call in the code to keep in mind that that the cleanup
is actually happening. Anyway, I can give a try to that approach and see how it
works.
By the way, some other thing I'd like to do is review the documentation strings
before merging the changes.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
I think the leaving exception handling out of ProcessRunner makes sense. I think the Cleanup class should handle exceptions generated by ProcessRunner. If some class registers a bad cleanup routine, that shouldn't stop other cleanup routines from running. I don't know if we just want to log that there's a problem, or try to catch exceptions and re-raise them at the end of the cleanup process, but we need some way for the process to not be completely stopped by a single problematic instruction.
I think an explicit cleanup call is a good thing to do, I just want to minimize things falling through the cracks when that isn't done. atexit can be a last resort.
I looked at the documentation strings and they seemed reasonable to me. We should probably add sphinx stuff to them at some point, but that's something we are doing as part of a separate effort, so if we don't do it now, we do at least have a plan for doing it later.
- 860. By Javier Collado
-
Added atexit registration to run cleanup even if not called explicitly
Changes suggested by Max.
- 861. By Javier Collado
-
Capture and log command exceptions when running cleanup
- 862. By Javier Collado
-
Fixed cleanfunction wrapping
- 863. By Javier Collado
-
Added documentation for the cleanup module
- 864. By Javier Collado
-
Updated documentation for clean methods in Machine class
- 865. By Javier Collado
-
Added warning about cleanup singleton object
- 866. By Javier Collado
-
Updated documentation for the process module
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
@Max
I've updated the code following your advice:
- atexit used to run cleanup as a fallback.
- ProcessRunner exceptions caught and logged during cleanup to clean as much as
possible (that was a good catch, thanks).
Aside from this, I've also updated the documentation.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
Would it make any sense for line 222 to be in a finally clause? I guess if we're catching all exceptions there, then it doesn't need to be.
Good call on updating the rst file, I always forget to do that. We should include any other needed updates to that as part of the upcoming documentation changes that I'd like to look at next week (after I've sorted out all the code review things related to that that we raised.)
Finally, some of the whitespace around the docstrings (lines 384 and 386) looks weird to me, but I haven't read PEP 257 very well, so if that's explained there, then I'll know about it shortly.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
Additional comments:
ProcessRunner feels funny defined as a class with just an __init__, but I like how you now have explicit things like .returncode rather than ['returncode']. So no real objection - just found it interesting.
Cleanup._clean_dir: can we remove the self.logger.warning call about the permissions? It seems to make the end of our log files nearly unreadable and things still work properly.
Cleanup._clean_dir: Do we really have to traverse the tree to make sure everything is writeable in order to safely call shutil.rmtree? If so, subprocess.
Given cleanup.py now has an atexit, is there any real gain by updating all the run scripts? I feel like we are exposing too much internal details with such a high level API. Another cheap way might be just to handle clean up in the _runtests function in run.py.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
> Additional comments:
>
> ProcessRunner feels funny defined as a class with just an __init__, but I like
> how you now have explicit things like .returncode rather than ['returncode'].
> So no real objection - just found it interesting.
>
> Cleanup._clean_dir: can we remove the self.logger.warning call about the
> permissions? It seems to make the end of our log files nearly unreadable and
> things still work properly.
Maybe this should be debug instead of warning? Or we could drop it, I guess nobody's looking at that warning and taking appropriate action.
>
> Cleanup._clean_dir: Do we really have to traverse the tree to make sure
> everything is writeable in order to safely call shutil.rmtree? If so,
> subprocess.
> to me.
We've had a general trend of moving away from shelling out whenever possible, but in this case, that implementation may be more trouble than it's worth. It predates this, though, so any problems with it are my fault and not Javier's.
>
> Given cleanup.py now has an atexit, is there any real gain by updating all the
> run scripts? I feel like we are exposing too much internal details with such a
> high level API. Another cheap way might be just to handle clean up in the
> _runtests function in run.py.
As Javier pointed at before, some testing indicated problems with os availability during atexit. I think we should raise the priority of run_ script consolidation so we don't have to keep going through this.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
On 03/28/2013 02:34 PM, Max Brustkern wrote:
>> Cleanup._clean_dir: can we remove the self.logger.warning call about the
>> permissions? It seems to make the end of our log files nearly unreadable and
>> things still work properly.
> Maybe this should be debug instead of warning? Or we could drop it, I guess nobody's looking at that warning and taking appropriate action.
I thought about changing to debug, but I run with debug enabled so much
it still drives me nuts. But maybe that's a better compromise. I'll let
Javier choose since its his MP and he's been around longer - no big deal
to me either way.
>> Cleanup._clean_dir: Do we really have to traverse the tree to make sure
>> everything is writeable in order to safely call shutil.rmtree? If so,
>> subprocess.
>> to me.
> We've had a general trend of moving away from shelling out whenever possible, but in this case, that implementation may be more trouble than it's worth. It predates this, though, so any problems with it are my fault and not Javier's.
Yeah, I hate shelling out. Do we know for a fact/why you must change the
permissions? I just set up a dummy test locally with read-only files and
shutil.rmtree still worked.
>> Given cleanup.py now has an atexit, is there any real gain by updating all the
>> run scripts? I feel like we are exposing too much internal details with such a
>> high level API. Another cheap way might be just to handle clean up in the
>> _runtests function in run.py.
> As Javier pointed at before, some testing indicated problems with os availability during atexit. I think we should raise the priority of run_ script consolidation so we don't have to keep going through this.
yes - sorry I forgot.
+1 on the run_script fixes.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
On 03/28/2013 03:40 PM, Andy Doan wrote:
> On 03/28/2013 02:34 PM, Max Brustkern wrote:
> ...
>>> Cleanup._clean_dir: Do we really have to traverse the tree to make sure
>>> everything is writeable in order to safely call shutil.rmtree? If so,
>>> subprocess.
>>> desirable
>>> to me.
>> We've had a general trend of moving away from shelling out whenever
>> possible, but in this case, that implementation may be more trouble
>> than it's worth. It predates this, though, so any problems with it
>> are my fault and not Javier's.
>
> Yeah, I hate shelling out. Do we know for a fact/why you must change
> the permissions? I just set up a dummy test locally with read-only
> files and shutil.rmtree still worked.
>
I thought that I had had problems with that before, but if it seems to
work okay now, maybe I was wrong, and we can just use rmtree
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
On 03/28/2013 02:44 PM, Max Brustkern wrote:
> On 03/28/2013 03:40 PM, Andy Doan wrote:
>> On 03/28/2013 02:34 PM, Max Brustkern wrote:
> I thought that I had had problems with that before, but if it seems to
> work okay now, maybe I was wrong, and we can just use rmtree
hmm - i don't want to distract too much from Javier's intent here. I'll
leave it up to him to decide if he want to do that in this change or
just land this and let us re-visit later.
- 867. By Javier Collado
-
Moved clean command code to its own method.
Command removed in finally block. Not really needed, but better in terms of
readability. - 868. By Javier Collado
-
Added note to make clear that process is launched right away
- 869. By Javier Collado
-
Removed log message that turned out to be too verbose
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
Some comments:
- I like the comment about the finally clause. It's not needed, but it improves
readability, so I've done that (and moved the code to a separate method).
- What is bothering me isn't the warning message, but the debug message for
each file whose permissions are changed because there are a lot of them in
the image directory. I've removed that and what I get now is the old output
that ends with the location where the yaml file has been downloaded. I think
we can go with this and check if there are so many warnings in the lab.
Anyway, if for some reason those messages are needed and we don't want them
in the debug output, then we can create a new log level name that is lower
than debug, but is only written to the log file.
- The problems I got during cleanup were when relying on __del__, not on
atexit. Anyway, it doesn't make any harm to run cleanup twice, so I'd prefer
to keep the try/finally block as it's now.
- I've added a note in the ProcessRunner documentation to make clear that the
command is executed immediately.
- I don't know PEP257 by heart, but I trust pep257
(https:/
option to print the paragraph from PEP257 that is being violated. For the
whitespace around docstrings, what you'd get would something as follows:
=======
Note: checks are relaxed for scripts (with #!) compared to modules.
process.py:24:4: PEP257 Class docstring should have 1 blank line around them.
PEP257 Class docstring should have 1 blank line around them.
Insert a blank line before and after all docstrings (one-line or
multi-line) that document a class -- generally speaking, the class's
methods are separated from each other by a single blank line, and the
docstring needs to be offset from the first method by a blank line;
for symmetry, put a blank line between the class header and the
docstring.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Javier Collado (javier.collado) wrote : | # |
Maybe this wasn't clear from my previous message, but note that when I write
PEP257 I talk about the pep itself (http://
and when I write pep257 I refer to the command line tool
(https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Max Brustkern (nuclearbob) wrote : | # |
> Maybe this wasn't clear from my previous message, but note that when I write
> PEP257 I talk about the pep itself (http://
> and when I write pep257 I refer to the command line tool
> (https:/
That was clear to me. In any case, I think this looks good.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andy Doan (doanac) wrote : | # |
+1 - this branch has some things I've been wanting or a long time :)
Preview Diff
1 | === modified file 'debian/changelog' |
2 | --- debian/changelog 2013-03-28 03:45:47 +0000 |
3 | +++ debian/changelog 2013-03-29 09:03:24 +0000 |
4 | @@ -1,6 +1,7 @@ |
5 | utah (0.10ubuntu1) UNRELEASED; urgency=low |
6 | |
7 | * Return error code on unhandled error (LP: #1160857) |
8 | + * Remove temporary files downloaded based on URL (LP: #1101186) |
9 | |
10 | -- Javier Collado <javier.collado@canonical.com> Wed, 27 Mar 2013 13:00:47 +0100 |
11 | |
12 | |
13 | === modified file 'debian/control' |
14 | --- debian/control 2013-02-26 08:12:33 +0000 |
15 | +++ debian/control 2013-03-29 09:03:24 +0000 |
16 | @@ -49,7 +49,7 @@ |
17 | Package: utah-client |
18 | Architecture: all |
19 | Depends: ${misc:Depends}, ${python:Depends}, |
20 | - bzr, python-jsonschema (>= 0.5~), python-yaml, utah-common |
21 | + bzr, python-jsonschema (>= 0.5~), python-psutil, python-yaml, utah-common |
22 | Recommends: git |
23 | Description: Ubuntu Test Automation Harness Client |
24 | Automation framework for testing in Ubuntu, client portion |
25 | |
26 | === modified file 'docs/source/reference.rst' |
27 | --- docs/source/reference.rst 2013-02-27 09:05:41 +0000 |
28 | +++ docs/source/reference.rst 2013-03-29 09:03:24 +0000 |
29 | @@ -7,6 +7,12 @@ |
30 | |
31 | .. automodule:: utah |
32 | |
33 | +``utah.cleanup`` |
34 | +---------------- |
35 | + |
36 | +.. automodule:: utah.cleanup |
37 | + :members: _Cleanup, cleanup |
38 | + |
39 | .. automodule:: utah.commandstr |
40 | :members: |
41 | |
42 | @@ -35,6 +41,9 @@ |
43 | :members: |
44 | :member-order: bysource |
45 | |
46 | +``utah.process`` |
47 | +---------------- |
48 | + |
49 | .. automodule:: utah.process |
50 | :members: |
51 | |
52 | |
53 | === modified file 'examples/run_install_test.py' |
54 | --- examples/run_install_test.py 2013-03-27 11:57:11 +0000 |
55 | +++ examples/run_install_test.py 2013-03-29 09:03:24 +0000 |
56 | @@ -19,6 +19,7 @@ |
57 | import logging |
58 | import sys |
59 | |
60 | +from utah.cleanup import cleanup |
61 | from utah.exceptions import UTAHException |
62 | from utah.group import check_user_group, print_group_error_message |
63 | from utah.provisioning.vm.vm import CustomVM, TinySQLiteInventory |
64 | @@ -106,4 +107,7 @@ |
65 | |
66 | |
67 | if __name__ == '__main__': |
68 | - run_install_test() |
69 | + try: |
70 | + run_install_test() |
71 | + finally: |
72 | + cleanup.run() |
73 | |
74 | === modified file 'examples/run_test_bamboo_feeder.py' |
75 | --- examples/run_test_bamboo_feeder.py 2013-03-27 11:57:11 +0000 |
76 | +++ examples/run_test_bamboo_feeder.py 2013-03-29 09:03:24 +0000 |
77 | @@ -20,6 +20,7 @@ |
78 | import sys |
79 | import logging |
80 | |
81 | +from utah.cleanup import cleanup |
82 | from utah.exceptions import UTAHException |
83 | from utah.group import check_user_group, print_group_error_message |
84 | from utah.provisioning.baremetal.bamboofeeder import BambooFeederMachine |
85 | @@ -106,4 +107,7 @@ |
86 | |
87 | |
88 | if __name__ == '__main__': |
89 | - run_test_bamboo_feeder() |
90 | + try: |
91 | + run_test_bamboo_feeder() |
92 | + finally: |
93 | + cleanup.run() |
94 | |
95 | === modified file 'examples/run_test_cobbler.py' |
96 | --- examples/run_test_cobbler.py 2013-03-27 11:57:11 +0000 |
97 | +++ examples/run_test_cobbler.py 2013-03-29 09:03:24 +0000 |
98 | @@ -19,6 +19,7 @@ |
99 | import sys |
100 | import logging |
101 | |
102 | +from utah.cleanup import cleanup |
103 | from utah.exceptions import UTAHException |
104 | from utah.group import check_user_group, print_group_error_message |
105 | from utah.provisioning.baremetal.cobbler import CobblerMachine |
106 | @@ -111,4 +112,7 @@ |
107 | |
108 | |
109 | if __name__ == '__main__': |
110 | - run_test_cobbler() |
111 | + try: |
112 | + run_test_cobbler() |
113 | + finally: |
114 | + cleanup.run() |
115 | |
116 | === modified file 'examples/run_test_vm.py' |
117 | --- examples/run_test_vm.py 2013-03-27 11:57:11 +0000 |
118 | +++ examples/run_test_vm.py 2013-03-29 09:03:24 +0000 |
119 | @@ -19,6 +19,7 @@ |
120 | import logging |
121 | import sys |
122 | |
123 | +from utah.cleanup import cleanup |
124 | from utah.exceptions import UTAHException |
125 | from utah.group import check_user_group, print_group_error_message |
126 | from utah.provisioning.vm.vm import TinySQLiteInventory |
127 | @@ -100,4 +101,7 @@ |
128 | |
129 | |
130 | if __name__ == '__main__': |
131 | - run_test_vm() |
132 | + try: |
133 | + run_test_vm() |
134 | + finally: |
135 | + cleanup.run() |
136 | |
137 | === modified file 'examples/run_utah_tests.py' |
138 | --- examples/run_utah_tests.py 2013-03-27 11:57:11 +0000 |
139 | +++ examples/run_utah_tests.py 2013-03-29 09:03:24 +0000 |
140 | @@ -20,6 +20,7 @@ |
141 | import sys |
142 | |
143 | from utah import config |
144 | +from utah.cleanup import cleanup |
145 | from utah.group import check_user_group, print_group_error_message |
146 | from utah.run import ( |
147 | common_arguments, |
148 | @@ -131,3 +132,5 @@ |
149 | run_utah_tests() |
150 | except AttributeError: |
151 | run_utah_tests() |
152 | + finally: |
153 | + cleanup.run() |
154 | |
155 | === added file 'utah/cleanup.py' |
156 | --- utah/cleanup.py 1970-01-01 00:00:00 +0000 |
157 | +++ utah/cleanup.py 2013-03-29 09:03:24 +0000 |
158 | @@ -0,0 +1,210 @@ |
159 | +# Ubuntu Testing Automation Harness |
160 | +# Copyright 2012 Canonical Ltd. |
161 | + |
162 | +# This program is free software: you can redistribute it and/or modify it |
163 | +# under the terms of the GNU General Public License version 3, as published |
164 | +# by the Free Software Foundation. |
165 | + |
166 | +# This program is distributed in the hope that it will be useful, but |
167 | +# WITHOUT ANY WARRANTY; without even the implied warranties of |
168 | +# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR |
169 | +# PURPOSE. See the GNU General Public License for more details. |
170 | + |
171 | +# You should have received a copy of the GNU General Public License along |
172 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
173 | + |
174 | +"""Generic functionality to execute callbacks on exit.""" |
175 | + |
176 | +import atexit |
177 | +import logging |
178 | +import os |
179 | +import shutil |
180 | +import traceback |
181 | + |
182 | +from utah.commandstr import commandstr |
183 | +from utah.orderedcollections import ( |
184 | + HashableDict, |
185 | + OrderedSet, |
186 | +) |
187 | +from utah.process import ProcessRunner |
188 | +import utah.timeout |
189 | + |
190 | + |
191 | +class _Cleanup(object): |
192 | + |
193 | + """Cleanup allocated resources on exit. |
194 | + |
195 | + .. warning:: |
196 | + This is private class not expected to be instanciated |
197 | + please use :attr:`utah.cleanup.cleanup` singleton |
198 | + object to call any of the methods documented below. |
199 | + |
200 | + """ |
201 | + |
202 | + def __init__(self): |
203 | + self.paths = OrderedSet() |
204 | + self.functions = OrderedSet() |
205 | + self.commands = OrderedSet() |
206 | + self.logger = logging.getLogger('cleanup') |
207 | + |
208 | + def run(self): |
209 | + """Run cleanup for the resources that have been allocated.""" |
210 | + self.logger.debug('Running cleanup') |
211 | + for function in self.functions: |
212 | + self._clean_function(function) |
213 | + for command in self.commands: |
214 | + self._clean_command(command) |
215 | + for path in self.paths: |
216 | + self._clean_path(path) |
217 | + |
218 | + def _clean_function(self, function): |
219 | + """Run clean function. |
220 | + |
221 | + :param function: |
222 | + Data needed to run the clean function: timeout, callable, |
223 | + positional arguments and keyword arguments. |
224 | + :type function: tuple |
225 | + |
226 | + """ |
227 | + timeout, command, args, kw = function |
228 | + self.logger.debug('Running: ' + |
229 | + commandstr(command, *args, **kw)) |
230 | + try: |
231 | + utah.timeout.timeout(timeout, command, *args, **kw) |
232 | + except Exception as err: |
233 | + self.logger.warning( |
234 | + 'Exception while running cleanup function: {}' |
235 | + .format(str(err))) |
236 | + self.functions.remove(function) |
237 | + |
238 | + def _clean_command(self, command): |
239 | + """Run clean command. |
240 | + |
241 | + :param command: A command as would be passed to `subprocess.Popen` |
242 | + :type command: iterable |
243 | + |
244 | + """ |
245 | + # All exceptions are captured and logged to make sure the cleanup |
246 | + # process is completed as much as possible |
247 | + try: |
248 | + ProcessRunner(command) |
249 | + except: |
250 | + self.logger.error('Cleanup error: {}' |
251 | + .format(traceback.format_exc())) |
252 | + finally: |
253 | + self.commands.remove(command) |
254 | + |
255 | + def _clean_path(self, path): |
256 | + """Clean path. |
257 | + |
258 | + :param path: Path to a link, file or directory. |
259 | + :type path: str |
260 | + |
261 | + """ |
262 | + if os.path.islink(path): |
263 | + self.logger.debug('Removing link ' + path) |
264 | + os.unlink(path) |
265 | + elif os.path.isfile(path): |
266 | + self._clean_file(path) |
267 | + elif os.path.isdir(path): |
268 | + self._clean_dir(path) |
269 | + else: |
270 | + self.logger.debug( |
271 | + '{} is not a link, file, or directory; not removing' |
272 | + .format(path)) |
273 | + self.paths.remove(path) |
274 | + |
275 | + def _clean_file(self, path): |
276 | + """Clean file. |
277 | + |
278 | + :param path: Path to a file. |
279 | + :type path: str |
280 | + |
281 | + """ |
282 | + self.logger.debug('Changing permissions of ' + path) |
283 | + try: |
284 | + os.chmod(path, 0664) |
285 | + except OSError as err: |
286 | + self.logger.warning( |
287 | + 'OSError when changing file permissions: {}' |
288 | + .format(str(err))) |
289 | + self.logger.debug('Removing file ' + path) |
290 | + try: |
291 | + os.unlink(path) |
292 | + except OSError as err: |
293 | + self.logger.warning('OSError when removing file: {}' |
294 | + .format(str(err))) |
295 | + |
296 | + def _clean_dir(self, path): |
297 | + """Clean directory. |
298 | + |
299 | + :param path: Path to a directory. |
300 | + :type paty: str |
301 | + |
302 | + """ |
303 | + # Cribbed from http://svn.python.org |
304 | + # /projects/python/trunk/Mac/BuildScript/build-installer.py |
305 | + for dirpath, dirnames, filenames in os.walk(path): |
306 | + for name in (dirnames + filenames): |
307 | + absolute_name = os.path.join(dirpath, name) |
308 | + if not os.path.islink(absolute_name): |
309 | + try: |
310 | + os.chmod(absolute_name, 0775) |
311 | + except OSError as err: |
312 | + self.logger.warning( |
313 | + 'OSError when changing directory permissions: {}' |
314 | + .format(str(err))) |
315 | + self.logger.debug('Recursively Removing directory {}'.format(path)) |
316 | + try: |
317 | + shutil.rmtree(path) |
318 | + except OSError as err: |
319 | + self.logger.warning('OSError when removing directory: {}' |
320 | + .format(str(err))) |
321 | + |
322 | + def add_path(self, path): |
323 | + """Register a path to be cleaned later. |
324 | + |
325 | + The way to clean different paths is as follows: |
326 | + - Links will be unlinked. |
327 | + - Files will have permissions changed to 664 and unlinked. |
328 | + - Directories will recursively have permissions changed to 775 |
329 | + (except for links contained within) and then be recursively |
330 | + removed. |
331 | + |
332 | + :param path: A link, file, or directory to be cleaned later. |
333 | + :type path: str |
334 | + |
335 | + """ |
336 | + self.paths.add(path) |
337 | + |
338 | + def add_function(self, timeout, function, *args, **kw): |
339 | + """Register a function to be run on cleanup. |
340 | + |
341 | + The function will be run through :func:`utah.timeout.timeout`. |
342 | + |
343 | + :param timeout: Timeout in seconds for the function to return a result. |
344 | + :type timeout: int |
345 | + :param function: A callable that will do some cleanup. |
346 | + :type function: callable |
347 | + :param args: Positional arguments to the function. |
348 | + :type args: Tuple |
349 | + :param kw: Keyword arguments to the function. |
350 | + :type args: dict |
351 | + |
352 | + """ |
353 | + self.functions.add((timeout, function, args, HashableDict(kw))) |
354 | + |
355 | + def add_command(self, cmd): |
356 | + """Register a command to be run on cleanup. |
357 | + |
358 | + :param cmd: A command as would be passed `subprocess.Popen`. |
359 | + :type cmd: iterable |
360 | + |
361 | + """ |
362 | + self.commands.add(cmd) |
363 | + |
364 | +#: Singleton object used to cleanup everything |
365 | +cleanup = _Cleanup() |
366 | + |
367 | +# Make sure cleanup is executed even if it's not explicitly called |
368 | +atexit.register(cleanup.run) |
369 | |
370 | === modified file 'utah/process.py' |
371 | --- utah/process.py 2012-12-08 02:10:12 +0000 |
372 | +++ utah/process.py 2013-03-29 09:03:24 +0000 |
373 | @@ -13,16 +13,17 @@ |
374 | # You should have received a copy of the GNU General Public License along |
375 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
376 | |
377 | -""" |
378 | -Process checking utilities |
379 | -""" |
380 | +"""Process related utilities.""" |
381 | +import logging |
382 | +import subprocess |
383 | + |
384 | import psutil |
385 | |
386 | |
387 | class ProcessChecker(object): |
388 | - """ |
389 | - Check all running process looking for a given pattern |
390 | - """ |
391 | + |
392 | + """Check all running process looking for a given pattern.""" |
393 | + |
394 | MESSAGE_TEMPLATE = ( |
395 | "{app} application is running.\n" |
396 | "Please stop it before launching utah since " |
397 | @@ -30,8 +31,17 @@ |
398 | "(Kernel-based Virtual Machine)\n") |
399 | |
400 | def check_cmdline(self, pattern): |
401 | - """ |
402 | - Check if the command line of any process matches the given pattern |
403 | + """Check if the command line of any process matches the given pattern. |
404 | + |
405 | + :param pattern: |
406 | + A fragment of the string that should be included in the command |
407 | + line. |
408 | + :type pattern: str |
409 | + :returns: |
410 | + Whether there's a process command line that matches the given |
411 | + pattern. |
412 | + :rtype: bool |
413 | + |
414 | """ |
415 | pids = psutil.get_pid_list() |
416 | cmdlines = [] |
417 | @@ -43,7 +53,42 @@ |
418 | return any(pattern in cmdline for cmdline in cmdlines) |
419 | |
420 | def get_error_message(self, app): |
421 | - """ |
422 | - Return error message for an incompatible application process running |
423 | - """ |
424 | + """Return error message for an incompatible application running.""" |
425 | return self.MESSAGE_TEMPLATE.format(app=app) |
426 | + |
427 | + |
428 | +class ProcessRunner(object): |
429 | + |
430 | + """Run a command in a subprocess and log output properly. |
431 | + |
432 | + :param arglist: Argument list as would be passsed to `subprocess.Popen` |
433 | + :type arglist: list |
434 | + |
435 | + :attribute output: Command output (both stdout and stderr) |
436 | + :attribute returncode: Command return code |
437 | + |
438 | + .. note:: |
439 | + Process is launched when object is instantiated, there isn't any |
440 | + separate `run` method. |
441 | + |
442 | + """ |
443 | + |
444 | + def __init__(self, arglist): |
445 | + logging.debug('Running command: ' + ' '.join(arglist)) |
446 | + logging.debug('Output follows:') |
447 | + p = subprocess.Popen(arglist, |
448 | + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
449 | + output = [] |
450 | + while p.poll() is None: |
451 | + line = p.stdout.readline().strip() |
452 | + logging.debug(line) |
453 | + output.append(line) |
454 | + |
455 | + if p.returncode == 0: |
456 | + logging.debug('Return code: {}'.format(p.returncode)) |
457 | + else: |
458 | + logging.warning('Command ({}) failed with return code: {}' |
459 | + .format(' '.join(arglist), p.returncode)) |
460 | + |
461 | + self.output = '\n'.join(output) |
462 | + self.returncode = p.returncode |
463 | |
464 | === modified file 'utah/provisioning/baremetal/bamboofeeder.py' |
465 | --- utah/provisioning/baremetal/bamboofeeder.py 2013-03-18 14:35:14 +0000 |
466 | +++ utah/provisioning/baremetal/bamboofeeder.py 2013-03-29 09:03:24 +0000 |
467 | @@ -24,6 +24,7 @@ |
468 | import tempfile |
469 | |
470 | from utah import config |
471 | +from utah.process import ProcessRunner |
472 | from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException |
473 | from utah.provisioning.baremetal.power import PowerMixin |
474 | from utah.provisioning.provisioning import ( |
475 | @@ -105,7 +106,7 @@ |
476 | self.cleanfunction(self._umountimage) |
477 | self.logger.debug('Mounting ' + self.wwwimage + ' at ' + |
478 | self.imagedir) |
479 | - self._runargs(['sudo', 'mount', self.wwwimage, self.imagedir, |
480 | + ProcessRunner(['sudo', 'mount', self.wwwimage, self.imagedir, |
481 | '-o', 'loop', '-o', 'offset=' + str(self.sector * 512), |
482 | '-o', 'uid=' + config.user]) |
483 | |
484 | @@ -168,7 +169,7 @@ |
485 | raise UTAHBMProvisioningException( |
486 | 'Unpacking initrd exited with status: {}'.format(exitstatus)) |
487 | self.logger.debug('Creating uInitrd with mkimage') |
488 | - self._runargs(['mkimage', |
489 | + ProcessRunner(['mkimage', |
490 | '-A', 'arm', |
491 | '-O', 'linux', |
492 | '-T', 'ramdisk', |
493 | @@ -184,7 +185,7 @@ |
494 | Unmount the image after we're done with it." |
495 | """ |
496 | self.logger.info('Unmounting image') |
497 | - self._runargs(['sudo', 'umount', self.imagedir]) |
498 | + ProcessRunner(['sudo', 'umount', self.imagedir]) |
499 | |
500 | def _cmdlinesetup(self, boot=None): |
501 | """ |
502 | @@ -238,7 +239,7 @@ |
503 | '01-' + self.macaddress.replace(':', '-')) |
504 | self.cleancommand(('sudo', 'rm', '-f', pxefile)) |
505 | self.logger.debug('Copying ' + tmppxefile + ' to ' + pxefile) |
506 | - self._runargs(['sudo', 'cp', tmppxefile, pxefile]) |
507 | + ProcessRunner(['sudo', 'cp', tmppxefile, pxefile]) |
508 | preenvfile = os.path.join(config.wwwdir, self.name + '.preEnv') |
509 | # TODO: sort this out with self.boot |
510 | # figure out which one should be what and customizable |
511 | @@ -307,5 +308,5 @@ |
512 | """ |
513 | super(BambooFeederMachine, self)._depcheck() |
514 | cmd = ['which', 'mkimage'] |
515 | - if self._runargs(cmd)['returncode'] != 0: |
516 | + if ProcessRunner(cmd).returncode != 0: |
517 | raise UTAHBMProvisioningException('u-boot-tools not installed') |
518 | |
519 | === modified file 'utah/provisioning/baremetal/cobbler.py' |
520 | --- utah/provisioning/baremetal/cobbler.py 2013-03-28 03:45:47 +0000 |
521 | +++ utah/provisioning/baremetal/cobbler.py 2013-03-29 09:03:24 +0000 |
522 | @@ -23,6 +23,7 @@ |
523 | import tempfile |
524 | |
525 | from utah import config |
526 | +from utah.process import ProcessRunner |
527 | from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException |
528 | from utah.provisioning.baremetal.power import PowerMixin |
529 | from utah.provisioning.provisioning import ( |
530 | @@ -102,10 +103,10 @@ |
531 | self.cleanfunction(self._removenfs) |
532 | os.makedirs(self.isodir, mode=0775) |
533 | self.logger.debug('Mounting ISO') |
534 | - result = self._runargs(['sudo', 'mount', '-o', 'loop', |
535 | + result = ProcessRunner(['sudo', 'mount', '-o', 'loop', |
536 | self.image.image, |
537 | self.isodir]) |
538 | - returncode = result['returncode'] |
539 | + returncode = result.returncode |
540 | if returncode != 0: |
541 | raise UTAHBMProvisioningException( |
542 | 'Mount failed with return code: {} Output: {}' |
543 | @@ -170,7 +171,7 @@ |
544 | raise UTAHBMProvisioningException( |
545 | 'Failed to setup NFS: {}'.format(err)) |
546 | self.logger.debug('Reloading NFS config') |
547 | - self._runargs(config.nfscommand + ['reload']) |
548 | + ProcessRunner(config.nfscommand + ['reload']) |
549 | self.logger.debug('Adding NFS boot options') |
550 | ip = self._ipaddr(config.nfsiface) |
551 | self.cmdline += (' root=/dev/nfs netboot=nfs nfsroot=' + |
552 | @@ -227,7 +228,7 @@ |
553 | tail_process = subprocess.Popen( |
554 | ['tail', '-n0', '-f', '/var/log/cobbler/cobbler.log'], |
555 | stdout=subprocess.PIPE) |
556 | - command_results = self._runargs(['sudo', 'cobbler'] + cmd) |
557 | + command_results = ProcessRunner(['sudo', 'cobbler'] + cmd) |
558 | retcode = command_results['returncode'] |
559 | if retcode != 0: |
560 | error_msg = 'Cobbler command failed: ' + ' '.join(cmd) |
561 | @@ -259,7 +260,7 @@ |
562 | if retcode != 0: |
563 | self.logger.info('Cobbler power command failed; falling back to ' |
564 | 'manual command') |
565 | - self._runargs(self.powercommand() + ['on']) |
566 | + ProcessRunner(self.powercommand() + ['on']) |
567 | self.active = True |
568 | |
569 | def stop(self): |
570 | @@ -273,7 +274,7 @@ |
571 | if retcode != 0: |
572 | self.logger.info('Cobbler power command failed; falling back to ' |
573 | 'manual command') |
574 | - self._runargs(self.powercommand() + ['off']) |
575 | + ProcessRunner(self.powercommand() + ['off']) |
576 | self.active = False |
577 | |
578 | def _removenfs(self): |
579 | @@ -282,12 +283,12 @@ |
580 | """ |
581 | if self.isodir is not None: |
582 | self.logger.info('Removing NFS share') |
583 | - self._runargs(['sudo', 'sed', '/' + self.isodir.replace('/', '\/') |
584 | + ProcessRunner(['sudo', 'sed', '/' + self.isodir.replace('/', '\/') |
585 | + '/d', '-i', config.nfsconfigfile]) |
586 | self.logger.debug('Reloading NFS config') |
587 | - self._runargs(config.nfscommand + ['reload']) |
588 | + ProcessRunner(config.nfscommand + ['reload']) |
589 | self.logger.info('Unmounting ISO') |
590 | - self._runargs(['sudo', 'umount', self.isodir]) |
591 | + ProcessRunner(['sudo', 'umount', self.isodir]) |
592 | self.logger.debug('Removing directory') |
593 | os.rmdir(self.isodir) |
594 | self.isodir = None |
595 | @@ -299,7 +300,7 @@ |
596 | super(CobblerMachine, self)._depcheck() |
597 | if self.installtype in ['alternate', 'desktop', 'server']: |
598 | cmd = config.nfscommand + ['status'] |
599 | - if self._runargs(cmd)['returncode'] != 0: |
600 | + if ProcessRunner(cmd).returncode != 0: |
601 | raise UTAHBMProvisioningException( |
602 | 'NFS needed for {} install'.format(self.installtype)) |
603 | if not os.path.isfile(config.nfsconfigfile): |
604 | |
605 | === modified file 'utah/provisioning/baremetal/power.py' |
606 | --- utah/provisioning/baremetal/power.py 2012-12-11 09:37:12 +0000 |
607 | +++ utah/provisioning/baremetal/power.py 2013-03-29 09:03:24 +0000 |
608 | @@ -21,6 +21,7 @@ |
609 | import time |
610 | |
611 | from utah import config |
612 | +from utah.process import ProcessRunner |
613 | from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException |
614 | |
615 | |
616 | @@ -70,14 +71,14 @@ |
617 | Use the machine's powercommand to start the machine. |
618 | """ |
619 | self.logger.debug('Starting system') |
620 | - self._runargs(self.powercommand() + ['on']) |
621 | + ProcessRunner(self.powercommand() + ['on']) |
622 | |
623 | def stop(self): |
624 | """ |
625 | Use the machine's powercommand to stop the machine. |
626 | """ |
627 | self.logger.debug('Stopping system') |
628 | - self._runargs(self.powercommand() + ['off']) |
629 | + ProcessRunner(self.powercommand() + ['off']) |
630 | |
631 | def restart(self): |
632 | """ |
633 | |
634 | === modified file 'utah/provisioning/provisioning.py' |
635 | --- utah/provisioning/provisioning.py 2013-03-18 14:35:14 +0000 |
636 | +++ utah/provisioning/provisioning.py 2013-03-29 09:03:24 +0000 |
637 | @@ -38,12 +38,9 @@ |
638 | import utah.timeout |
639 | |
640 | from utah import config |
641 | -from utah.commandstr import commandstr |
642 | +from utah.cleanup import cleanup |
643 | +from utah.process import ProcessRunner |
644 | from utah.iso import ISO |
645 | -from utah.orderedcollections import ( |
646 | - HashableDict, |
647 | - OrderedSet, |
648 | -) |
649 | from utah.preseed import Preseed |
650 | from utah.provisioning.rsyslog import RSyslog |
651 | from utah.provisioning.exceptions import UTAHProvisioningException |
652 | @@ -128,10 +125,6 @@ |
653 | else: |
654 | self.clean = clean |
655 | |
656 | - self.cleanfiles = OrderedSet() |
657 | - self.cleanfunctions = OrderedSet() |
658 | - self.cleancommands = OrderedSet() |
659 | - |
660 | self._namesetup(name) |
661 | self._dirsetup(directory) |
662 | |
663 | @@ -317,7 +310,7 @@ |
664 | """ |
665 | self.logger.info('Checking network connectivity (ping)') |
666 | returncode = \ |
667 | - self._runargs(['ping', '-c1', '-w5', self.name])['returncode'] |
668 | + ProcessRunner(['ping', '-c1', '-w5', self.name]).returncode |
669 | if returncode != 0: |
670 | err = 'Ping returned {0}'.format(returncode) |
671 | raise UTAHProvisioningException(err, retry=True) |
672 | @@ -446,8 +439,6 @@ |
673 | Destroying the install on a physical machine is optional. |
674 | """ |
675 | self.provisioned = False |
676 | - self.__del__() |
677 | - del self |
678 | return True |
679 | |
680 | def _load(self): |
681 | @@ -582,106 +573,45 @@ |
682 | .format(cls=self.__class__.__name__, |
683 | method=method)) |
684 | |
685 | - def cleanup(self): |
686 | - """ |
687 | - Clean up temporary files and tear down other setup (VM, etc.). |
688 | - If a subset is specified, only that subset will be cleaned. |
689 | - This can be used to clean up install files after the install has |
690 | - started. |
691 | - Files are removed first. |
692 | - Functions are run second. |
693 | - Commands are run third. |
694 | - """ |
695 | - if self.clean: |
696 | - self.logger.debug('Running cleanup') |
697 | - for function in self.cleanfunctions: |
698 | - timeout, command, args, kw = function |
699 | - self.logger.debug('Running: ' + |
700 | - commandstr(command, *args, **kw)) |
701 | - try: |
702 | - utah.timeout.timeout(timeout, command, *args, **kw) |
703 | - except Exception as err: |
704 | - self.logger.warning('Exception while running cleanup ' |
705 | - 'function: {}'.format(str(err))) |
706 | - self.cleanfunctions.remove(function) |
707 | - for command in self.cleancommands: |
708 | - self._runargs(command) |
709 | - self.cleancommands.remove(command) |
710 | - for path in self.cleanfiles: |
711 | - if os.path.islink(path): |
712 | - self.logger.debug('Removing link ' + path) |
713 | - os.unlink(path) |
714 | - elif os.path.isfile(path): |
715 | - self.logger.debug('Changing permissions of ' + path) |
716 | - try: |
717 | - os.chmod(path, 0664) |
718 | - except OSError as err: |
719 | - self.logger.warning('OSError when changing file ' |
720 | - 'permissions: {}' |
721 | - .format(str(err))) |
722 | - self.logger.debug('Removing file ' + path) |
723 | - try: |
724 | - os.unlink(path) |
725 | - except OSError as err: |
726 | - self.logger.warning('OSError when removing file: {}' |
727 | - .format(str(err))) |
728 | - elif os.path.isdir(path): |
729 | - # Cribbed from http://svn.python.org |
730 | - # /projects/python/trunk/Mac/BuildScript/build-installer.py |
731 | - for dirpath, dirnames, filenames in os.walk(path): |
732 | - for name in (dirnames + filenames): |
733 | - absolute_name = os.path.join(dirpath, name) |
734 | - if not os.path.islink(absolute_name): |
735 | - self.logger.debug('Changing permissions of ' |
736 | - + absolute_name) |
737 | - try: |
738 | - os.chmod(absolute_name, 0775) |
739 | - except OSError as err: |
740 | - self.logger.warning( |
741 | - 'OSError when ' |
742 | - 'changing directory permissions: {}' |
743 | - .format(str(err))) |
744 | - self.logger.debug('Recursively Removing directory ' |
745 | - + path) |
746 | - try: |
747 | - shutil.rmtree(path) |
748 | - except OSError as err: |
749 | - self.logger.warning('OSError when removing ' |
750 | - 'directory: {}' |
751 | - .format(str(err))) |
752 | - else: |
753 | - self.logger.debug(path + ' is not a link, file, or ' |
754 | - 'directory; not removing') |
755 | - self.cleanfiles.remove(path) |
756 | - else: |
757 | - self.logger.debug('Not cleaning up') |
758 | - |
759 | def cleanfile(self, path): |
760 | - """ |
761 | - Register a link, file, or directory to be cleaned later. |
762 | - Links will be unlinked. |
763 | - Files will have permissions changed to 664 and unlinked. |
764 | - Directories will recursively have permissions changed to 775 (except |
765 | - for links contained within) and then be recursively removed. |
766 | - """ |
767 | - self.cleanfiles.add(path) |
768 | + """Register a path to be cleaned later. |
769 | + |
770 | + :param path: A link, file, or directory to be cleaned later. |
771 | + :type path: str |
772 | + |
773 | + .. seealso:: :meth:`utah.cleanup._Cleanup.add_path` |
774 | + |
775 | + """ |
776 | + if self.clean: |
777 | + cleanup.add_path(path) |
778 | |
779 | def cleanfunction(self, function, *args, **kw): |
780 | - """ |
781 | - Register a function to be run on cleanup. |
782 | - Functions are run through utah.timeout.timeout. |
783 | - """ |
784 | - self.cleanfunctions.add((60, function, args, HashableDict(kw))) |
785 | + """Register a function to be run on cleanup. |
786 | + |
787 | + :param function: A callable that will do some cleanup. |
788 | + :type function: callable |
789 | + :param args: Positional arguments to the function. |
790 | + :type args: Tuple |
791 | + :param kw: Keyword arguments to the function. |
792 | + :type args: dict |
793 | + |
794 | + .. seealso:: :meth:`utah.cleanup._Cleanup.add_function` |
795 | + |
796 | + """ |
797 | + if self.clean: |
798 | + cleanup.add_function(60, function, args, kw) |
799 | |
800 | def cleancommand(self, cmd): |
801 | - """ |
802 | - Register a command to be run on cleanup. |
803 | - Commands are run through Machine._runargs. |
804 | - """ |
805 | - self.cleancommands.add(cmd) |
806 | - |
807 | - def __del__(self): |
808 | - self.cleanup() |
809 | + """Register a command to be run on cleanup. |
810 | + |
811 | + :param cmd: A command as would be passed `subprocess.Popen`. |
812 | + :type cmd: iterable |
813 | + |
814 | + .. seealso:: :meth:`utah.cleanup._Cleanup.add_command` |
815 | + |
816 | + """ |
817 | + if self.clean: |
818 | + cleanup.add_command(cmd) |
819 | |
820 | def _uuid_check(self): |
821 | uuid_check_command = ('[ "{uuid}" == "$(cat /etc/utah/uuid)" ]' |
822 | @@ -956,12 +886,12 @@ |
823 | os.chmod(filename, 0755) |
824 | |
825 | orderfilename = os.path.join(casper_dir, 'ORDER') |
826 | - exitstatus = self._runargs( |
827 | + exitstatus = ProcessRunner( |
828 | ['sed', '-i', |
829 | '1i/scripts/casper-bottom/' |
830 | 'utah\\n' |
831 | '[ -e /conf/param.conf ] && . /conf/param.conf', |
832 | - orderfilename])['returncode'] |
833 | + orderfilename]).returncode |
834 | if exitstatus != 0: |
835 | raise UTAHProvisioningException('Failed to setup casper script') |
836 | |
837 | |
838 | === modified file 'utah/provisioning/vm/vm.py' |
839 | --- utah/provisioning/vm/vm.py 2013-03-05 21:03:40 +0000 |
840 | +++ utah/provisioning/vm/vm.py 2013-03-29 09:03:24 +0000 |
841 | @@ -28,7 +28,7 @@ |
842 | from xml.etree import ElementTree |
843 | |
844 | from utah import config |
845 | -from utah.process import ProcessChecker |
846 | +from utah.process import ProcessChecker, ProcessRunner |
847 | from utah.provisioning.inventory.sqlite import SQLiteInventory |
848 | from utah.provisioning.provisioning import CustomInstallMixin, Machine |
849 | from utah.provisioning.rsyslog import RSyslog |
850 | @@ -266,7 +266,7 @@ |
851 | cmd = ['qemu-img', 'create', '-f', 'qcow2', diskfile, disksize] |
852 | self.logger.debug('Creating ' + disksize + ' disk using:') |
853 | self.logger.debug(' '.join(cmd)) |
854 | - if self._runargs(cmd)['returncode'] != 0: |
855 | + if ProcessRunner(cmd).returncode != 0: |
856 | raise UTAHVMProvisioningException( |
857 | 'Could not create disk image at ' + diskfile) |
858 | disk = {'bus': self.diskbus, |
859 | @@ -500,6 +500,7 @@ |
860 | self.logger.info('Creating custom virtual machine') |
861 | |
862 | tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_') |
863 | + self.cleanfile(tmpdir) |
864 | self.logger.debug('Working dir: ' + tmpdir) |
865 | os.chdir(tmpdir) |
866 | |
867 | @@ -536,7 +537,6 @@ |
868 | self.logger.info('Setting up final VM') |
869 | self.vm = self.lv.defineXML(ElementTree.tostring(xml.getroot())) |
870 | |
871 | - self.cleanfile(tmpdir) |
872 | return True |
873 | |
874 | def _start(self): |
875 | |
876 | === modified file 'utah/url.py' |
877 | --- utah/url.py 2013-03-13 16:30:11 +0000 |
878 | +++ utah/url.py 2013-03-29 09:03:24 +0000 |
879 | @@ -31,6 +31,7 @@ |
880 | import bzrlib.plugin |
881 | import bzrlib.errors |
882 | |
883 | +from utah.cleanup import cleanup |
884 | from utah.exceptions import UTAHException |
885 | from utah.timeout import timeout |
886 | from utah.retry import retry |
887 | @@ -214,6 +215,7 @@ |
888 | cmd = bzrlib.builtins.cmd_export() |
889 | assert cmd is not None |
890 | tmp_dir = tempfile.mkdtemp(prefix='utah_') |
891 | + cleanup.add_path(tmp_dir) |
892 | |
893 | def bzr_export_retriable(): |
894 | """bzr export a URL retrying on http errors |
This reiterates to me the need to clean up the run scripts. It's causing everybody who's not me lots of extra work.
Should there be any exception handling associated with ProcessRunner? Even if ProcessRunner itself isn't handling exceptions, I think maybe the cleanup call should have it in a try block so that one failed process run won't spoil the whole cleanup process.
Maybe this is needless indirection, but would it make any sense to set Machine._runargs to ProcessRunner? Then, we don't have to change every instance of that, and, if for some unholy reason a Machine subclass wants to define its own ProcessRunner, it can? Since you've already done the hard part of replacing all the _runargs bits, maybe this is superfluous.
Also, I'd still like the Machine class to at least try to call cleanup when it's destroyed. Right now, it looks to me like the only time a Machine ever cleans up is when a run script specifically tells it to clean up. I think the Machine classes should be more self-sufficient than that, and I do sometimes use them directly. bootspeed also uses them directly right now (although we'd like that to go away eventually.) If we implemented this without adding explicit cleanup command to the bootspeed script, we'd start leaking resources. Maybe using atexit in the Cleanup class would work? I also wonder about what happens if we're trying to, in the future, for some weird reason, manage multiple Machines in the same script. Being able to clean them up separately might be nice, but it's not really strictly necessary. I think in that case, an atexit-based solution would be better than something in the Machine class destructor, unless we want each Machine object to have its own Cleanup object. That's also a possibility, but I'm not sure what we'd gain from it.