aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTyler Baker <forcedinductionz@gmail.com>2015-04-15 11:08:54 -0700
committerTyler Baker <forcedinductionz@gmail.com>2015-04-15 11:08:54 -0700
commitaa547b81751d9452664388dc6dc44d8ac2132447 (patch)
tree6c3ea7b31eaf28b966ee5cd4b2e86b2231343df2
parent8da9881ddbde9c5b99942501eb02dd0eccf8bc92 (diff)
parentc0425c787a4d669be86c0d05487a56dc98643019 (diff)
Merge pull request #4 from roxell/develop
Thanks, applied.
-rwxr-xr-xstream-lava-log.py318
-rw-r--r--text_output.py77
2 files changed, 361 insertions, 34 deletions
diff --git a/stream-lava-log.py b/stream-lava-log.py
index 065e118..f3e6e4e 100755
--- a/stream-lava-log.py
+++ b/stream-lava-log.py
@@ -15,24 +15,175 @@
#
# Copyright Tyler Baker 2015
+import os
+import sys
import argparse
import urlparse
+import datetime
import time
import xmlrpclib
-import ConfigParser, os
+import ConfigParser
+import curses
+import re
+
+from text_output import TextBlock
+
+
+class FileOutputHandler(object):
+ def __init__(self, file_obj, outputter):
+ self.file_obj = file_obj
+ self.outputter = outputter
+
+ self.full_output = ""
+ self.printed_output = ""
+
+
+ def run(self):
+ while True:
+ self._update_output()
+ self._print_output()
+
+ if not self.outputter.is_running(): break
+
+ time.sleep(2)
+
+ self.file_obj.write("Job has finished.")
+
+
+ def _update_output(self):
+ self.full_output = self.outputter.get_output()
+
+
+ def _print_output(self):
+ if not self.full_output:
+ self.file_obj.write("No job output...\n")
+
+ new_output = self.full_output[len(self.printed_output):]
+
+ self.file_obj.write(new_output)
+ self.file_obj.flush()
+ self.printed_output = self.full_output
+
+
+class CursesOutput(object):
+ def __init__(self, outputter, follow=True):
+ self.outputter = outputter
+ self.textblock = TextBlock()
+ self.follow = follow
+
+ self.win_height = 0
+ self.win_width = 0
+ self.win_changed = False
+ self.cur_line = 0
+ self.status_win = None
+ self.state_win_height = 2
+ self.output = ""
+ self.text_changed = False
+
+
+ def run(self):
+ curses.wrapper(self._run)
+
+
+ def _run(self, stdscr):
+ self.stdscr = stdscr
+ self._setup_win()
+
+ while True:
+ self._update_win()
+ self._poll_state()
+
+ self._redraw_output()
+ self._redraw_status()
+
+ self._refresh()
+ time.sleep(0.1)
+
+
+ def _setup_win(self):
+ self.win_height, self.win_width = self.stdscr.getmaxyx()
+ self.status_win = curses.newwin(self.state_win_height, self.win_width, self.win_height-self.state_win_height, 0)
+ self.status_win.bkgdset(' ', curses.A_REVERSE)
+ self.textblock.set_width(self.win_width, reflow=False)
+ self.win_changed = True
+
+
+ def _update_win(self):
+ if curses.is_term_resized(self.win_height, self.win_width):
+ self.win_height, self.win_width = self.stdscr.getmaxyx()
+ curses.resizeterm(self.win_height, self.win_width)
+
+ self.status_win.resize(self.state_win_height, self.win_width)
+ self.status_win.mvwin(self.win_height-self.state_win_height, 0)
+
+ self.textblock.set_width(self.win_width, reflow=False)
+
+ self.win_changed = True
+
+
+ def _poll_state(self):
+ old_output_len = len(self.output)
+
+ self.output = self.outputter.get_output()
+
+ if len(self.output) != old_output_len:
+ self.textblock.set_text(self.output, reflow=False)
+ self.text_changed = True
+
+
+ def _redraw_output(self):
+ if self.text_changed or self.win_changed:
+ output_lines = None
+
+ self.textblock.reflow()
+ if self.follow:
+ output_lines = self.textblock.get_block(-1, self.win_height-self.state_win_height)
+ else:
+ output_lines = self.textblock.get_block(self.cur_line, self.win_height-self.state_win_height)
+
+ self.stdscr.clear()
+ self._draw_text(output_lines)
+
+ self.win_changed = False
+ self.text_changed = False
+
+
+ def _redraw_status(self):
+ details = "description: %s" % self.outputter.get_description()
+ details += " device_type: %s" % self.outputter.get_device_type_id()
+ details += " hostname: %s" % self.outputter.get_hostname()
+ self.status_win.addstr(0, 0, details[:self.win_width-1])
+
+ status = "active: %s" % self.outputter.is_running()
+ status += " action: %s" % self.outputter.last_action()
+ self.status_win.addstr(1, 0, status[:self.win_width-1])
+
+
+ def _draw_text(self, lines):
+ for index, line in enumerate(lines):
+ self.stdscr.addstr(index, 0, line)
+
+
+ def _refresh(self):
+ self.stdscr.refresh()
+ self.status_win.refresh()
+
class Config(object):
def __init__(self, config_sources=None):
self.config_sources = config_sources or list()
+
def add_config_override(self, config_source):
self.config_sources.insert(0, config_source)
+
def has_enough_config(self):
return (self.get_config_variable('username') and
self.get_config_variable('token') and
self.get_config_variable('server'))
+
def construct_url(self):
if not self.has_enough_config():
raise Exception("Not enough configuration to construct the URL")
@@ -48,6 +199,7 @@ class Config(object):
self.get_config_variable('token') +
'@' + url.netloc + url.path)
+
def get_config_variable(self, variable_name):
for config_source in self.config_sources:
method_name = 'get_%s' % variable_name
@@ -71,6 +223,7 @@ class FileConfigParser(object):
self.token = None
self.server = None
+
def get_username(self):
if self.username: return self.username
@@ -78,6 +231,7 @@ class FileConfigParser(object):
self.username = self.config_parser.get(self.section, 'username')
return self.username
+
def get_token(self):
if self.token: return self.token
@@ -85,6 +239,7 @@ class FileConfigParser(object):
self.token = self.config_parser.get(self.section, 'token')
return self.token
+
def get_server(self):
if self.server: return self.server
@@ -93,7 +248,6 @@ class FileConfigParser(object):
return self.server
-
class ArgumentParser(object):
def __init__(self, args):
self.username = args.get('username')
@@ -101,15 +255,19 @@ class ArgumentParser(object):
self.server = args.get('server')
self.job = args.get('job')
+
def get_username(self):
return self.username
+
def get_token(self):
return self.token
+
def get_server(self):
return self.server
+
def get_job(self):
return self.job
@@ -132,8 +290,7 @@ def handle_connection(func):
except xmlrpclib.Fault as e:
if e.faultCode == 404 and e.faultString == \
"Job output not found.":
- print "Waiting for job output..."
- time.sleep(5)
+ pass
except (IOError, Exception) as e:
print "Function %s raised an exception, exiting..." % func.__name__
print e
@@ -146,6 +303,7 @@ class LavaConnection(object):
self.configuration = configuration
self.connection = None
+
@handle_connection
def connect(self):
url = self.configuration.construct_url()
@@ -155,55 +313,134 @@ class LavaConnection(object):
self.connection.system.listMethods()
print "Connection Successful."
+
@handle_connection
def get_job_status(self, job_id):
return self.connection.scheduler.job_status(job_id)
+
+ @handle_connection
+ def get_job_details(self, job_id):
+ return self.connection.scheduler.job_details(job_id)
+
+
@handle_connection
def get_job_output(self, job_id):
return self.connection.scheduler.job_output(job_id)
+
class LavaRunJob(object):
- def __init__(self, connection, job_id):
+ def __init__(self, connection, job_id, poll_interval):
self.END_STATES = ['Complete', 'Incomplete', 'Canceled']
self.job_id = job_id
- self.printed_output = None
self.connection = connection
+ self.poll_interval = poll_interval or 2
+ self.output = ""
+ self.state = dict()
+ self.details = dict()
+ self.raw_details = dict()
+ self.actions = list()
+ self.last_poll_time = None
+ self.next_poll_time = datetime.datetime.now()
+ self._is_running = True
+
+
+ def get_description(self):
+ self._get_state()
+ return self.details.get('description', '')
+
+
+ def get_hostname(self):
+ self._get_state()
+ return self.details.get('hostname', '')
+
+
+ def get_device_type_id(self):
+ self._get_state()
+ return self.details.get('device_type_id', '')
- def get_status(self):
- return self.connection.get_job_status(self.job_id)['job_status']
def get_output(self):
- return self.connection.get_job_output(self.job_id)
+ self._get_state()
+ return self.output
+
def is_running(self):
- return self.get_status() not in self.END_STATES
-
- def print_output(self):
- full_output = str(self.get_output())
- if self.printed_output:
- new_output = full_output[len(self.printed_output):]
- else:
- new_output = full_output
- if new_output == 'None':
- print "No job output..."
- elif new_output == '':
- pass
- else:
- print new_output
- self.printed_output = full_output
+ self._get_state()
+ return self._is_running
+
+
+ def last_action(self):
+ if not self.actions:
+ return "-"
+ return self.actions[-1]
- def run(self):
- is_running = True
+ def all_actions(self):
+ return self.actions
+
+
+ def connect(self):
self.connection.connect()
- while is_running:
- self.print_output()
- time.sleep(2)
- is_running = self.is_running()
- print "Job has finished."
+ def _get_state(self):
+ if self._is_running and datetime.datetime.now() > self.next_poll_time:
+ self.state = self.connection.get_job_status(self.job_id)
+ self.raw_details = self.connection.get_job_details(self.job_id)
+ self.output = self.connection.get_job_output(self.job_id)
+
+ self.last_poll_time = self.next_poll_time
+ self.next_poll_time = self.last_poll_time + datetime.timedelta(seconds=self.poll_interval)
+
+ if not self.output:
+ self.output = ""
+ else:
+ self.output = str(self.output)
+
+ self._parse_output()
+ self._parse_details()
+
+ self._is_running = self.state['job_status'] not in self.END_STATES
+
+
+ def _parse_details(self):
+ description = self.raw_details.get('description', None)
+ if description:
+ self.details['description'] = description
+
+ device_cache = self.raw_details.get('_actual_device_cache', None)
+ if device_cache:
+ hostname = device_cache.get('hostname', None)
+ if hostname:
+ self.details['hostname'] = hostname
+
+ device_type_id = device_cache.get('device_type_id', None)
+ if device_type_id:
+ self.details['device_type_id'] = device_type_id
+
+
+ def _parse_output(self):
+ del self.actions[:]
+ for line in self.output.splitlines():
+ if 'ACTION-B' in line:
+ self.actions.append(self._parse_actions(line))
+
+
+ def _parse_actions(self, line):
+ substr = line[line.find('ACTION-B')+len('ACTION-B')+2:]
+ if substr.startswith('deploy_linaro_'):
+ deployment_elems = list()
+ re_elems = re.compile('u\'[a-z]+\'')
+ for elem in re_elems.findall(substr):
+ deployment_elems.append(elem[2:-1])
+ return "deploy " + ','.join(deployment_elems)
+ elif substr.startswith('lava_test_shell'):
+ substr = substr[substr.find('testdef\': u')+len('testdef\': u')+1:]
+ substr = substr[:substr.find('.yaml')]
+ return "test_shell " + substr
+
+ return "unknown (%s)" % substr[:substr.find(' ')]
def get_config(args):
@@ -215,12 +452,22 @@ def get_config(args):
def main(args):
config = get_config(args)
lava_connection = LavaConnection(config)
- lava_job = LavaRunJob(lava_connection, config.get_config_variable('job'))
- lava_job.run()
+ lava_job = LavaRunJob(lava_connection,
+ config.get_config_variable('job'),
+ 2)
+ lava_job.connect()
+
+ if args["curses"]:
+ output_handler = CursesOutput(lava_job)
+ else:
+ output_handler = FileOutputHandler(sys.stdout, lava_job)
+
+ output_handler.run()
exit(0)
+
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("--config", help="configuration for the LAVA server")
@@ -229,5 +476,8 @@ if __name__ == '__main__':
parser.add_argument("--token", help="token for LAVA server api")
parser.add_argument("--server", help="server url for LAVA server")
parser.add_argument("--job", help="job to fetch console log from")
+ parser.add_argument("--curses", help="use curses for output", action="store_true")
args = vars(parser.parse_args())
- main(args) \ No newline at end of file
+ main(args)
+
+# vim: set sw=4 sts=4 et fileencoding=utf-8 :
diff --git a/text_output.py b/text_output.py
new file mode 100644
index 0000000..b0bfb28
--- /dev/null
+++ b/text_output.py
@@ -0,0 +1,77 @@
+#!/usr/bin/python
+#
+# This file is part of lava-hacks. lava-hacks is free software: you can
+# redistribute it and/or modify it under the terms of the GNU General Public
+# License as published by the Free Software Foundation, version 2.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+# details.
+#
+# You should have received a copy of the GNU General Public License along with
+# this program; if not, write to the Free Software Foundation, Inc., 51
+# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# Copyright Anders Roxell 2015
+
+def get_sub_str(text, from_pos, num_chars, break_chars):
+ sub_str = text[from_pos:from_pos+num_chars]
+ for index, ch in enumerate(sub_str):
+ if ch in break_chars:
+ return (sub_str[:index], from_pos+index+1)
+ return (sub_str, from_pos+len(sub_str))
+
+
+class TextBlock(object):
+ def __init__(self, text="", width=0):
+ self.text = str(text)
+ self.width = width
+ self.block = list()
+
+
+ def set_width(self, width, reflow=True):
+ self.width = width
+
+ if reflow: self.reflow()
+
+
+ def set_text(self, text, reflow=True):
+ self.text = str(text)
+
+ if reflow: self.reflow()
+
+
+ def append_text(self, new_text, reflow=True):
+ self.text += str(text)
+
+ if reflow: self.reflow()
+
+
+ def get_block(self, start_line, num_lines):
+ block_len = len(self.block)
+ if start_line < 0:
+ start_line = block_len-start_line-num_lines
+
+ if start_line+num_lines > block_len:
+ num_lines = block_len-start_line
+ return self.block[start_line:start_line+num_lines]
+
+
+ def reflow(self, width=0):
+ if not self.width:
+ raise Exception("Cannot reflow to windows of width %d" % self.width)
+
+ del self.block[:]
+ self.width = width or self.width
+
+ text_len = len(self.text)
+ cur_pos = 0
+
+ while cur_pos < text_len:
+ cur_line, next_pos = get_sub_str(self.text, cur_pos, self.width, ('\n',))
+ self.block.append(cur_line)
+ cur_pos = next_pos
+
+
+# vim: set sw=4 sts=4 et fileencoding=utf-8 :