Merge lp:~nuclearbob/utah/rsyslog-client into lp:utah

Proposed by Max Brustkern
Status: Merged
Merged at revision: 870
Proposed branch: lp:~nuclearbob/utah/rsyslog-client
Merge into: lp:utah
Diff against target: 286 lines (+78/-11)
6 files modified
client.py (+9/-3)
debian/changelog (+3/-0)
utah/client/common.py (+50/-8)
utah/client/runner.py (+7/-0)
utah/client/testcase.py (+5/-0)
utah/client/testsuite.py (+4/-0)
To merge this branch: bzr merge lp:~nuclearbob/utah/rsyslog-client
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Max Brustkern (community) Needs Resubmitting
Review via email: mp+159015@code.launchpad.net

Description of the change

This branch adds syslog output on the LOCAL0 facility to the client. I'd like to add server-side code to use and process this output, but that's not done yet, and I'd like to get some other thoughts on the client-side design and implementation before I proceed.

To post a comment you must log in.
lp:~nuclearbob/utah/rsyslog-client updated
862. By Max Brustkern

Merged iso squashfs validation update

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

On 04/15/2013 03:49 PM, Max Brustkern wrote:

> https://code.launchpad.net/~nuclearbob/utah/rsyslog-client/+merge/159015

> === modified file 'client.py'
> if not os.getuid() == 0:
> parser.print_usage()
> - sys.stderr.write('{}: UTAH client is expected '
> - 'to be executed as root user\n'
> - .format(parser.prog))
> + msg = ('{}: UTAH client is expected to be executed as root user\n'
> + .format(parser.prog))
> + sys.stderr.write(msg)
> + syslog.syslog(syslog.LOG_ERR, msg)

is the "\n" going to make the syslog stuff look/act weird?

> === modified file 'utah/client/common.py'
> --- utah/client/common.py 2013-04-08 19:16:05 +0000

> - p = subprocess.Popen("{} {}".format(cmd_prefix, command),
> - shell=True, cwd=cwd,
> - stdout=subprocess.PIPE, stderr=subprocess.PIPE)
> + p = subprocess.Popen(cmd, shell=True, cwd=cwd, stdout=subprocess.PIPE,
> + stderr=subprocess.PIPE)
>
> if timeout != 0:
> signal.signal(signal.SIGALRM, alarm_handler)
> signal.alarm(timeout)
>
> + stdout = []
> + stderr = []

This seems inefficient. Why not use cStringIO? Then you won't have to do
the '\n'.join(...) at the end of the function.

> try:
> - stdout, stderr = p.communicate()
> - stdout = _normalize_encoding(stdout)
> - stderr = _normalize_encoding(stderr)
> + while p.poll() is None:

Is this a safe way to deal with a process? The concern I have is that
stdout might be blocked, but there is data on stderr that needs to be
read and we sit blocked? I think we might want to make this a "select"
type look on stdout/stderr fd's.

> + stdout_tmp = _normalize_encoding(p.stdout.readline().strip())
> + stderr_tmp = _normalize_encoding(p.stderr.readline().strip())
> + syslog.syslog(stdout_tmp)
> + syslog.syslog(stderr_tmp)
> + stdout.append(stdout_tmp)
> + stderr.append(stderr_tmp)
>
> if timeout != 0:
> signal.alarm(0)

Revision history for this message
Max Brustkern (nuclearbob) wrote :

> On 04/15/2013 03:49 PM, Max Brustkern wrote:
>
> ...
>
> is the "\n" going to make the syslog stuff look/act weird?

Not sure, but I can check.

>
> ...
>
> This seems inefficient. Why not use cStringIO? Then you won't have to do
> the '\n'.join(...) at the end of the function.

The only time I tried to use cStringIO it turned into a horrible mess that didn't work, so I haven't really looked at it again since then. If you think it's a good fit for here, I think something better than what I've got would be nice. I can try that.

>
> > try:
> > - stdout, stderr = p.communicate()
> > - stdout = _normalize_encoding(stdout)
> > - stderr = _normalize_encoding(stderr)
> > + while p.poll() is None:
>
> Is this a safe way to deal with a process? The concern I have is that
> stdout might be blocked, but there is data on stderr that needs to be
> read and we sit blocked? I think we might want to make this a "select"
> type look on stdout/stderr fd's.

I should look up the "select" thing, since I'm not familiar with it. I'm learning lots of things today! We used to do basically this as part of the vm-tools integration, and it worked fine, but that's no guarantee it couldn't fail here.

lp:~nuclearbob/utah/rsyslog-client updated
863. By Javier Collado

Merged documentation string fixes

