Add API resource for unified job stdout with HTML output of ANSI color codes.

This commit is contained in:
Chris Church
2014-04-01 20:15:36 -04:00
parent fb5b069273
commit 86599cf1e3
18 changed files with 747 additions and 18 deletions

View File

@@ -16,3 +16,18 @@ class BrowsableAPIRenderer(renderers.BrowsableAPIRenderer):
return return
if method in ('DELETE', 'OPTIONS'): if method in ('DELETE', 'OPTIONS'):
return True # Don't actually need to return a form return True # Don't actually need to return a form
class PlainTextRenderer(renderers.BaseRenderer):
media_type = 'text/plain'
format = 'txt'
def render(self, data, media_type=None, renderer_context=None):
if not isinstance(data, basestring):
data = unicode(data)
return data.encode(self.charset)
class AnsiTextRenderer(PlainTextRenderer):
media_type = 'text/plain'
format = 'ansi'

View File

@@ -393,6 +393,12 @@ class UnifiedJobSerializer(BaseSerializer):
res['unified_job_template'] = obj.unified_job_template.get_absolute_url() res['unified_job_template'] = obj.unified_job_template.get_absolute_url()
if obj.schedule and obj.schedule.active: if obj.schedule and obj.schedule.active:
res['schedule'] = obj.schedule.get_absolute_url() res['schedule'] = obj.schedule.get_absolute_url()
if isinstance(obj, ProjectUpdate):
res['stdout'] = reverse('api:project_update_stdout', args=(obj.pk,))
elif isinstance(obj, InventoryUpdate):
res['stdout'] = reverse('api:inventory_update_stdout', args=(obj.pk,))
elif isinstance(obj, Job):
res['stdout'] = reverse('api:job_stdout', args=(obj.pk,))
return res return res
def to_native(self, obj): def to_native(self, obj):
@@ -445,15 +451,20 @@ class UnifiedJobListSerializer(UnifiedJobSerializer):
return ret return ret
class BaseTaskSerializer(BaseSerializer): class UnifiedJobStdoutSerializer(UnifiedJobSerializer):
job_env = serializers.SerializerMethodField('get_job_env') class Meta:
fields = ('result_stdout',)
def get_job_env(self, obj): def get_types(self):
job_env_d = obj.job_env if type(self) is UnifiedJobSerializer:
if 'BROKER_URL' in job_env_d: return ['project_update', 'inventory_update', 'job']
job_env_d.pop('BROKER_URL') else:
return job_env_d return super(UnifiedJobSerializer, self).get_types()
def to_native(self, obj):
ret = super(UnifiedJobStdoutSerializer, self).to_native(obj)
return ret.get('result_stdout', '')
class UserSerializer(BaseSerializer): class UserSerializer(BaseSerializer):

View File

@@ -0,0 +1,28 @@
# Retrieve {{ model_verbose_name|title }} Stdout:
Make GET request to this resource to retrieve the stdout from running this
{{ model_verbose_name }}.
## Format
Use the `format` query string parameter to specify the output format.
* Browsable API: `?format=api`
* HTML: `?format=html`
* Plain Text: `?format=txt`
* Plain Text with ANSI color codes: `?format=ansi`
When using the HTML or API formats, use the `scheme` query string parameter to
change the output colors. The value must be one of the following (default is
`ansi2html`):
* `ansi2html`
* `osx`
* `xterm`
* `xterm-bright`
* `solarized`
Use `dark=1` or `dark=0` as a query string parameter to force or disable a
dark background.
{% include "api/_new_in_awx.md" %}

View File

