diff options
author | Tyler Baker <forcedinductionz@gmail.com> | 2015-04-15 11:08:54 -0700 |
---|---|---|
committer | Tyler Baker <forcedinductionz@gmail.com> | 2015-04-15 11:08:54 -0700 |
commit | aa547b81751d9452664388dc6dc44d8ac2132447 (patch) | |
tree | 6c3ea7b31eaf28b966ee5cd4b2e86b2231343df2 | |
parent | 8da9881ddbde9c5b99942501eb02dd0eccf8bc92 (diff) | |
parent | c0425c787a4d669be86c0d05487a56dc98643019 (diff) |
Merge pull request #4 from roxell/develop
Thanks, applied.
-rwxr-xr-x | stream-lava-log.py | 318 | ||||
-rw-r--r-- | text_output.py | 77 |
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 : |