Source branch: lp:~javier.collado/utah/documentation_fixes

864. By Javier Collado

Merged changes to validate master runlist before provisioning (LP: #1082087)

Source branch: lp:~javier.collado/utah/bug1082087

Revision history for this message
Max Brustkern (nuclearbob) wrote :

> On 04/15/2013 03:49 PM, Max Brustkern wrote:
>
> > https://code.launchpad.net/~nuclearbob/utah/rsyslog-client/+merge/159015
>
> > === modified file 'client.py'
> > if not os.getuid() == 0:
> > parser.print_usage()
> > - sys.stderr.write('{}: UTAH client is expected '
> > - 'to be executed as root user\n'
> > - .format(parser.prog))
> > + msg = ('{}: UTAH client is expected to be executed as root user\n'
> > + .format(parser.prog))
> > + sys.stderr.write(msg)
> > + syslog.syslog(syslog.LOG_ERR, msg)
>
> is the "\n" going to make the syslog stuff look/act weird?
>
It looks okay to me:
Apr 17 09:09:42 eden utah: client.py: UTAH client is expected to be executed as root user
Apr 17 09:09:48 eden utah: client.py: UTAH client is expected to be executed as root user
Apr 17 09:10:04 eden utah: Fetching testsuite: utah_tests_sample

I'm going to work on the other things.

Revision history for this message
Max Brustkern (nuclearbob) wrote :

I switched to StringIO and pushed that. As for subprocess stuff, what do you think about something like this:
http://stackoverflow.com/questions/375427/non-blocking-read-on-a-subprocess-pipe-in-python

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

On 04/17/2013 12:45 PM, Max Brustkern wrote:
> As for subprocess stuff, what do you think about something like this:
> http://stackoverflow.com/questions/375427/non-blocking-read-on-a-subprocess-pipe-in-python

that stinks. in C a pipe is normal FD and you can do select stuff. I
guess threads are the way to go.

lp:~nuclearbob/utah/rsyslog-client updated
865. By Javier Collado

Fix broken build

The package build is broken because a weird exception:
RuntimeError: sys.path must be a list of directory names

Looking in python source code I see this change:
http://hg.python.org/cpython/rev/c9bb16b7d148

that indeed changes what it was an ImportError to a RuntimeError. However, it's
not clear why PyList_Check(path) is failing because I'm pretty sure that
sys.path is a list (I even tried to filter it with os.path.isdir). Hence, I
guess that path in that code isn't really sys.path under some circumstances.

Anyway, catching that exception and handling it the same way as ImportError
works fine.

Revision history for this message
Max Brustkern (nuclearbob) wrote :

I implemented the thread and queue method and tested it. The script I use always sends stdout before stderr, but in the output if this, sometimes the stderr comes before the stdout. I don't know how big of a problem that is, or whether there's a reasonable thing to do about it that doesn't potentially allow one buffer to fill up while waiting on the other one. The stdout and stderr in the yaml at the end do come out right.

review: Needs Resubmitting
lp:~nuclearbob/utah/rsyslog-client updated
875. By Max Brustkern

Using StringIO instead of arrays

876. By Max Brustkern

Using threads and queues to read process output; this seems like something we could use a generalized function, but I don't see anywhere else we need it right now

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'client.py'
2--- client.py 2013-04-04 13:13:34 +0000
3+++ client.py 2013-04-18 18:26:24 +0000
4@@ -21,6 +21,7 @@
5 import logging
6 import platform
7 import sys
8+import syslog
9 import os
10
11 from utah.url import url_argument
12@@ -88,6 +89,8 @@
13
14
15 def main():
16+ syslog.openlog('utah', 0, syslog.LOG_LOCAL0)
17+
18 """Interpret command line arguments and run tests."""
19 # TODO: write <2.7 optparse version and set based on version of python
20 # being used.
21@@ -96,9 +99,10 @@
22
23 if not os.getuid() == 0:
24 parser.print_usage()
25- sys.stderr.write('{}: UTAH client is expected '
26- 'to be executed as root user\n'
27- .format(parser.prog))
28+ msg = ('{}: UTAH client is expected to be executed as root user\n'
29+ .format(parser.prog))
30+ sys.stderr.write(msg)
31+ syslog.syslog(syslog.LOG_ERR, msg)
32 sys.exit(ReturnCodes.INVALID_USER)
33
34 setup_logging(args.debug)
35@@ -131,6 +135,7 @@
36 sys.stdout = open(output, open_flags)
37 except IOError as e:
38 sys.stderr.write(str(e))
39+ syslog.syslog(syslog.LOG_ERR, str(e))
40 sys.exit(ReturnCodes.EXCEPTION_ERROR)
41 if args.format == 'text':
42 result_class = Result
43@@ -152,6 +157,7 @@
44
45 except UTAHClientError as e:
46 sys.stderr.write(str(e))
47+ syslog.syslog(syslog.LOG_ERR, str(e))
48 sys.exit(ReturnCodes.EXCEPTION_ERROR)
49
50 sys.exit(returncode)
51
52=== modified file 'debian/changelog'
53--- debian/changelog 2013-04-17 08:26:37 +0000
54+++ debian/changelog 2013-04-18 18:26:24 +0000
55@@ -4,6 +4,9 @@
56 * Check squashfs on the server images as well during ISO static
57 validation.
58
59+ [ Max Brustkern ]
60+ * Added syslog output (including real time test output) to client
61+
62 -- Max Brustkern <max@canonical.com> Tue, 16 Apr 2013 09:21:19 -0400
63
64 utah (0.10ubuntu1) UNRELEASED; urgency=low
65
66=== modified file 'utah/client/common.py'
67--- utah/client/common.py 2013-04-08 19:16:05 +0000
68+++ utah/client/common.py 2013-04-18 18:26:24 +0000
69@@ -24,8 +24,13 @@
70 import re
71 import signal
72 import subprocess
73+import syslog
74 import time
75
76+from Queue import Queue, Empty
77+from StringIO import StringIO
78+from threading import Thread
79+
80 import jsonschema
81 import yaml
82
83@@ -108,6 +113,14 @@
84 """
85
86
87+# Process queue stuff inspired by:
88+# http://stackoverflow.com/a/4896288
89+def enqueue_output(out, queue):
90+ for line in iter(out.readline, b''):
91+ queue.put(line)
92+ out.close()
93+
94+
95 # Inspired by:
96 # http://stackoverflow.com/a/3326559
97 def run_cmd(command, cwd=None, timeout=0, cmd_type=CMD_TC_TEST, run_as=None,
98@@ -166,19 +179,43 @@
99 if battery_measurements:
100 start_battery = battery.get_data()
101
102+ cmd = '{} {}'.format(cmd_prefix, command)
103+ syslog.syslog('Running command: {}'.format(cmd))
104+
105+ stdout = StringIO()
106+ stderr = StringIO()
107+ outq = Queue()
108+ errq = Queue()
109+
110 start_time = datetime.datetime.now()
111- p = subprocess.Popen("{} {}".format(cmd_prefix, command),
112- shell=True, cwd=cwd,
113- stdout=subprocess.PIPE, stderr=subprocess.PIPE)
114+ p = subprocess.Popen(cmd, shell=True, cwd=cwd, stdout=subprocess.PIPE,
115+ stderr=subprocess.PIPE)
116
117 if timeout != 0:
118 signal.signal(signal.SIGALRM, alarm_handler)
119 signal.alarm(timeout)
120
121+ outt = Thread(target=enqueue_output, args=(p.stdout, outq))
122+ outt.daemon = True
123+ outt.start()
124+ errt = Thread(target=enqueue_output, args=(p.stderr, errq))
125+ errt.daemon = True
126+ errt.start()
127+
128 try:
129- stdout, stderr = p.communicate()
130- stdout = _normalize_encoding(stdout)
131- stderr = _normalize_encoding(stderr)
132+ while p.poll() is None:
133+ try:
134+ stdout_tmp = _normalize_encoding(outq.get_nowait())
135+ syslog.syslog(stdout_tmp)
136+ stdout.write(stdout_tmp)
137+ except Empty:
138+ pass
139+ try:
140+ stderr_tmp = _normalize_encoding(errq.get_nowait())
141+ syslog.syslog(stderr_tmp)
142+ stderr.write(stderr_tmp)
143+ except Empty:
144+ pass
145
146 if timeout != 0:
147 signal.alarm(0)
148@@ -209,10 +246,15 @@
149
150 time_delta = datetime.datetime.now() - start_time
151
152+ if p.returncode:
153+ syslog.syslog(syslog.LOG_ERR,
154+ 'Process exited with non-zero return code: {}'
155+ .format(p.returncode))
156+
157 kwargs = {'command': command,
158 'retcode': p.returncode,
159- 'stdout': stdout,
160- 'stderr': stderr,
161+ 'stdout': stdout.getvalue(),
162+ 'stderr': stderr.getvalue(),
163 'start_time': start_time.strftime(DATE_FORMAT),
164 'time_delta': str(time_delta),
165 'cmd_type': cmd_type,
166
167=== modified file 'utah/client/runner.py'
168--- utah/client/runner.py 2013-04-04 15:04:43 +0000
169+++ utah/client/runner.py 2013-04-18 18:26:24 +0000
170@@ -22,6 +22,7 @@
171 import shutil
172 import stat
173 import subprocess
174+import syslog
175 import urllib
176
177 from utah.client import exceptions
178@@ -308,6 +309,7 @@
179 # Return success despite of the results
180 # to let server know that it has to check the state file
181 # to know the final result after the reboot
182+ syslog.syslog('REBOOT: Test case requested reboot')
183 return ReturnCodes.REBOOT
184
185 self.errors += suite.errors
186@@ -530,6 +532,7 @@
187 return result
188
189 if name not in self.fetched_suites:
190+ syslog.syslog('Fetching testsuite: {}'.format(name))
191 # Retry bzr export on http errors for 60 seconds
192 res = timeout(60, retry, vcs_get_retriable)
193 self.result.add_result(res)
194@@ -623,8 +626,12 @@
195 def returncode(self):
196 """Provide return code based on test status."""
197 if self.errors > 0 or self.fetch_errors > 0:
198+ syslog.syslog(syslog.LOG_ERR,
199+ 'ERROR: Errors encountered during testing')
200 return ReturnCodes.ERROR
201 elif self.failures > 0:
202+ syslog.syslog(syslog.LOG_ERR, 'FAIL: Not all tests passed')
203 return ReturnCodes.FAIL
204 else:
205+ syslog.syslog('PASS: All tests passed')
206 return ReturnCodes.PASS
207
208=== modified file 'utah/client/testcase.py'
209--- utah/client/testcase.py 2013-04-08 19:12:25 +0000
210+++ utah/client/testcase.py 2013-04-18 18:26:24 +0000
211@@ -17,6 +17,7 @@
212
213
214 import os
215+import syslog
216
217 import jsonschema
218
219@@ -170,6 +171,7 @@
220 """Run build, but only if we haven't started a run yet."""
221 if self.status == 'NOTRUN':
222 self.set_status('BUILD')
223+ syslog.syslog('Testcase {} build'.format(self.name))
224 cmd_result = run_cmd(
225 self.build_cmd,
226 cwd=self.working_dir,
227@@ -181,6 +183,7 @@
228 """Run tc_setup, but only if build() has just passed."""
229 if self.status == 'BUILD' and result.status == 'PASS':
230 self.set_status('SETUP')
231+ syslog.syslog('Testcase {} setup'.format(self.name))
232 cmd_result = run_cmd(
233 self.tc_setup,
234 cwd=self.working_dir,
235@@ -192,6 +195,7 @@
236 """Run tc_cleanup after a run."""
237 if self.status == 'RUN':
238 self.set_status('CLEANUP')
239+ syslog.syslog('Testcase {} cleanup'.format(self.name))
240 cmd_result = run_cmd(
241 self.tc_cleanup,
242 cwd=self.working_dir,
243@@ -256,6 +260,7 @@
244 'action': self.action,
245 'expected_results': self.expected_results,
246 }
247+ syslog.syslog('Testcase {} run'.format(self.name))
248 cmd_result = run_cmd(
249 self.command,
250 timeout=timeout,
251
252=== modified file 'utah/client/testsuite.py'
253--- utah/client/testsuite.py 2013-04-04 15:09:51 +0000
254+++ utah/client/testsuite.py 2013-04-18 18:26:24 +0000
255@@ -18,6 +18,7 @@
256
257 import jsonschema
258 import os
259+import syslog
260 import yaml
261
262 from utah.client import exceptions
263@@ -204,6 +205,7 @@
264 """Run build, but only if we haven't started a run yet."""
265 if self.status == 'NOTRUN':
266 self.set_status('BUILD')
267+ syslog.syslog('Testsuite {} build'.format(self.name))
268 cmd_result = run_cmd(
269 self.build_cmd,
270 cwd=self.name,
271@@ -215,6 +217,7 @@
272 """Run ts_setup, but only if build() has just passed."""
273 if self.status == 'BUILD' and result.status == 'PASS':
274 self.set_status('SETUP')
275+ syslog.syslog('Testsuite {} setup'.format(self.name))
276 cmd_result = run_cmd(
277 self.ts_setup,
278 cwd=self.name,
279@@ -226,6 +229,7 @@
280 """Run ts_cleanup after a run."""
281 if self.status == 'INPROGRESS':
282 self.set_status('CLEANUP')
283+ syslog.syslog('Testsuite {} cleanup'.format(self.name))
284 cmd_result = run_cmd(
285 self.ts_cleanup,
286 cwd=self.name,

Subscribers

People subscribed via source and target branches