@@ -47,6 +47,7 @@ project_urls = patterns('awx.api.views',
project_update_urls = patterns('awx.api.views', project_update_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/$', 'project_update_detail'), url(r'^(?P<pk>[0-9]+)/$', 'project_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'project_update_cancel'), url(r'^(?P<pk>[0-9]+)/cancel/$', 'project_update_cancel'),
url(r'^(?P<pk>[0-9]+)/stdout/$', 'project_update_stdout'),
) )
team_urls = patterns('awx.api.views', team_urls = patterns('awx.api.views',
@@ -112,6 +113,7 @@ inventory_source_urls = patterns('awx.api.views',
inventory_update_urls = patterns('awx.api.views', inventory_update_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/$', 'inventory_update_detail'), url(r'^(?P<pk>[0-9]+)/$', 'inventory_update_detail'),
url(r'^(?P<pk>[0-9]+)/cancel/$', 'inventory_update_cancel'), url(r'^(?P<pk>[0-9]+)/cancel/$', 'inventory_update_cancel'),
url(r'^(?P<pk>[0-9]+)/stdout/$', 'inventory_update_stdout'),
) )
credential_urls = patterns('awx.api.views', credential_urls = patterns('awx.api.views',
@@ -142,6 +144,7 @@ job_urls = patterns('awx.api.views',
url(r'^(?P<pk>[0-9]+)/job_host_summaries/$', 'job_job_host_summaries_list'), url(r'^(?P<pk>[0-9]+)/job_host_summaries/$', 'job_job_host_summaries_list'),
url(r'^(?P<pk>[0-9]+)/job_events/$', 'job_job_events_list'), url(r'^(?P<pk>[0-9]+)/job_events/$', 'job_job_events_list'),
url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'job_activity_stream_list'), url(r'^(?P<pk>[0-9]+)/activity_stream/$', 'job_activity_stream_list'),
url(r'^(?P<pk>[0-9]+)/stdout/$', 'job_stdout'),
) )
job_host_summary_urls = patterns('awx.api.views', job_host_summary_urls = patterns('awx.api.views',

View File

@@ -29,6 +29,10 @@ from rest_framework.settings import api_settings
from rest_framework.views import exception_handler from rest_framework.views import exception_handler
from rest_framework import status from rest_framework import status
# Ansi2HTML
from ansi2html import Ansi2HTMLConverter
from ansi2html.style import SCHEME
# AWX # AWX
from awx.main.licenses import LicenseReader from awx.main.licenses import LicenseReader
from awx.main.models import * from awx.main.models import *
@@ -37,8 +41,10 @@ from awx.main.access import get_user_queryset
from awx.main.signals import ignore_inventory_computed_fields, ignore_inventory_group_removal from awx.main.signals import ignore_inventory_computed_fields, ignore_inventory_group_removal
from awx.api.authentication import JobTaskAuthentication from awx.api.authentication import JobTaskAuthentication
from awx.api.permissions import * from awx.api.permissions import *
from awx.api.renderers import *
from awx.api.serializers import * from awx.api.serializers import *
from awx.api.generics import * from awx.api.generics import *
from awx.api.generics import get_view_name
def api_exception_handler(exc): def api_exception_handler(exc):
@@ -1407,6 +1413,54 @@ class UnifiedJobList(ListAPIView):
serializer_class = UnifiedJobListSerializer serializer_class = UnifiedJobListSerializer
new_in_148 = True new_in_148 = True
class UnifiedJobStdout(RetrieveAPIView):
serializer_class = UnifiedJobStdoutSerializer
renderer_classes = [BrowsableAPIRenderer, renderers.StaticHTMLRenderer,
PlainTextRenderer, AnsiTextRenderer]
filter_backends = ()
new_in_148 = True
def retrieve(self, request, *args, **kwargs):
unified_job = self.get_object()
if request.accepted_renderer.format in ('html', 'api'):
scheme = request.QUERY_PARAMS.get('scheme', 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 == 'api')
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__))
if content_only:
headers = conv.produce_headers()
body = conv.convert(unified_job.result_stdout_raw, full=False)
data = '\n'.join([headers, body])
data = '<div class="nocode body_foreground body_background">%s</div>' % data
else:
data = conv.convert(unified_job.result_stdout_raw)
# Fix ugly grey background used by default.
data = data.replace('.body_background { background-color: #AAAAAA; }',
'.body_background { background-color: #f5f5f5; }')
return Response(data)
elif request.accepted_renderer.format == 'ansi':
return Response(unified_job.result_stdout_raw)
else:
return super(UnifiedJobStdout, self).retrieve(request, *args, **kwargs)
class ProjectUpdateStdout(UnifiedJobStdout):
model = ProjectUpdate
class InventoryUpdateStdout(UnifiedJobStdout):
model = InventoryUpdate
class JobStdout(UnifiedJobStdout):
model = Job
class ActivityStreamList(SimpleListAPIView): class ActivityStreamList(SimpleListAPIView):
model = ActivityStream model = ActivityStream

View File

@@ -2,6 +2,7 @@ Local versions of third-party packages required by Tower. Package names and
versions are listed below, along with notes on which files are included. versions are listed below, along with notes on which files are included.
amqp==1.4.4 (amqp/*) amqp==1.4.4 (amqp/*)
ansi2html==1.0.6 (ansi2html/*)
anyjson==0.3.3 (anyjson/*) anyjson==0.3.3 (anyjson/*)
argparse==1.2.1 (argparse.py, needed for Python 2.6 support) argparse==1.2.1 (argparse.py, needed for Python 2.6 support)
Babel==1.3 (babel/*, excluded bin/pybabel) Babel==1.3 (babel/*, excluded bin/pybabel)

View File

@@ -0,0 +1,2 @@
from ansi2html.converter import Ansi2HTMLConverter
__all__ = ['Ansi2HTMLConverter']

View File

@@ -0,0 +1,492 @@
# This file is part of ansi2html
# Convert ANSI (terminal) colours and attributes to HTML
# Copyright (C) 2012 Ralph Bean <rbean@redhat.com>
# Copyright (C) 2013 Sebastian Pipping <sebastian@pipping.org>
#
# Inspired by and developed off of the work by pixelbeat and blackjack.
#
# This program 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, either version 3 of
# the License, or (at your option) any later version.
#
# 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, see
# <http://www.gnu.org/licenses/>.
import re
import sys
import optparse
import pkg_resources
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
from ansi2html.style import get_styles, SCHEME
import six
from six.moves import map
from six.moves import zip
ANSI_FULL_RESET = 0
ANSI_INTENSITY_INCREASED = 1
ANSI_INTENSITY_REDUCED = 2
ANSI_INTENSITY_NORMAL = 22
ANSI_STYLE_ITALIC = 3
ANSI_STYLE_NORMAL = 23
ANSI_BLINK_SLOW = 5
ANSI_BLINK_FAST = 6
ANSI_BLINK_OFF = 25
ANSI_UNDERLINE_ON = 4
ANSI_UNDERLINE_OFF = 24
ANSI_CROSSED_OUT_ON = 9
ANSI_CROSSED_OUT_OFF = 29
ANSI_VISIBILITY_ON = 28
ANSI_VISIBILITY_OFF = 8
ANSI_FOREGROUND_CUSTOM_MIN = 30
ANSI_FOREGROUND_CUSTOM_MAX = 37
ANSI_FOREGROUND_256 = 38
ANSI_FOREGROUND_DEFAULT = 39
ANSI_BACKGROUND_CUSTOM_MIN = 40
ANSI_BACKGROUND_CUSTOM_MAX = 47
ANSI_BACKGROUND_256 = 48
ANSI_BACKGROUND_DEFAULT = 49
ANSI_NEGATIVE_ON = 7
ANSI_NEGATIVE_OFF = 27
_template = six.u("""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=%(output_encoding)s">
<title>%(title)s</title>
<style type="text/css">\n%(style)s\n</style>
</head>
<body class="body_foreground body_background" style="font-size: %(font_size)s;" >
<pre>
%(content)s
</pre>
</body>
</html>
""")
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, '<a href="%s">%s</a>' % (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([
"""<span id="line-%i">%s</span>""" % (i, line)
for i, line in enumerate(combined.split('\n'))
])
return combined
def _apply_regex(self, ansi):
if self.escaped:
specials = OrderedDict([
('&', '&amp;'),
('<', '&lt;'),
('>', '&gt;'),
])
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 '</span>'
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 '</span>'
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 '<span style="%s">' % "; ".join(style)
else:
yield '<span class="%s">' % " ".join(css_classes)
inside_span = True
yield ansi[last_end:]
if inside_span:
yield '</span>'
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 '<style type="text/css">\n%(style)s\n</style>\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 <style> tag.")
parser.add_option(
"-f", '--font-size', dest='font_size', metavar='SIZE',
default="normal",
help="Set the global font size in the output.")
parser.add_option(
"-l", '--light-background', dest='light_background',
default=False, action="store_true",
help="Set output to 'light background' mode.")
parser.add_option(
"-a", '--linkify', dest='linkify',
default=False, action="store_true",
help="Transform URLs into <a> links.")
parser.add_option(
"-u", '--unescape', dest='escaped',
default=True, action="store_false",
help="Do not escape XML tags found in the input.")
parser.add_option(
"-m", '--markup-lines', dest="markup_lines",
default=False, action="store_true",
help="Surround lines with <span id='line-n'>..</span>.")
parser.add_option(
'--input-encoding', dest='input_encoding', metavar='ENCODING',
default='utf-8',
help="Specify input encoding")
parser.add_option(
'--output-encoding', dest='output_encoding', metavar='ENCODING',
default='utf-8',
help="Specify output encoding")
parser.add_option(
'-s', '--scheme', dest='scheme', metavar='SCHEME',
default='ansi2html', choices=scheme_names,
help=("Specify color palette scheme. Default: %%default. Choices: %s"
% scheme_names))
parser.add_option(
'-t', '--title', dest='output_title',
default='',
help="Specify output title")
opts, args = parser.parse_args()
conv = Ansi2HTMLConverter(
inline=opts.inline,
dark_bg=not opts.light_background,
font_size=opts.font_size,
linkify=opts.linkify,
escaped=opts.escaped,
markup_lines=opts.markup_lines,
output_encoding=opts.output_encoding,
scheme=opts.scheme,
title=opts.output_title,
)
def _read(input_bytes):
if six.PY3:
# This is actually already unicode. How to we explicitly decode in
# python3? I don't know the answer yet.
return input_bytes
else:
return input_bytes.decode(opts.input_encoding)
def _print(output_unicode, end='\n'):
if hasattr(sys.stdout, 'buffer'):
output_bytes = (output_unicode + end).encode(opts.output_encoding)
sys.stdout.buffer.write(output_bytes)
elif not six.PY3:
sys.stdout.write((output_unicode + end).encode(opts.output_encoding))
else:
sys.stdout.write(output_unicode + end)
# Produce only the headers and quit
if opts.headers:
_print(conv.produce_headers(), end='')
return
full = not bool(opts.partial or opts.inline)
if six.PY3:
output = conv.convert("".join(sys.stdin.readlines()), full=full, ensure_trailing_newline=True)
_print(output, end='')
else:
output = conv.convert(six.u("").join(
map(_read, sys.stdin.readlines())
), full=full, ensure_trailing_newline=True)
_print(output, end='')

View File

@@ -0,0 +1,113 @@
# This file is part of ansi2html.
# Copyright (C) 2012 Kuno Woudt <kuno@frob.nl>
# Copyright (C) 2013 Sebastian Pipping <sebastian@pipping.org>
#
# This program 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, either version 3 of
# the License, or (at your option) any later version.
#
# 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, see
# <http://www.gnu.org/licenses/>.
import sys
class Rule(object):
def __init__(self, klass, **kw):
self.klass = klass
self.kw = '; '.join([(k.replace('_', '-')+': '+kw[k])
for k in sorted(kw.keys())]).strip()
def __str__(self):
return '%s { %s; }' % (self.klass, self.kw)
def index(r, g, b):
return str(16 + (r * 36) + (g * 6) + b)
def color(r, g, b):
return "#%.2x%.2x%.2x" % (r * 42, g * 42, b * 42)
def level(grey):
return "#%.2x%.2x%.2x" % (((grey * 10) + 8,) * 3)
def index2(grey):
return str(232 + grey)
# http://en.wikipedia.org/wiki/ANSI_escape_code#Colors
SCHEME = { # black red green brown/yellow blue magenta cyan grey/white
'ansi2html': ("#000316", "#aa0000", "#00aa00", "#aa5500", "#0000aa",
"#E850A8", "#00aaaa", "#F5F1DE"),
'xterm': ("#000000", "#cd0000", "#00cd00", "#cdcd00", "#0000ee",
"#cd00cd", "#00cdcd", "#e5e5e5"),
'xterm-bright': ("#7f7f7f", "#ff0000", "#00ff00", "#ffff00", "#5c5cff",
"#ff00ff", "#00ffff", "#ffffff"),
'osx': ("#000000", "#c23621", "#25bc24", "#adad27", "#492ee1",
"#d338d3", "#33bbc8", "#cbcccd"),
# http://ethanschoonover.com/solarized
'solarized': ("#262626", "#d70000", "#5f8700", "#af8700", "#0087ff",
"#af005f", "#00afaf", "#e4e4e4"),
}
def get_styles(dark_bg=True, scheme='ansi2html'):
css = [
Rule('.body_foreground', color=('#000000', '#AAAAAA')[dark_bg]),
Rule('.body_background', background_color=('#AAAAAA', '#000000')[dark_bg]),
Rule('.body_foreground > .bold,.bold > .body_foreground, body.body_foreground > pre > .bold',
color=('#000000', '#FFFFFF')[dark_bg], font_weight=('bold', 'normal')[dark_bg]),
Rule('.inv_foreground', color=('#000000', '#FFFFFF')[not dark_bg]),
Rule('.inv_background', background_color=('#AAAAAA', '#000000')[not dark_bg]),
Rule('.ansi1', font_weight='bold'),
Rule('.ansi2', font_weight='lighter'),
Rule('.ansi3', font_style='italic'),
Rule('.ansi4', text_decoration='underline'),
Rule('.ansi5', text_decoration='blink'),
Rule('.ansi6', text_decoration='blink'),
Rule('.ansi8', visibility='hidden'),
Rule('.ansi9', text_decoration='line-through'),
]
# set palette
pal = SCHEME[scheme]
for _index in range(8):
css.append(Rule('.ansi3%s' % _index, color=pal[_index]))
css.append(Rule('.inv3%s' % _index, background_color=pal[_index]))
for _index in range(8):
css.append(Rule('.ansi4%s' % _index, background_color=pal[_index]))
css.append(Rule('.inv4%s' % _index, color=pal[_index]))
# css.append("/* Define the explicit color codes (obnoxious) */\n\n")
for green in range(0, 6):
for red in range(0, 6):
for blue in range(0, 6):
css.append(Rule(".ansi38-%s" % index(red, green, blue),
color=color(red, green, blue)))
css.append(Rule(".inv38-%s" % index(red, green, blue),
background=color(red, green, blue)))
css.append(Rule(".ansi48-%s" % index(red, green, blue),
background=color(red, green, blue)))
css.append(Rule(".inv48-%s" % index(red, green, blue),
color=color(red, green, blue)))
for grey in range(0, 24):
css.append(Rule('.ansi38-%s' % index2(grey), color=level(grey)))
css.append(Rule('.inv38-%s' % index2(grey), background=level(grey)))
css.append(Rule('.ansi48-%s' % index2(grey), background=level(grey)))
css.append(Rule('.inv48-%s' % index2(grey), color=level(grey)))
return css

View File

@@ -0,0 +1,2 @@
def read_to_unicode(obj):
return [line.decode('utf-8') for line in obj.readlines()]

View File

@@ -4,6 +4,7 @@
# Python # Python
import json import json
import logging import logging
import re
import shlex import shlex
import os import os
import os.path import os.path
@@ -492,15 +493,19 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
super(UnifiedJob, self).delete() super(UnifiedJob, self).delete()
@property @property
def result_stdout(self): def result_stdout_raw(self):
if self.result_stdout_file != "": if self.result_stdout_file != "":
if not os.path.exists(self.result_stdout_file): if not os.path.exists(self.result_stdout_file):
return "stdout capture is missing" return "stdout capture is missing"
stdout_fd = open(self.result_stdout_file, "r") with open(self.result_stdout_file, "r") as stdout_fd:
output = stdout_fd.read() return stdout_fd.read()
stdout_fd.close() else:
return output return self.result_stdout_text
return self.result_stdout_text
@property
def result_stdout(self):
ansi_escape = re.compile(r'\x1b[^m]*m')
return ansi_escape.sub('', self.result_stdout_raw)
@property @property
def celery_task(self): def celery_task(self):

View File

@@ -201,7 +201,6 @@ class BaseTask(Task):
env[key] = str(value) env[key] = str(value)
# Set environment variables needed for inventory and job event # Set environment variables needed for inventory and job event
# callbacks to work. # callbacks to work.
env['ANSIBLE_NOCOLOR'] = '1' # Prevent output of escape sequences.
# Update PYTHONPATH to use local site-packages. # Update PYTHONPATH to use local site-packages.
python_paths = env.get('PYTHONPATH', '').split(os.pathsep) python_paths = env.get('PYTHONPATH', '').split(os.pathsep)
local_site_packages = self.get_path_to('..', 'lib', 'site-packages') local_site_packages = self.get_path_to('..', 'lib', 'site-packages')

View File

@@ -1,4 +1,4 @@
{# Copy of base.html from rest_framework with minor AWX change. #} {# Copy of base.html from rest_framework with minor Ansible Tower change. #}
{% load url from future %} {% load url from future %}
{% load rest_framework %} {% load rest_framework %}
<!DOCTYPE html> <!DOCTYPE html>
@@ -34,7 +34,7 @@
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container-fluid"> <div class="container-fluid">
<span href="/"> <span href="/">
{% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %} {% block branding %}<a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
</span> </span>
<ul class="nav pull-right"> <ul class="nav pull-right">
{% block userlinks %} {% block userlinks %}
@@ -119,9 +119,9 @@
</div> </div>
<div class="response-info"> <div class="response-info">
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %} <pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span> {% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
{% endfor %} {% endfor %}
{# Original line below had content|urlize_quoted_links; for AWX disable automatic URL creation here. #} {# Original line below had content|urlize_quoted_links; for Ansible Tower disable automatic URL creation here. #}
</div>{{ content }}</pre>{% endautoescape %} </div>{{ content }}</pre>{% endautoescape %}
</div> </div>
</div> </div>

Binary file not shown.

View File

@@ -8,6 +8,7 @@ Django>=1.4
# The following packages and their dependencies are bundled with AWX # The following packages and their dependencies are bundled with AWX
# (in awx/lib/site-packages): # (in awx/lib/site-packages):
#ansi2html
#boto #boto
#django-auth-ldap #django-auth-ldap
#django-celery #django-celery

View File

@@ -58,6 +58,7 @@ Django-1.5.5.tar.gz
#python-swiftclient-2.0.3.tar.gz #python-swiftclient-2.0.3.tar.gz
#rackspace-novaclient-1.4.tar.gz #rackspace-novaclient-1.4.tar.gz
# Remaining dev/prod packages: # Remaining dev/prod packages:
#ansi2html-1.0.6.tar.gz
#boto-2.27.0.tar.gz #boto-2.27.0.tar.gz
#django-auth-ldap-1.1.8.tar.gz #django-auth-ldap-1.1.8.tar.gz
#django-celery-3.1.10.tar.gz #django-celery-3.1.10.tar.gz

View File

@@ -5,6 +5,7 @@ Django>=1.4
# The following packages and their dependencies are bundled with AWX # The following packages and their dependencies are bundled with AWX
# (in awx/lib/site-packages): # (in awx/lib/site-packages):
#ansi2html
#boto #boto
#django-auth-ldap #django-auth-ldap
#django-celery #django-celery

View File

@@ -56,6 +56,7 @@ Django-1.5.5.tar.gz
#python-swiftclient-2.0.3.tar.gz #python-swiftclient-2.0.3.tar.gz
#rackspace-novaclient-1.4.tar.gz #rackspace-novaclient-1.4.tar.gz
# Remaining dev/prod packages: # Remaining dev/prod packages:
#ansi2html-1.0.6.tar.gz
#boto-2.27.0.tar.gz #boto-2.27.0.tar.gz
#django-auth-ldap-1.1.8.tar.gz #django-auth-ldap-1.1.8.tar.gz
#django-celery-3.1.10.tar.gz #django-celery-3.1.10.tar.gz