Merge lp:~javier.collado/utah/bug1101186 into lp:utah

Proposed by Javier Collado
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
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Max Brustkern (community) Approve
Javier Collado (community) Needs Resubmitting
Review via email: mp+155947@code.launchpad.net

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.Process runner (needed by cleanup)

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.

To post a comment you must log in.
Revision history for this message
Max Brustkern (nuclearbob) wrote :

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.

Revision history for this message
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.

Revision history for this message
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.

lp:~javier.collado/utah/bug1101186 updated
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

Revision history for this message
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.

review: Needs Resubmitting
Revision history for this message
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.

review: Approve
Revision history for this message
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.check_call(['rm', '-r', '-f', dir]) is looking a lot more desirable to me.

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.

Revision history for this message
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.check_call(['rm', '-r', '-f', dir]) is looking a lot more 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.
>
> 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.

Revision history for this message
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.check_call(['rm', '-r', '-f', dir]) is looking a lot more 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.

>> 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.

Revision history for this message
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.check_call(['rm', '-r', '-f', dir]) is looking a lot more
>>> 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

Revision history for this message
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.

lp:~javier.collado/utah/bug1101186 updated
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

Revision history for this message
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://pypi.python.org/pypi/pep257/) which includes the nice -e/--explain
  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.

Revision history for this message
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://www.python.org/dev/peps/pep-0257/)
and when I write pep257 I refer to the command line tool
(https://pypi.python.org/pypi/pep257/).

Revision history for this message
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://www.python.org/dev/peps/pep-0257/)
> and when I write pep257 I refer to the command line tool
> (https://pypi.python.org/pypi/pep257/).

That was clear to me. In any case, I think this looks good.

review: Approve
Revision history for this message
Andy Doan (doanac) wrote :

+1 - this branch has some things I've been wanting or a long time :)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches