diff --git a/awx/api/templates/api/stdout.html b/awx/api/templates/api/stdout.html new file mode 100644 index 0000000000..25aa79d08c --- /dev/null +++ b/awx/api/templates/api/stdout.html @@ -0,0 +1,50 @@ +{% if content_only %}
{{ body }}
+
+
+{% endif %}
\ No newline at end of file
diff --git a/awx/api/views.py b/awx/api/views.py
index 7a212976cd..064ae2a9b9 100644
--- a/awx/api/views.py
+++ b/awx/api/views.py
@@ -3,6 +3,7 @@
# All Rights Reserved.
# Python
+import cgi
import datetime
import dateutil
import time
@@ -33,13 +34,12 @@ from rest_framework.settings import api_settings
from rest_framework.views import exception_handler
from rest_framework import status
-# Ansi2HTML
-from ansi2html import Ansi2HTMLConverter
-from ansi2html.style import SCHEME
-
# QSStats
import qsstats
+# ANSIConv
+import ansiconv
+
# AWX
from awx.main.task_engine import TaskSerializer, TASK_FILE
from awx.main.access import get_user_queryset
@@ -2201,28 +2201,23 @@ class UnifiedJobStdout(RetrieveAPIView):
def retrieve(self, request, *args, **kwargs):
unified_job = self.get_object()
if request.accepted_renderer.format in ('html', 'api', 'json'):
- scheme = request.QUERY_PARAMS.get('scheme', None)
start_line = request.QUERY_PARAMS.get('start_line', 0)
end_line = request.QUERY_PARAMS.get('end_line', None)
- if scheme not in SCHEME:
- scheme = 'ansi2html'
dark_val = request.QUERY_PARAMS.get('dark', '')
dark = bool(dark_val and dark_val[0].lower() in ('1', 't', 'y'))
content_only = bool(request.accepted_renderer.format in ('api', 'json'))
dark_bg = (content_only and dark) or (not content_only and (dark or not dark_val))
- conv = Ansi2HTMLConverter(scheme=scheme, dark_bg=dark_bg,
- title=get_view_name(self.__class__))
content, start, end, absolute_end = unified_job.result_stdout_raw_limited(start_line, end_line)
- if content_only:
- headers = conv.produce_headers()
- body = conv.convert(content, full=False) # Escapes any HTML that may be in content.
- data = '\n'.join([headers, body])
- data = '-%(content)s -- - - -""") - - -class _State(object): - def __init__(self): - self.reset() - - def reset(self): - self.intensity = ANSI_INTENSITY_NORMAL - self.style = ANSI_STYLE_NORMAL - self.blink = ANSI_BLINK_OFF - self.underline = ANSI_UNDERLINE_OFF - self.crossedout = ANSI_CROSSED_OUT_OFF - self.visibility = ANSI_VISIBILITY_ON - self.foreground = (ANSI_FOREGROUND_DEFAULT, None) - self.background = (ANSI_BACKGROUND_DEFAULT, None) - self.negative = ANSI_NEGATIVE_OFF - - def adjust(self, ansi_code, parameter=None): - if ansi_code in (ANSI_INTENSITY_INCREASED, ANSI_INTENSITY_REDUCED, ANSI_INTENSITY_NORMAL): - self.intensity = ansi_code - elif ansi_code in (ANSI_STYLE_ITALIC, ANSI_STYLE_NORMAL): - self.style = ansi_code - elif ansi_code in (ANSI_BLINK_SLOW, ANSI_BLINK_FAST, ANSI_BLINK_OFF): - self.blink = ansi_code - elif ansi_code in (ANSI_UNDERLINE_ON, ANSI_UNDERLINE_OFF): - self.underline = ansi_code - elif ansi_code in (ANSI_CROSSED_OUT_ON, ANSI_CROSSED_OUT_OFF): - self.crossedout = ansi_code - elif ansi_code in (ANSI_VISIBILITY_ON, ANSI_VISIBILITY_OFF): - self.visibility = ansi_code - elif ANSI_FOREGROUND_CUSTOM_MIN <= ansi_code <= ANSI_FOREGROUND_CUSTOM_MAX: - self.foreground = (ansi_code, None) - elif ansi_code == ANSI_FOREGROUND_256: - self.foreground = (ansi_code, parameter) - elif ansi_code == ANSI_FOREGROUND_DEFAULT: - self.foreground = (ansi_code, None) - elif ANSI_BACKGROUND_CUSTOM_MIN <= ansi_code <= ANSI_BACKGROUND_CUSTOM_MAX: - self.background = (ansi_code, None) - elif ansi_code == ANSI_BACKGROUND_256: - self.background = (ansi_code, parameter) - elif ansi_code == ANSI_BACKGROUND_DEFAULT: - self.background = (ansi_code, None) - elif ansi_code in (ANSI_NEGATIVE_ON, ANSI_NEGATIVE_OFF): - self.negative = ansi_code - - def to_css_classes(self): - css_classes = [] - - def append_unless_default(output, value, default): - if value != default: - css_class = 'ansi%d' % value - output.append(css_class) - - def append_color_unless_default(output, color, default, negative, neg_css_class): - value, parameter = color - if value != default: - prefix = 'inv' if negative else 'ansi' - css_class_index = str(value) \ - if (parameter is None) \ - else '%d-%d' % (value, parameter) - output.append(prefix + css_class_index) - elif negative: - output.append(neg_css_class) - - append_unless_default(css_classes, self.intensity, ANSI_INTENSITY_NORMAL) - append_unless_default(css_classes, self.style, ANSI_STYLE_NORMAL) - append_unless_default(css_classes, self.blink, ANSI_BLINK_OFF) - append_unless_default(css_classes, self.underline, ANSI_UNDERLINE_OFF) - append_unless_default(css_classes, self.crossedout, ANSI_CROSSED_OUT_OFF) - append_unless_default(css_classes, self.visibility, ANSI_VISIBILITY_ON) - - flip_fore_and_background = (self.negative == ANSI_NEGATIVE_ON) - append_color_unless_default(css_classes, self.foreground, ANSI_FOREGROUND_DEFAULT, flip_fore_and_background, 'inv_background') - append_color_unless_default(css_classes, self.background, ANSI_BACKGROUND_DEFAULT, flip_fore_and_background, 'inv_foreground') - - return css_classes - - -def linkify(line): - for match in re.findall(r'https?:\/\/\S+', line): - line = line.replace(match, '%s' % (match, match)) - - return line - - -def _needs_extra_newline(text): - if not text or text.endswith('\n'): - return False - return True - - -class CursorMoveUp(object): - pass - - -class Ansi2HTMLConverter(object): - """ Convert Ansi color codes to CSS+HTML - - Example: - >>> conv = Ansi2HTMLConverter() - >>> ansi = " ".join(sys.stdin.readlines()) - >>> html = conv.convert(ansi) - """ - - def __init__(self, - inline=False, - dark_bg=True, - font_size='normal', - linkify=False, - escaped=True, - markup_lines=False, - output_encoding='utf-8', - scheme='ansi2html', - title='' - ): - - self.inline = inline - self.dark_bg = dark_bg - self.font_size = font_size - self.linkify = linkify - self.escaped = escaped - self.markup_lines = markup_lines - self.output_encoding = output_encoding - self.scheme = scheme - self.title = title - self._attrs = None - - if inline: - self.styles = dict([(item.klass.strip('.'), item) for item in get_styles(self.dark_bg, self.scheme)]) - - self.ansi_codes_prog = re.compile('\033\\[' '([\\d;]*)' '([a-zA-z])') - - def apply_regex(self, ansi): - parts = self._apply_regex(ansi) - parts = self._collapse_cursor(parts) - parts = list(parts) - - if self.linkify: - parts = [linkify(part) for part in parts] - - combined = "".join(parts) - - if self.markup_lines: - combined = "\n".join([ - """%s""" % (i, line) - for i, line in enumerate(combined.split('\n')) - ]) - - return combined - - def _apply_regex(self, ansi): - if self.escaped: - specials = OrderedDict([ - ('&', '&'), - ('<', '<'), - ('>', '>'), - ]) - for pattern, special in specials.items(): - ansi = ansi.replace(pattern, special) - - state = _State() - inside_span = False - last_end = 0 # the index of the last end of a code we've seen - for match in self.ansi_codes_prog.finditer(ansi): - yield ansi[last_end:match.start()] - last_end = match.end() - - params, command = match.groups() - - if command not in 'mMA': - continue - - # Special cursor-moving code. The only supported one. - if command == 'A': - yield CursorMoveUp - continue - - try: - params = list(map(int, params.split(';'))) - except ValueError: - params = [ANSI_FULL_RESET] - - # Find latest reset marker - last_null_index = None - skip_after_index = -1 - for i, v in enumerate(params): - if i <= skip_after_index: - continue - - if v == ANSI_FULL_RESET: - last_null_index = i - elif v in (ANSI_FOREGROUND_256, ANSI_BACKGROUND_256): - skip_after_index = i + 2 - - # Process reset marker, drop everything before - if last_null_index is not None: - params = params[last_null_index + 1:] - if inside_span: - inside_span = False - yield '' - state.reset() - - if not params: - continue - - # Turn codes into CSS classes - skip_after_index = -1 - for i, v in enumerate(params): - if i <= skip_after_index: - continue - - if v in (ANSI_FOREGROUND_256, ANSI_BACKGROUND_256): - try: - parameter = params[i + 2] - except IndexError: - continue - skip_after_index = i + 2 - else: - parameter = None - state.adjust(v, parameter=parameter) - - if inside_span: - yield '' - inside_span = False - - css_classes = state.to_css_classes() - if not css_classes: - continue - - if self.inline: - style = [self.styles[klass].kw for klass in css_classes if - klass in self.styles] - yield '' % "; ".join(style) - else: - yield '' % " ".join(css_classes) - inside_span = True - - yield ansi[last_end:] - if inside_span: - yield '' - inside_span = False - - def _collapse_cursor(self, parts): - """ Act on any CursorMoveUp commands by deleting preceding tokens """ - - final_parts = [] - for part in parts: - - # Throw out empty string tokens ("") - if not part: - continue - - # Go back, deleting every token in the last 'line' - if part == CursorMoveUp: - final_parts.pop() - while '\n' not in final_parts[-1]: - final_parts.pop() - - continue - - # Otherwise, just pass this token forward - final_parts.append(part) - - return final_parts - - def prepare(self, ansi='', ensure_trailing_newline=False): - """ Load the contents of 'ansi' into this object """ - - body = self.apply_regex(ansi) - - if ensure_trailing_newline and _needs_extra_newline(body): - body += '\n' - - self._attrs = { - 'dark_bg': self.dark_bg, - 'font_size': self.font_size, - 'body': body, - } - - return self._attrs - - def attrs(self): - """ Prepare attributes for the template """ - if not self._attrs: - raise Exception("Method .prepare not yet called.") - return self._attrs - - def convert(self, ansi, full=True, ensure_trailing_newline=False): - attrs = self.prepare(ansi, ensure_trailing_newline=ensure_trailing_newline) - if not full: - return attrs["body"] - else: - return _template % { - 'style' : "\n".join(map(str, get_styles(self.dark_bg, self.scheme))), - 'title' : self.title, - 'font_size' : self.font_size, - 'content' : attrs["body"], - 'output_encoding' : self.output_encoding, - } - - def produce_headers(self): - return '\n' % { - 'style' : "\n".join(map(str, get_styles(self.dark_bg, self.scheme))) - } - - -def main(): - """ - $ ls --color=always | ansi2html > directories.html - $ sudo tail /var/log/messages | ccze -A | ansi2html > logs.html - $ task burndown | ansi2html > burndown.html - """ - - scheme_names = sorted(six.iterkeys(SCHEME)) - version_str = pkg_resources.get_distribution('ansi2html').version - parser = optparse.OptionParser( - usage=main.__doc__, - version="%%prog %s" % version_str) - parser.add_option( - "-p", "--partial", dest="partial", - default=False, action="store_true", - help="Process lines as them come in. No headers are produced.") - parser.add_option( - "-i", "--inline", dest="inline", - default=False, action="store_true", - help="Inline style without headers or template.") - parser.add_option( - "-H", "--headers", dest="headers", - default=False, action="store_true", - help="Just produce the