Merge branch 'upstream_master'
* upstream_master: (70 commits) Automate cleanup of old dist directory Don't import modules that don't export anything Include ui template in manifest Lock dependency version until tests are fixed Add script to lookup locations based on a sourcemap Allow all static files to be loaded from dist Remove stray console log [beautify] disable requiretty for root Keep git URLs in SCP format for project updates. Fixes https://trello.com/c/xUL2FZyu Adding a --noinput flag. "source" for apt Update license writer for Matt's comments. Update package fact format after discussions with core team Move code under lib/ansible to js/shared forgot to call super Remove docs for scheme parameter. Replace ansi2html (GPL) with ansiconv (MIT). Final socket fix Update source location of socket.io-client Revert accidental socket.io upgrade ...
2
.gitignore
vendored
@ -15,7 +15,7 @@ awx/*.log
|
||||
tower/tower_warnings.log
|
||||
celerybeat-schedule
|
||||
awx/ui/static/docs
|
||||
awx/ui/static/dist
|
||||
awx/ui/dist
|
||||
|
||||
# Python & setuptools
|
||||
__pycache__
|
||||
|
||||
11
MANIFEST.in
@ -2,10 +2,8 @@ recursive-include awx *.py
|
||||
recursive-include awx/static *.ico
|
||||
recursive-include awx/templates *.html
|
||||
recursive-include awx/api/templates *.md
|
||||
recursive-include awx/ui *.html
|
||||
recursive-include awx/ui/static *.css *.ico *.png *.gif *.jpg *.gz
|
||||
recursive-include awx/ui/static *.eot *.svg *.ttf *.woff *.otf
|
||||
recursive-include awx/ui/static/lib *
|
||||
recursive-include awx/ui/templates *.html
|
||||
recursive-include awx/ui/dist *
|
||||
recursive-include awx/playbooks *.yml
|
||||
recursive-include awx/lib/site-packages *
|
||||
recursive-include config *
|
||||
@ -14,11 +12,7 @@ recursive-include config/rpm *
|
||||
recursive-exclude awx devonly.py*
|
||||
recursive-exclude awx/api/tests *
|
||||
recursive-exclude awx/main/tests *
|
||||
recursive-exclude awx/ui/static/lib/ansible *
|
||||
recursive-exclude awx/settings local_settings.py*
|
||||
include awx/ui/static/dist/tower.concat.js
|
||||
include awx/ui/static/dist/tower.concat.js.gz
|
||||
include awx/ui/static/js/config.js
|
||||
include tools/scripts/request_tower_configuration.sh
|
||||
include tools/scripts/ansible-tower
|
||||
include tools/munin_monitors/*
|
||||
@ -26,4 +20,3 @@ include tools/sosreport/*
|
||||
include COPYING
|
||||
prune awx/public
|
||||
prune awx/projects
|
||||
prune awx/ui/static/lib/jstree/_*
|
||||
|
||||
11
Makefile
@ -92,6 +92,7 @@ clean-grunt:
|
||||
# Remove UI build files
|
||||
clean-ui:
|
||||
rm -rf awx/ui/static/dist
|
||||
rm -rf awx/ui/dist
|
||||
rm -rf awx/ui/static/docs
|
||||
|
||||
# Remove temporary build files, compiled Python files.
|
||||
@ -202,8 +203,6 @@ server_noattach:
|
||||
tmux select-pane -U
|
||||
tmux split-window -v 'exec make receiver'
|
||||
tmux split-window -h 'exec make taskmanager'
|
||||
tmux select-pane -U
|
||||
tmux split-window -h 'exec make sync_ui'
|
||||
|
||||
server: server_noattach
|
||||
tmux -2 attach-session -t tower
|
||||
@ -279,7 +278,7 @@ package.json: packaging/grunt/package.template
|
||||
sed -e 's#%NAME%#$(NAME)#;s#%VERSION%#$(VERSION)#;s#%GIT_REMOTE_URL%#$(GIT_REMOTE_URL)#;' $< > $@
|
||||
|
||||
sync_ui: node_modules Brocfile.js
|
||||
$(NODE) tools/ui/timepiece.js awx/ui/static/dist
|
||||
$(NODE) tools/ui/timepiece.js awx/ui/dist
|
||||
|
||||
# Update local npm install
|
||||
node_modules: package.json
|
||||
@ -287,14 +286,14 @@ node_modules: package.json
|
||||
touch $@
|
||||
|
||||
devjs: node_modules clean-ui Brocfile.js bower.json Gruntfile.js
|
||||
$(BROCCOLI) build awx/ui/static/dist -- --debug
|
||||
$(BROCCOLI) build awx/ui/dist -- --debug
|
||||
|
||||
# Build minified JS/CSS.
|
||||
minjs: node_modules clean-ui Brocfile.js
|
||||
$(BROCCOLI) build awx/ui/static/dist -- --silent --no-debug --no-tests --compress
|
||||
$(BROCCOLI) build awx/ui/dist -- --silent --no-debug --no-tests --compress
|
||||
|
||||
minjs_ci: node_modules clean-ui Brocfile.js
|
||||
$(BROCCOLI) build awx/ui/static/dist -- --no-debug --compress
|
||||
$(BROCCOLI) build awx/ui/dist -- --no-debug --compress
|
||||
|
||||
# Check .js files for errors and lint
|
||||
jshint: node_modules Gruntfile.js
|
||||
|
||||
@ -33,6 +33,7 @@ from polymorphic import PolymorphicModel
|
||||
from awx.main.constants import SCHEDULEABLE_PROVIDERS
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.utils import get_type_for_model, get_model_for_type
|
||||
from awx.main.redact import REPLACE_STR
|
||||
|
||||
logger = logging.getLogger('awx.api.serializers')
|
||||
|
||||
@ -1419,6 +1420,17 @@ class JobSerializer(UnifiedJobSerializer, JobOptionsSerializer):
|
||||
return ret
|
||||
if 'job_template' in ret and (not obj.job_template or not obj.job_template.active):
|
||||
ret['job_template'] = None
|
||||
|
||||
if obj.job_template and obj.job_template.survey_enabled:
|
||||
if ret['extra_vars']:
|
||||
try:
|
||||
extra_vars = json.loads(ret['extra_vars'])
|
||||
for key in obj.job_template.survey_password_variables():
|
||||
if key in extra_vars:
|
||||
extra_vars[key] = REPLACE_STR
|
||||
ret['extra_vars'] = json.dumps(extra_vars)
|
||||
except ValueError:
|
||||
pass
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
50
awx/api/templates/api/stdout.html
Normal file
@ -0,0 +1,50 @@
|
||||
{% if content_only %}<div class="nocode ansi_fore ansi_back{% if dark %} ansi_dark{% endif %}">{% else %}
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>{{ title }}</title>
|
||||
{% endif %}<style type="text/css">
|
||||
.ansi_fore { color: #000000; }
|
||||
.ansi_back { background-color: #F5F5F5; }
|
||||
.ansi_fore.ansi_dark { color: #AAAAAA; }
|
||||
.ansi_back.ansi_dark { background-color: #000000; }
|
||||
.ansi1 { font-weight: bold; }
|
||||
.ansi3 { font-weight: italic; }
|
||||
.ansi4 { text-decoration: underline; }
|
||||
.ansi9 { text-decoration: line-through; }
|
||||
.ansi30 { color: #000316; }
|
||||
.ansi31 { color: #AA0000; }
|
||||
.ansi32 { color: #00AA00; }
|
||||
.ansi33 { color: #AA5500; }
|
||||
.ansi34 { color: #0000AA; }
|
||||
.ansi35 { color: #E850A8; }
|
||||
.ansi36 { color: #00AAAA; }
|
||||
.ansi37 { color: #F5F1DE; }
|
||||
.ansi40 { background-color: #000000; }
|
||||
.ansi41 { background-color: #AA0000; }
|
||||
.ansi42 { background-color: #00AA00; }
|
||||
.ansi43 { background-color: #AA5500; }
|
||||
.ansi44 { background-color: #0000AA; }
|
||||
.ansi45 { background-color: #E850A8; }
|
||||
.ansi46 { background-color: #00AAAA; }
|
||||
.ansi47 { background-color: #F5F1DE; }
|
||||
body.ansi_back pre {
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
div.ansi_back.ansi_dark {
|
||||
padding: 0 8px;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>{% if content_only %}{{ body }}
|
||||
</div>
|
||||
{% else %}
|
||||
</head>
|
||||
<body class="ansi_fore ansi_back{% if dark %} ansi_dark{% endif %}">
|
||||
<pre>{{ body }}</pre>
|
||||
</body>
|
||||
</html>
|
||||
{% endif %}
|
||||
@ -17,16 +17,6 @@ Use the `format` query string parameter to specify the output format.
|
||||
formats, the `start_line` and `end_line` query string parameters can be used
|
||||
to specify a range of line numbers to retrieve.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
# All Rights Reserved.
|
||||
|
||||
# Python
|
||||
import cgi
|
||||
import datetime
|
||||
import dateutil
|
||||
import time
|
||||
@ -33,18 +34,16 @@ 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
|
||||
from awx.main.ha import is_ha_environment
|
||||
from awx.main.redact import UriCleaner
|
||||
from awx.api.authentication import JobTaskAuthentication
|
||||
from awx.api.utils.decorators import paginated
|
||||
from awx.api.generics import get_view_name
|
||||
@ -2202,36 +2201,30 @@ 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)
|
||||
content = UriCleaner.remove_sensitive(content)
|
||||
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 = '<div class="nocode body_foreground body_background">%s</div>' % data
|
||||
else:
|
||||
data = conv.convert(content)
|
||||
# Fix ugly grey background used by default.
|
||||
data = data.replace('.body_background { background-color: #AAAAAA; }',
|
||||
'.body_background { background-color: #f5f5f5; }')
|
||||
|
||||
body = ansiconv.to_html(cgi.escape(content))
|
||||
context = {
|
||||
'title': get_view_name(self.__class__),
|
||||
'body': mark_safe(body),
|
||||
'dark': dark_bg,
|
||||
'content_only': content_only,
|
||||
}
|
||||
data = render_to_string('api/stdout.html', context).strip()
|
||||
|
||||
if request.accepted_renderer.format == 'api':
|
||||
return Response(mark_safe(data))
|
||||
if request.accepted_renderer.format == 'json':
|
||||
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body})
|
||||
return Response(data)
|
||||
elif request.accepted_renderer.format == 'ansi':
|
||||
return Response(UriCleaner.remove_sensitive(unified_job.result_stdout_raw))
|
||||
return Response(unified_job.result_stdout_raw)
|
||||
else:
|
||||
return super(UnifiedJobStdout, self).retrieve(request, *args, **kwargs)
|
||||
|
||||
|
||||
@ -2,7 +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.
|
||||
|
||||
amqp==1.4.5 (amqp/*)
|
||||
ansi2html==1.0.6 (ansi2html/*)
|
||||
ansiconv==1.0.0 (ansiconv.py)
|
||||
anyjson==0.3.3 (anyjson/*)
|
||||
argparse==1.2.1 (argparse.py, needed for Python 2.6 support)
|
||||
azure==0.9.0 (azure/*)
|
||||
@ -21,7 +21,7 @@ django-celery==3.1.10 (djcelery/*)
|
||||
django-crum==0.6.1 (crum/*)
|
||||
django-extensions==1.3.3 (django_extensions/*)
|
||||
django-jsonfield==0.9.12 (jsonfield/*, minor fix in jsonfield/fields.py)
|
||||
django-polymorphic==0.5.3 (polymorphic/*)
|
||||
django_polymorphic==0.5.3 (polymorphic/*)
|
||||
django-split-settings==0.1.1 (split_settings/*)
|
||||
django-taggit==0.11.2 (taggit/*)
|
||||
djangorestframework==2.3.13 (rest_framework/*)
|
||||
@ -36,9 +36,9 @@ kombu==3.0.21 (kombu/*)
|
||||
Markdown==2.4.1 (markdown/*, excluded bin/markdown_py)
|
||||
mock==1.0.1 (mock.py)
|
||||
ordereddict==1.1 (ordereddict.py, needed for Python 2.6 support)
|
||||
os-diskconfig-python-novaclient-ext==0.1.2 (os_diskconfig_python_novaclient_ext/*)
|
||||
os-networksv2-python-novaclient-ext==0.21 (os_networksv2_python_novaclient_ext.py)
|
||||
os-virtual-interfacesv2-python-novaclient-ext==0.15 (os_virtual_interfacesv2_python_novaclient_ext.py)
|
||||
os_diskconfig_python_novaclient_ext==0.1.2 (os_diskconfig_python_novaclient_ext/*)
|
||||
os_networksv2_python_novaclient_ext==0.21 (os_networksv2_python_novaclient_ext.py)
|
||||
os_virtual_interfacesv2_python_novaclient_ext==0.15 (os_virtual_interfacesv2_python_novaclient_ext.py)
|
||||
pbr==0.10.0 (pbr/*)
|
||||
pexpect==3.1 (pexpect/*, excluded pxssh.py, fdpexpect.py, FSM.py, screen.py,
|
||||
ANSI.py)
|
||||
@ -51,8 +51,8 @@ python-swiftclient==2.2.0 (swiftclient/*, excluded bin/swift)
|
||||
pytz==2014.10 (pytz/*)
|
||||
rackspace-auth-openstack==1.3 (rackspace_auth_openstack/*)
|
||||
rackspace-novaclient==1.4 (no files)
|
||||
rax-default-network-flags-python-novaclient-ext==0.2.3 (rax_default_network_flags_python_novaclient_ext/*)
|
||||
rax-scheduled-images-python-novaclient-ext==0.2.1 (rax_scheduled_images_python_novaclient_ext/*)
|
||||
rax_default_network_flags_python_novaclient_ext==0.2.3 (rax_default_network_flags_python_novaclient_ext/*)
|
||||
rax_scheduled_images_python_novaclient_ext==0.2.1 (rax_scheduled_images_python_novaclient_ext/*)
|
||||
requests==2.5.1 (requests/*)
|
||||
setuptools==12.0.5 (setuptools/*, _markerlib/*, pkg_resources/*, easy_install.py)
|
||||
simplejson==3.6.0 (simplejson/*, excluded simplejson/_speedups.so)
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
from ansi2html.converter import Ansi2HTMLConverter
|
||||
__all__ = ['Ansi2HTMLConverter']
|
||||
@ -1,492 +0,0 @@
|
||||
# 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([
|
||||
('&', '&'),
|
||||
('<', '<'),
|
||||
('>', '>'),
|
||||
])
|
||||
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='')
|
||||
@ -1,113 +0,0 @@
|
||||
# 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
|
||||
@ -1,2 +0,0 @@
|
||||
def read_to_unicode(obj):
|
||||
return [line.decode('utf-8') for line in obj.readlines()]
|
||||
127
awx/lib/site-packages/ansiconv.py
Normal file
@ -0,0 +1,127 @@
|
||||
"""
|
||||
Converts ANSI coded text and converts it to either plain text
|
||||
or to HTML.
|
||||
"""
|
||||
import re
|
||||
|
||||
supported_sgr_codes = [1, 3, 4, 9, 30, 31, 32, 33, 34, 35, 36, 37, 40, 41, 42,
|
||||
43, 44, 45, 46, 47]
|
||||
|
||||
|
||||
def to_plain(ansi):
|
||||
"""Takes the given string and strips all ANSI codes out.
|
||||
|
||||
:param ansi: The string to strip
|
||||
:return: The stripped string
|
||||
"""
|
||||
return re.sub(r'\x1B\[[0-9;]*[ABCDEFGHJKSTfmnsulh]', '', ansi)
|
||||
|
||||
|
||||
def to_html(ansi, replace_newline=False):
|
||||
"""Converts the given ANSI string to HTML
|
||||
|
||||
If `replace_newline` is set to True, then all newlines will be
|
||||
replaced with <br />.
|
||||
|
||||
:param ansi: The ANSI text.
|
||||
:param replace_newline: Whether to replace newlines with HTML.
|
||||
:return: The resulting HTML string.
|
||||
"""
|
||||
blocks = ansi.split('\x1B')
|
||||
parsed_blocks = []
|
||||
for block in blocks:
|
||||
command, text = _block_to_html(block)
|
||||
|
||||
# The command "A" means move the cursor up, so we emulate that here.
|
||||
if command == 'A' and len(parsed_blocks) > 0:
|
||||
parsed_blocks.pop()
|
||||
while len(parsed_blocks) > 0 and '\n' not in parsed_blocks[-1]:
|
||||
parsed_blocks.pop()
|
||||
|
||||
parsed_blocks.append(text)
|
||||
|
||||
text = ''.join(parsed_blocks)
|
||||
|
||||
if replace_newline:
|
||||
text = text.replace('\n', '<br />\n')
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def base_css(dark=True):
|
||||
"""Some base CSS with all of the default ANSI styles/colors.
|
||||
|
||||
:param dark: Whether background should be dark or light.
|
||||
:return: A string of CSS
|
||||
"""
|
||||
return "\n".join([
|
||||
css_rule('.ansi_fore', color=('#000000', '#FFFFFF')[dark]),
|
||||
css_rule('.ansi_back', background_color=('#FFFFFF', '#000000')[dark]),
|
||||
css_rule('.ansi1', font_weight='bold'),
|
||||
css_rule('.ansi3', font_weight='italic'),
|
||||
css_rule('.ansi4', text_decoration='underline'),
|
||||
css_rule('.ansi9', text_decoration='line-through'),
|
||||
css_rule('.ansi30', color="#000000"),
|
||||
css_rule('.ansi31', color="#FF0000"),
|
||||
css_rule('.ansi32', color="#00FF00"),
|
||||
css_rule('.ansi33', color="#FFFF00"),
|
||||
css_rule('.ansi34', color="#0000FF"),
|
||||
css_rule('.ansi35', color="#FF00FF"),
|
||||
css_rule('.ansi36', color="#00FFFF"),
|
||||
css_rule('.ansi37', color="#FFFFFF"),
|
||||
css_rule('.ansi40', background_color="#000000"),
|
||||
css_rule('.ansi41', background_color="#FF0000"),
|
||||
css_rule('.ansi42', background_color="#00FF00"),
|
||||
css_rule('.ansi43', background_color="#FFFF00"),
|
||||
css_rule('.ansi44', background_color="#0000FF"),
|
||||
css_rule('.ansi45', background_color="#FF00FF"),
|
||||
css_rule('.ansi46', background_color="#00FFFF"),
|
||||
css_rule('.ansi47', background_color="#FFFFFF")
|
||||
])
|
||||
|
||||
|
||||
def css_rule(class_name, **properties):
|
||||
"""Creates a CSS rule string.
|
||||
|
||||
The named parameters are used as the css properties. Underscores
|
||||
are converted to hyphens.
|
||||
|
||||
:param class_name: The CSS class name
|
||||
:param properties: The properties sent as named params.
|
||||
:return: The CSS string
|
||||
"""
|
||||
prop_str = lambda name, val: name.replace('_', '-') + ': ' + val
|
||||
return '{0} {{ {1}; }}'.format(
|
||||
class_name,
|
||||
'; '.join([prop_str(prop, properties[prop]) for prop in properties])
|
||||
)
|
||||
|
||||
|
||||
def _block_to_html(text):
|
||||
"""Converts the given block of ANSI coded text to HTML.
|
||||
|
||||
The text is only given back as HTML if the ANSI code is at the
|
||||
beginning of the string (e.g. "[0;33mFoobar")
|
||||
|
||||
:param text: The text block to convert.
|
||||
:return: The text as HTML
|
||||
"""
|
||||
match = re.match(r'^\[(?P<code>\d+(?:;\d+)*)?(?P<command>[Am])', text)
|
||||
if match is None:
|
||||
return None, text
|
||||
|
||||
command = match.group('command')
|
||||
text = text[match.end():]
|
||||
|
||||
if match.group('code') is None:
|
||||
return command, text
|
||||
|
||||
classes = []
|
||||
for code in match.group('code').split(';'):
|
||||
if int(code) in supported_sgr_codes:
|
||||
classes.append('ansi{0}'.format(code))
|
||||
|
||||
if classes:
|
||||
text = '<span class="{0}">{1}</span>'.format(' '.join(classes), text)
|
||||
|
||||
return command, text
|
||||
@ -24,6 +24,8 @@ class Command(BaseCommandInstance):
|
||||
self.include_option_hostname_uuid_find()
|
||||
|
||||
def handle(self, *args, **options):
|
||||
super(Command, self).handle(*args, **options)
|
||||
|
||||
# Is there an existing record for this machine? If so, retrieve that record and look for issues.
|
||||
try:
|
||||
# Get the instance.
|
||||
|
||||
@ -15,9 +15,8 @@ from django.conf import settings
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from django.db import transaction, DatabaseError
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from django.utils.timezone import now
|
||||
from django.utils.tzinfo import FixedOffset
|
||||
from django.db import connection
|
||||
from django.db import connection
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
@ -28,7 +27,6 @@ logger = logging.getLogger('awx.main.commands.run_callback_receiver')
|
||||
MAX_REQUESTS = 10000
|
||||
WORKERS = 4
|
||||
|
||||
|
||||
class CallbackReceiver(object):
|
||||
def __init__(self):
|
||||
self.parent_mappings = {}
|
||||
@ -89,7 +87,7 @@ class CallbackReceiver(object):
|
||||
queue_worker[2] = w
|
||||
if workers_changed:
|
||||
signal.signal(signal.SIGINT, shutdown_handler([p[2] for p in worker_queues] + [main_process]))
|
||||
signal.signal(signal.SIGTERM, shutdown_handler([p[2] for p in worker_queues] + [main_process]))
|
||||
signal.signal(signal.SIGTERM, shutdown_handler([p[2] for p in worker_queues] + [main_process]))
|
||||
if not main_process.is_alive():
|
||||
sys.exit(1)
|
||||
time.sleep(0.1)
|
||||
@ -241,7 +239,7 @@ class Command(NoArgsCommand):
|
||||
Save Job Callback receiver (see awx.plugins.callbacks.job_event_callback)
|
||||
Runs as a management command and receives job save events. It then hands
|
||||
them off to worker processors (see Worker) which writes them to the database
|
||||
'''
|
||||
'''
|
||||
help = 'Launch the job callback receiver'
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
|
||||
55
awx/main/management/commands/run_fact_cache_receiver.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# All Rights Reserved
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.management.base import NoArgsCommand
|
||||
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.socket import Socket
|
||||
|
||||
from pymongo import MongoClient
|
||||
|
||||
logger = logging.getLogger('awx.main.commands.run_fact_cache_receiver')
|
||||
|
||||
class FactCacheReceiver(object):
|
||||
|
||||
def __init__(self):
|
||||
self.client = MongoClient('localhost', 27017)
|
||||
|
||||
def process_fact_message(self, message):
|
||||
host = message['host']
|
||||
facts = message['facts']
|
||||
date_key = message['date_key']
|
||||
host_db = self.client.host_facts
|
||||
host_collection = host_db[host]
|
||||
facts.update(dict(tower_host=host, datetime=date_key))
|
||||
rec = host_collection.find({"datetime": date_key})
|
||||
if rec.count():
|
||||
this_fact = rec.next()
|
||||
this_fact.update(facts)
|
||||
host_collection.save(this_fact)
|
||||
else:
|
||||
host_collection.insert(facts)
|
||||
|
||||
def run_receiver(self):
|
||||
with Socket('fact_cache', 'r') as facts:
|
||||
for message in facts.listen():
|
||||
print("Message received: " + str(message))
|
||||
if 'host' not in message or 'facts' not in message or 'date_key' not in message:
|
||||
continue
|
||||
self.process_fact_message(message)
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
'''
|
||||
blah blah
|
||||
'''
|
||||
help = 'Launch the Fact Cache Receiver'
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
fcr = FactCacheReceiver()
|
||||
try:
|
||||
fcr.run_receiver()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
@ -11,7 +11,6 @@ from threading import Thread
|
||||
# Django
|
||||
from django.conf import settings
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from django.utils.timezone import now
|
||||
|
||||
# AWX
|
||||
import awx
|
||||
@ -49,23 +48,21 @@ class TowerBaseNamespace(BaseNamespace):
|
||||
return set(['recv_connect'])
|
||||
|
||||
def valid_user(self):
|
||||
if 'HTTP_COOKIE' not in self.environ:
|
||||
if 'QUERY_STRING' not in self.environ:
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
all_keys = [e.strip() for e in self.environ['HTTP_COOKIE'].split(";")]
|
||||
for each_key in all_keys:
|
||||
k, v = each_key.split("=")
|
||||
if k == "token":
|
||||
token_actual = urllib.unquote_plus(v).decode().replace("\"","")
|
||||
auth_token = AuthToken.objects.filter(key=token_actual)
|
||||
if not auth_token.exists():
|
||||
return False
|
||||
auth_token = auth_token[0]
|
||||
if not auth_token.expired:
|
||||
return auth_token.user
|
||||
else:
|
||||
return False
|
||||
k, v = self.environ['QUERY_STRING'].split("=")
|
||||
if k == "Token":
|
||||
token_actual = urllib.unquote_plus(v).decode().replace("\"","")
|
||||
auth_token = AuthToken.objects.filter(key=token_actual)
|
||||
if not auth_token.exists():
|
||||
return False
|
||||
auth_token = auth_token[0]
|
||||
if not auth_token.expired:
|
||||
return auth_token.user
|
||||
else:
|
||||
return False
|
||||
except Exception, e:
|
||||
logger.error("Exception validating user: " + str(e))
|
||||
return False
|
||||
|
||||
@ -11,7 +11,6 @@ import time
|
||||
# Django
|
||||
from django.conf import settings
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from django.utils.timezone import now
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
|
||||
@ -23,6 +23,7 @@ from awx.main.models.base import * # noqa
|
||||
from awx.main.models.unified_jobs import * # noqa
|
||||
from awx.main.utils import decrypt_field, ignore_inventory_computed_fields
|
||||
from awx.main.utils import emit_websocket_notification
|
||||
from awx.main.redact import PlainTextCleaner
|
||||
|
||||
logger = logging.getLogger('awx.main.models.jobs')
|
||||
|
||||
@ -208,6 +209,15 @@ class JobTemplate(UnifiedJobTemplate, JobOptions):
|
||||
vars.append(survey_element['variable'])
|
||||
return vars
|
||||
|
||||
def survey_password_variables(self):
|
||||
vars = []
|
||||
if self.survey_enabled and 'spec' in self.survey_spec:
|
||||
# Get variables that are type password
|
||||
for survey_element in self.survey_spec['spec']:
|
||||
if survey_element['type'] == 'password':
|
||||
vars.append(survey_element['variable'])
|
||||
return vars
|
||||
|
||||
def survey_variable_validation(self, data):
|
||||
errors = []
|
||||
if not self.survey_enabled:
|
||||
@ -220,7 +230,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions):
|
||||
if survey_element['variable'] not in data and \
|
||||
survey_element['required']:
|
||||
errors.append("'%s' value missing" % survey_element['variable'])
|
||||
elif survey_element['type'] in ["textarea", "text"]:
|
||||
elif survey_element['type'] in ["textarea", "text", "password"]:
|
||||
if survey_element['variable'] in data:
|
||||
if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < survey_element['min']:
|
||||
errors.append("'%s' value %s is too small (must be at least %s)" %
|
||||
@ -452,6 +462,25 @@ class Job(UnifiedJob, JobOptions):
|
||||
evars.update(extra_vars)
|
||||
self.update_fields(extra_vars=json.dumps(evars))
|
||||
|
||||
def _survey_search_and_replace(self, content):
|
||||
# Use job template survey spec to identify password fields.
|
||||
# Then lookup password fields in extra_vars and save the values
|
||||
jt = self.job_template
|
||||
if jt and jt.survey_enabled and 'spec' in jt.survey_spec:
|
||||
# Use password vars to find in extra_vars
|
||||
for key in jt.survey_password_variables():
|
||||
if key in self.extra_vars_dict:
|
||||
content = PlainTextCleaner.remove_sensitive(content, self.extra_vars_dict[key])
|
||||
return content
|
||||
|
||||
def _result_stdout_raw_limited(self, *args, **kwargs):
|
||||
buff, start, end, abs_end = super(Job, self)._result_stdout_raw_limited(*args, **kwargs)
|
||||
return self._survey_search_and_replace(buff), start, end, abs_end
|
||||
|
||||
def _result_stdout_raw(self, *args, **kwargs):
|
||||
content = super(Job, self)._result_stdout_raw(*args, **kwargs)
|
||||
return self._survey_search_and_replace(content)
|
||||
|
||||
def copy(self):
|
||||
presets = {}
|
||||
for kw in self.job_template._get_unified_job_field_names():
|
||||
|
||||
@ -231,7 +231,15 @@ class UnifiedJobTemplate(PolymorphicModel, CommonModelNameNotUnique):
|
||||
if field not in update_fields:
|
||||
update_fields.append(field)
|
||||
# Do the actual save.
|
||||
super(UnifiedJobTemplate, self).save(*args, **kwargs)
|
||||
try:
|
||||
super(UnifiedJobTemplate, self).save(*args, **kwargs)
|
||||
except ValueError:
|
||||
# A fix for https://trello.com/c/S4rU1F21
|
||||
# Does not resolve the root cause. Tis merely a bandaid.
|
||||
if 'scm_delete_on_next_update' in update_fields:
|
||||
update_fields.remove('scm_delete_on_next_update')
|
||||
super(UnifiedJobTemplate, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
def _get_current_status(self):
|
||||
# Override in subclasses as needed.
|
||||
@ -625,16 +633,27 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
else:
|
||||
return StringIO("stdout capture is missing")
|
||||
|
||||
def _escape_ascii(self, content):
|
||||
ansi_escape = re.compile(r'\x1b[^m]*m')
|
||||
return ansi_escape.sub('', content)
|
||||
|
||||
def _result_stdout_raw(self, redact_sensitive=True, escape_ascii=False):
|
||||
content = self.result_stdout_raw_handle().read()
|
||||
if redact_sensitive:
|
||||
content = UriCleaner.remove_sensitive(content)
|
||||
if escape_ascii:
|
||||
content = self._escape_ascii(content)
|
||||
return content
|
||||
|
||||
@property
|
||||
def result_stdout_raw(self):
|
||||
return self.result_stdout_raw_handle().read()
|
||||
return self._result_stdout_raw()
|
||||
|
||||
@property
|
||||
def result_stdout(self):
|
||||
ansi_escape = re.compile(r'\x1b[^m]*m')
|
||||
return ansi_escape.sub('', UriCleaner.remove_sensitive(self.result_stdout_raw))
|
||||
return self._result_stdout_raw(escape_ascii=True)
|
||||
|
||||
def result_stdout_raw_limited(self, start_line=0, end_line=None):
|
||||
def _result_stdout_raw_limited(self, start_line=0, end_line=None, redact_sensitive=True, escape_ascii=False):
|
||||
return_buffer = u""
|
||||
if end_line is not None:
|
||||
end_line = int(end_line)
|
||||
@ -651,12 +670,19 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
end_actual = min(int(end_line), len(stdout_lines))
|
||||
else:
|
||||
end_actual = len(stdout_lines)
|
||||
|
||||
if redact_sensitive:
|
||||
return_buffer = UriCleaner.remove_sensitive(return_buffer)
|
||||
if escape_ascii:
|
||||
return_buffer = self._escape_ascii(return_buffer)
|
||||
|
||||
return return_buffer, start_actual, end_actual, absolute_end
|
||||
|
||||
def result_stdout_raw_limited(self, start_line=0, end_line=None):
|
||||
return self._result_stdout_raw_limited(start_line, end_line)
|
||||
|
||||
def result_stdout_limited(self, start_line=0, end_line=None):
|
||||
ansi_escape = re.compile(r'\x1b[^m]*m')
|
||||
content, start, end, absolute_end = UriCleaner.remove_sensitive(self.result_stdout_raw_limited(start_line, end_line))
|
||||
return ansi_escape.sub('', content), start, end, absolute_end
|
||||
return self._result_stdout_raw_limited(start_line, end_line, escape_ascii=True)
|
||||
|
||||
@property
|
||||
def celery_task(self):
|
||||
@ -729,9 +755,6 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
|
||||
def signal_start(self, **kwargs):
|
||||
"""Notify the task runner system to begin work on this task."""
|
||||
# Sanity check: If we are running unit tests, then run synchronously.
|
||||
if getattr(settings, 'CELERY_UNIT_TEST', False):
|
||||
return self.start(None, **kwargs)
|
||||
|
||||
# Sanity check: Are we able to start the job? If not, do not attempt
|
||||
# to do so.
|
||||
@ -747,6 +770,10 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
|
||||
if 'extra_vars' in kwargs:
|
||||
self.handle_extra_data(kwargs['extra_vars'])
|
||||
|
||||
# Sanity check: If we are running unit tests, then run synchronously.
|
||||
if getattr(settings, 'CELERY_UNIT_TEST', False):
|
||||
return self.start(None, **kwargs)
|
||||
|
||||
# Save the pending status, and inform the SocketIO listener.
|
||||
self.update_fields(start_args=json.dumps(kwargs), status='pending')
|
||||
self.socketio_emit_status("pending")
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import re
|
||||
import urlparse
|
||||
|
||||
REPLACE_STR = '$encrypted$'
|
||||
|
||||
class UriCleaner(object):
|
||||
REPLACE_STR = '$encrypted$'
|
||||
REPLACE_STR = REPLACE_STR
|
||||
# https://regex101.com/r/sV2dO2/2
|
||||
SENSITIVE_URI_PATTERN = re.compile(ur'(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?\xab\xbb\u201c\u201d\u2018\u2019]))', re.MULTILINE)
|
||||
|
||||
@ -51,4 +53,9 @@ class UriCleaner(object):
|
||||
|
||||
return redactedtext
|
||||
|
||||
class PlainTextCleaner(object):
|
||||
REPLACE_STR = REPLACE_STR
|
||||
|
||||
@staticmethod
|
||||
def remove_sensitive(cleartext, sensitive):
|
||||
return re.sub(r'%s' % re.escape(sensitive), '$encrypted$', cleartext)
|
||||
|
||||
@ -63,6 +63,7 @@ class Socket(object):
|
||||
settings.CALLBACK_CONSUMER_PORT),
|
||||
'task_commands': settings.TASK_COMMAND_PORT,
|
||||
'websocket': settings.SOCKETIO_NOTIFICATION_PORT,
|
||||
'fact_cache': settings.FACT_CACHE_PORT,
|
||||
}[self._bucket]
|
||||
|
||||
def connect(self):
|
||||
|
||||
@ -550,6 +550,10 @@ class RunJob(BaseTask):
|
||||
env['JOB_ID'] = str(job.pk)
|
||||
env['INVENTORY_ID'] = str(job.inventory.pk)
|
||||
env['ANSIBLE_CALLBACK_PLUGINS'] = plugin_dir
|
||||
# TODO: env['ANSIBLE_LIBRARY'] # plugins/library
|
||||
# TODO: env['ANSIBLE_CACHE_PLUGINS'] # plugins/fact_caching
|
||||
# TODD: env['ANSIBLE_CACHE_PLUGIN'] # tower
|
||||
# TODO: env['ANSIBLE_CACHE_PLUGIN_CONNECTION'] # connection to tower service
|
||||
env['REST_API_URL'] = settings.INTERNAL_API_URL
|
||||
env['REST_API_TOKEN'] = job.task_auth_token or ''
|
||||
env['CALLBACK_CONSUMER_PORT'] = str(settings.CALLBACK_CONSUMER_PORT)
|
||||
@ -787,14 +791,16 @@ class RunProjectUpdate(BaseTask):
|
||||
scm_password = False
|
||||
if scm_url_parts.scheme != 'svn+ssh':
|
||||
scm_username = False
|
||||
elif scm_url_parts.scheme == 'ssh':
|
||||
elif scm_url_parts.scheme.endswith('ssh'):
|
||||
scm_password = False
|
||||
scm_url = update_scm_url(scm_type, scm_url, scm_username,
|
||||
scm_password)
|
||||
scm_password, scp_format=True)
|
||||
else:
|
||||
scm_url = update_scm_url(scm_type, scm_url, scp_format=True)
|
||||
|
||||
# When using Ansible >= 1.5, pass the extra accept_hostkey parameter to
|
||||
# the git module.
|
||||
if scm_type == 'git' and scm_url_parts.scheme == 'ssh':
|
||||
if scm_type == 'git' and scm_url_parts.scheme.endswith('ssh'):
|
||||
try:
|
||||
if Version(kwargs['ansible_version']) >= Version('1.5'):
|
||||
extra_vars['scm_accept_hostkey'] = 'true'
|
||||
|
||||
@ -13,15 +13,17 @@ import tempfile
|
||||
import time
|
||||
from multiprocessing import Process
|
||||
from subprocess import Popen
|
||||
import re
|
||||
|
||||
# PyYAML
|
||||
import yaml
|
||||
|
||||
# Django
|
||||
import django.test
|
||||
from django.conf import settings, UserSettingsHolder
|
||||
from django.contrib.auth.models import User
|
||||
import django.test
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
@ -211,17 +213,20 @@ class BaseTestMixin(QueueTestMixin):
|
||||
def make_organizations(self, created_by, count=1):
|
||||
results = []
|
||||
for x in range(0, count):
|
||||
self.object_ctr = self.object_ctr + 1
|
||||
results.append(Organization.objects.create(
|
||||
name="org%s-%s" % (x, self.object_ctr), description="org%s" % x, created_by=created_by
|
||||
))
|
||||
results.append(self.make_organization(created_by=created_by, count=x))
|
||||
return results
|
||||
|
||||
def make_organization(self, created_by):
|
||||
return self.make_organizations(created_by, 1)[0]
|
||||
def make_organization(self, created_by, count=1):
|
||||
self.object_ctr = self.object_ctr + 1
|
||||
return Organization.objects.create(
|
||||
name="org%s-%s" % (count, self.object_ctr), description="org%s" % count, created_by=created_by
|
||||
)
|
||||
|
||||
def make_project(self, name, description='', created_by=None,
|
||||
def make_project(self, name=None, description='', created_by=None,
|
||||
playbook_content='', role_playbooks=None, unicode_prefix=True):
|
||||
if not name:
|
||||
name = self.unique_name('Project')
|
||||
|
||||
if not os.path.exists(settings.PROJECTS_ROOT):
|
||||
os.makedirs(settings.PROJECTS_ROOT)
|
||||
# Create temp project directory.
|
||||
@ -283,7 +288,7 @@ class BaseTestMixin(QueueTestMixin):
|
||||
|
||||
return Inventory.objects.create(name=name or self.unique_name('Inventory'), organization=organization, created_by=created_by)
|
||||
|
||||
def make_job_template(self, name=None, created_by=None, organization=None, inventory=None, project=None, playbook=None):
|
||||
def make_job_template(self, name=None, created_by=None, organization=None, inventory=None, project=None, playbook=None, **kwargs):
|
||||
created_by = self.decide_created_by(created_by)
|
||||
if not inventory:
|
||||
inventory = self.make_inventory(organization=organization, created_by=created_by)
|
||||
@ -300,24 +305,47 @@ class BaseTestMixin(QueueTestMixin):
|
||||
if project not in organization.projects.all():
|
||||
organization.projects.add(project)
|
||||
|
||||
return JobTemplate.objects.create(
|
||||
name=name or self.unique_name('JobTemplate'),
|
||||
job_type='check',
|
||||
inventory=inventory,
|
||||
project=project,
|
||||
playbook=project.playbooks[0],
|
||||
host_config_key=settings.SYSTEM_UUID,
|
||||
created_by=created_by,
|
||||
)
|
||||
opts = {
|
||||
'name' : name or self.unique_name('JobTemplate'),
|
||||
'job_type': 'check',
|
||||
'inventory': inventory,
|
||||
'project': project,
|
||||
'host_config_key': settings.SYSTEM_UUID,
|
||||
'created_by': created_by,
|
||||
'playbook': playbook,
|
||||
}
|
||||
opts.update(kwargs)
|
||||
return JobTemplate.objects.create(**opts)
|
||||
|
||||
def make_job(self, job_template=None, created_by=None, inital_state='new'):
|
||||
def make_job(self, job_template=None, created_by=None, inital_state='new', **kwargs):
|
||||
created_by = self.decide_created_by(created_by)
|
||||
if not job_template:
|
||||
job_template = self.make_job_template(created_by=created_by)
|
||||
|
||||
job = job_template.create_job(created_by=created_by)
|
||||
job.status = inital_state
|
||||
return job
|
||||
opts = {
|
||||
'created_by': created_by,
|
||||
'status': inital_state,
|
||||
}
|
||||
opts.update(kwargs)
|
||||
return job_template.create_job(**opts)
|
||||
|
||||
def make_credential(self, **kwargs):
|
||||
opts = {
|
||||
'name': self.unique_name('Credential'),
|
||||
'kind': 'ssh',
|
||||
'user': self.super_django_user,
|
||||
'username': '',
|
||||
'ssh_key_data': '',
|
||||
'ssh_key_unlock': '',
|
||||
'password': '',
|
||||
'sudo_username': '',
|
||||
'sudo_password': '',
|
||||
'su_username': '',
|
||||
'su_password': '',
|
||||
'vault_password': '',
|
||||
}
|
||||
opts.update(kwargs)
|
||||
return Credential.objects.create(**opts)
|
||||
|
||||
def setup_instances(self):
|
||||
instance = Instance(uuid=settings.SYSTEM_UUID, primary=True, hostname='127.0.0.1')
|
||||
@ -419,6 +447,14 @@ class BaseTestMixin(QueueTestMixin):
|
||||
obj = json.loads(response.content)
|
||||
elif response['Content-Type'].startswith('application/yaml'):
|
||||
obj = yaml.safe_load(response.content)
|
||||
elif response['Content-Type'].startswith('text/plain'):
|
||||
obj = {
|
||||
'content': response.content
|
||||
}
|
||||
elif response['Content-Type'].startswith('text/html'):
|
||||
obj = {
|
||||
'content': response.content
|
||||
}
|
||||
else:
|
||||
self.fail('Unsupport response content type %s' % response['Content-Type'])
|
||||
else:
|
||||
@ -556,12 +592,58 @@ class BaseTestMixin(QueueTestMixin):
|
||||
msg += 'fields %s not returned ' % ', '.join(not_returned)
|
||||
self.assertTrue(set(obj.keys()) <= set(fields), msg)
|
||||
|
||||
def check_not_found(self, string, substr):
|
||||
self.assertEqual(string.find(substr), -1, "'%s' found in:\n%s" % (substr, string))
|
||||
def check_not_found(self, string, substr, description=None, word_boundary=False):
|
||||
if word_boundary:
|
||||
count = len(re.findall(r'\b%s\b' % re.escape(substr), string))
|
||||
else:
|
||||
count = string.find(substr)
|
||||
if count == -1:
|
||||
count = 0
|
||||
|
||||
msg = ''
|
||||
if description:
|
||||
msg = 'Test "%s".\n' % description
|
||||
msg += '"%s" found in: "%s"' % (substr, string)
|
||||
self.assertEqual(count, 0, msg)
|
||||
|
||||
def check_found(self, string, substr, count, description=None, word_boundary=False):
|
||||
if word_boundary:
|
||||
count_actual = len(re.findall(r'\b%s\b' % re.escape(substr), string))
|
||||
else:
|
||||
count_actual = string.count(substr)
|
||||
|
||||
msg = ''
|
||||
if description:
|
||||
msg = 'Test "%s".\n' % description
|
||||
msg += 'Found %d occurances of "%s" instead of %d in: "%s"' % (count_actual, substr, count, string)
|
||||
self.assertEqual(count_actual, count, msg)
|
||||
|
||||
def check_job_result(self, job, expected='successful', expect_stdout=True,
|
||||
expect_traceback=False):
|
||||
msg = u'job status is %s, expected %s' % (job.status, expected)
|
||||
msg = u'%s\nargs:\n%s' % (msg, job.job_args)
|
||||
msg = u'%s\nenv:\n%s' % (msg, job.job_env)
|
||||
if job.result_traceback:
|
||||
msg = u'%s\ngot traceback:\n%s' % (msg, job.result_traceback)
|
||||
if job.result_stdout:
|
||||
msg = u'%s\ngot stdout:\n%s' % (msg, job.result_stdout)
|
||||
if isinstance(expected, (list, tuple)):
|
||||
self.assertTrue(job.status in expected)
|
||||
else:
|
||||
self.assertEqual(job.status, expected, msg)
|
||||
if expect_stdout:
|
||||
self.assertTrue(job.result_stdout)
|
||||
else:
|
||||
self.assertTrue(job.result_stdout in ('', 'stdout capture is missing'),
|
||||
u'expected no stdout, got:\n%s' %
|
||||
job.result_stdout)
|
||||
if expect_traceback:
|
||||
self.assertTrue(job.result_traceback)
|
||||
else:
|
||||
self.assertFalse(job.result_traceback,
|
||||
u'expected no traceback, got:\n%s' %
|
||||
job.result_traceback)
|
||||
|
||||
def check_found(self, string, substr, count=1):
|
||||
count_actual = string.count(substr)
|
||||
self.assertEqual(count_actual, count, "Found %d occurances of '%s' instead of %d in:\n%s" % (count_actual, substr, count, string))
|
||||
|
||||
def start_taskmanager(self, command_port):
|
||||
self.start_redis()
|
||||
@ -589,6 +671,17 @@ class BaseLiveServerTest(BaseTestMixin, django.test.LiveServerTestCase):
|
||||
'''
|
||||
Base class for tests requiring a live test server.
|
||||
'''
|
||||
def setUp(self):
|
||||
super(BaseLiveServerTest, self).setUp()
|
||||
settings.INTERNAL_API_URL = self.live_server_url
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True,
|
||||
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True,
|
||||
ANSIBLE_TRANSPORT='local')
|
||||
class BaseJobExecutionTest(QueueStartStopTestMixin, BaseLiveServerTest):
|
||||
'''
|
||||
Base class for celery task tests.
|
||||
'''
|
||||
|
||||
# Helps with test cases.
|
||||
# Save all components of a uri (i.e. scheme, username, password, etc.) so that
|
||||
|
||||
@ -310,7 +310,6 @@ class CleanupJobsTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
self.group.hosts.add(self.host)
|
||||
self.project = None
|
||||
self.credential = None
|
||||
settings.INTERNAL_API_URL = self.live_server_url
|
||||
self.start_queue()
|
||||
|
||||
def tearDown(self):
|
||||
@ -320,18 +319,7 @@ class CleanupJobsTest(BaseCommandMixin, BaseLiveServerTest):
|
||||
shutil.rmtree(self.test_project_path, True)
|
||||
|
||||
def create_test_credential(self, **kwargs):
|
||||
opts = {
|
||||
'name': 'test-creds',
|
||||
'user': self.super_django_user,
|
||||
'ssh_username': '',
|
||||
'ssh_key_data': '',
|
||||
'ssh_key_unlock': '',
|
||||
'ssh_password': '',
|
||||
'sudo_username': '',
|
||||
'sudo_password': '',
|
||||
}
|
||||
opts.update(kwargs)
|
||||
self.credential = Credential.objects.create(**opts)
|
||||
self.credential = self.make_credential(kwargs)
|
||||
return self.credential
|
||||
|
||||
def create_test_project(self, playbook_content):
|
||||
|
||||
3
awx/main/tests/jobs/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
from awx.main.tests.jobs.jobs_monolithic import * # noqa
|
||||
from survey_password import * # noqa
|
||||
from base import * # noqa
|
||||
515
awx/main/tests/jobs/base.py
Normal file
@ -0,0 +1,515 @@
|
||||
# Python
|
||||
import uuid
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.tests.base import BaseTestMixin
|
||||
|
||||
TEST_PLAYBOOK = '''- hosts: all
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: woohoo
|
||||
command: test 1 = 1
|
||||
'''
|
||||
|
||||
class BaseJobTestMixin(BaseTestMixin):
|
||||
|
||||
|
||||
def _create_inventory(self, name, organization, created_by,
|
||||
groups_hosts_dict):
|
||||
'''Helper method for creating inventory with groups and hosts.'''
|
||||
inventory = organization.inventories.create(
|
||||
name=name,
|
||||
created_by=created_by,
|
||||
)
|
||||
for group_name, host_names in groups_hosts_dict.items():
|
||||
group = inventory.groups.create(
|
||||
name=group_name,
|
||||
created_by=created_by,
|
||||
)
|
||||
for host_name in host_names:
|
||||
host = inventory.hosts.create(
|
||||
name=host_name,
|
||||
created_by=created_by,
|
||||
)
|
||||
group.hosts.add(host)
|
||||
return inventory
|
||||
|
||||
def populate(self):
|
||||
# Here's a little story about the Ansible Bread Company, or ABC. They
|
||||
# make machines that make bread - bakers, slicers, and packagers - and
|
||||
# these machines are each controlled by a Linux boxes, which is in turn
|
||||
# managed by Ansible Commander.
|
||||
|
||||
# Sue is the super user. You don't mess with Sue or you're toast. Ha.
|
||||
self.user_sue = self.make_user('sue', super_user=True)
|
||||
|
||||
# There are three organizations in ABC using Ansible, since it's the
|
||||
# best thing for dev ops automation since, well, sliced bread.
|
||||
|
||||
# Engineering - They design and build the machines.
|
||||
self.org_eng = Organization.objects.create(
|
||||
name='engineering',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# Support - They fix it when it's not working.
|
||||
self.org_sup = Organization.objects.create(
|
||||
name='support',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# Operations - They implement the production lines using the machines.
|
||||
self.org_ops = Organization.objects.create(
|
||||
name='operations',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
|
||||
# Alex is Sue's IT assistant who can also administer all of the
|
||||
# organizations.
|
||||
self.user_alex = self.make_user('alex')
|
||||
self.org_eng.admins.add(self.user_alex)
|
||||
self.org_sup.admins.add(self.user_alex)
|
||||
self.org_ops.admins.add(self.user_alex)
|
||||
|
||||
# Bob is the head of engineering. He's an admin for engineering, but
|
||||
# also a user within the operations organization (so he can see the
|
||||
# results if things go wrong in production).
|
||||
self.user_bob = self.make_user('bob')
|
||||
self.org_eng.admins.add(self.user_bob)
|
||||
self.org_ops.users.add(self.user_bob)
|
||||
|
||||
# Chuck is the lead engineer. He has full reign over engineering, but
|
||||
# no other organizations.
|
||||
self.user_chuck = self.make_user('chuck')
|
||||
self.org_eng.admins.add(self.user_chuck)
|
||||
|
||||
# Doug is the other engineer working under Chuck. He can write
|
||||
# playbooks and check them, but Chuck doesn't quite think he's ready to
|
||||
# run them yet. Poor Doug.
|
||||
self.user_doug = self.make_user('doug')
|
||||
self.org_eng.users.add(self.user_doug)
|
||||
|
||||
# Juan is another engineer working under Chuck. He has a little more freedom
|
||||
# to run playbooks but can't create job templates
|
||||
self.user_juan = self.make_user('juan')
|
||||
self.org_eng.users.add(self.user_juan)
|
||||
|
||||
# Hannibal is Chuck's right-hand man. Chuck usually has him create the job
|
||||
# templates that the rest of the team will use
|
||||
self.user_hannibal = self.make_user('hannibal')
|
||||
self.org_eng.users.add(self.user_hannibal)
|
||||
|
||||
# Eve is the head of support. She can also see what goes on in
|
||||
# operations to help them troubleshoot problems.
|
||||
self.user_eve = self.make_user('eve')
|
||||
self.org_sup.admins.add(self.user_eve)
|
||||
self.org_ops.users.add(self.user_eve)
|
||||
|
||||
# Frank is the other support guy.
|
||||
self.user_frank = self.make_user('frank')
|
||||
self.org_sup.users.add(self.user_frank)
|
||||
|
||||
# Greg is the head of operations.
|
||||
self.user_greg = self.make_user('greg')
|
||||
self.org_ops.admins.add(self.user_greg)
|
||||
|
||||
# Holly is an operations engineer.
|
||||
self.user_holly = self.make_user('holly')
|
||||
self.org_ops.users.add(self.user_holly)
|
||||
|
||||
# Iris is another operations engineer.
|
||||
self.user_iris = self.make_user('iris')
|
||||
self.org_ops.users.add(self.user_iris)
|
||||
|
||||
# Randall and Billybob are new ops interns that ops uses to test
|
||||
# their playbooks and inventory
|
||||
self.user_randall = self.make_user('randall')
|
||||
self.org_ops.users.add(self.user_randall)
|
||||
|
||||
# He works with Randall
|
||||
self.user_billybob = self.make_user('billybob')
|
||||
self.org_ops.users.add(self.user_billybob)
|
||||
|
||||
# Jim is the newest intern. He can login, but can't do anything quite yet
|
||||
# except make everyone else fresh coffee.
|
||||
self.user_jim = self.make_user('jim')
|
||||
|
||||
# There are three main projects, one each for the development, test and
|
||||
# production branches of the playbook repository. All three orgs can
|
||||
# use the production branch, support can use the production and testing
|
||||
# branches, and operations can only use the production branch.
|
||||
self.proj_dev = self.make_project('dev', 'development branch',
|
||||
self.user_sue, TEST_PLAYBOOK)
|
||||
self.org_eng.projects.add(self.proj_dev)
|
||||
self.proj_test = self.make_project('test', 'testing branch',
|
||||
self.user_sue, TEST_PLAYBOOK)
|
||||
self.org_eng.projects.add(self.proj_test)
|
||||
self.org_sup.projects.add(self.proj_test)
|
||||
self.proj_prod = self.make_project('prod', 'production branch',
|
||||
self.user_sue, TEST_PLAYBOOK)
|
||||
self.org_eng.projects.add(self.proj_prod)
|
||||
self.org_sup.projects.add(self.proj_prod)
|
||||
self.org_ops.projects.add(self.proj_prod)
|
||||
|
||||
# Operations also has 2 additional projects specific to the east/west
|
||||
# production environments.
|
||||
self.proj_prod_east = self.make_project('prod-east',
|
||||
'east production branch',
|
||||
self.user_sue, TEST_PLAYBOOK)
|
||||
self.org_ops.projects.add(self.proj_prod_east)
|
||||
self.proj_prod_west = self.make_project('prod-west',
|
||||
'west production branch',
|
||||
self.user_sue, TEST_PLAYBOOK)
|
||||
self.org_ops.projects.add(self.proj_prod_west)
|
||||
|
||||
# The engineering organization has a set of servers to use for
|
||||
# development and testing (2 bakers, 1 slicer, 1 packager).
|
||||
self.inv_eng = self._create_inventory(
|
||||
name='engineering environment',
|
||||
organization=self.org_eng,
|
||||
created_by=self.user_sue,
|
||||
groups_hosts_dict={
|
||||
'bakers': ['eng-baker1', 'eng-baker2'],
|
||||
'slicers': ['eng-slicer1'],
|
||||
'packagers': ['eng-packager1'],
|
||||
},
|
||||
)
|
||||
|
||||
# The support organization has a set of servers to use for
|
||||
# testing and reproducing problems from operations (1 baker, 1 slicer,
|
||||
# 1 packager).
|
||||
self.inv_sup = self._create_inventory(
|
||||
name='support environment',
|
||||
organization=self.org_sup,
|
||||
created_by=self.user_sue,
|
||||
groups_hosts_dict={
|
||||
'bakers': ['sup-baker1'],
|
||||
'slicers': ['sup-slicer1'],
|
||||
'packagers': ['sup-packager1'],
|
||||
},
|
||||
)
|
||||
|
||||
# The operations organization manages multiple sets of servers for the
|
||||
# east and west production facilities.
|
||||
self.inv_ops_east = self._create_inventory(
|
||||
name='east production environment',
|
||||
organization=self.org_ops,
|
||||
created_by=self.user_sue,
|
||||
groups_hosts_dict={
|
||||
'bakers': ['east-baker%d' % n for n in range(1, 4)],
|
||||
'slicers': ['east-slicer%d' % n for n in range(1, 3)],
|
||||
'packagers': ['east-packager%d' % n for n in range(1, 3)],
|
||||
},
|
||||
)
|
||||
self.inv_ops_west = self._create_inventory(
|
||||
name='west production environment',
|
||||
organization=self.org_ops,
|
||||
created_by=self.user_sue,
|
||||
groups_hosts_dict={
|
||||
'bakers': ['west-baker%d' % n for n in range(1, 6)],
|
||||
'slicers': ['west-slicer%d' % n for n in range(1, 4)],
|
||||
'packagers': ['west-packager%d' % n for n in range(1, 3)],
|
||||
},
|
||||
)
|
||||
|
||||
# Operations is divided into teams to work on the east/west servers.
|
||||
# Greg and Holly work on east, Greg and iris work on west.
|
||||
self.team_ops_east = self.org_ops.teams.create(
|
||||
name='easterners',
|
||||
created_by=self.user_sue)
|
||||
self.team_ops_east.projects.add(self.proj_prod)
|
||||
self.team_ops_east.projects.add(self.proj_prod_east)
|
||||
self.team_ops_east.users.add(self.user_greg)
|
||||
self.team_ops_east.users.add(self.user_holly)
|
||||
self.team_ops_west = self.org_ops.teams.create(
|
||||
name='westerners',
|
||||
created_by=self.user_sue)
|
||||
self.team_ops_west.projects.add(self.proj_prod)
|
||||
self.team_ops_west.projects.add(self.proj_prod_west)
|
||||
self.team_ops_west.users.add(self.user_greg)
|
||||
self.team_ops_west.users.add(self.user_iris)
|
||||
|
||||
# The south team is no longer active having been folded into the east team
|
||||
self.team_ops_south = self.org_ops.teams.create(
|
||||
name='southerners',
|
||||
created_by=self.user_sue,
|
||||
active=False,
|
||||
)
|
||||
self.team_ops_south.projects.add(self.proj_prod)
|
||||
self.team_ops_south.users.add(self.user_greg)
|
||||
|
||||
# The north team is going to be deleted
|
||||
self.team_ops_north = self.org_ops.teams.create(
|
||||
name='northerners',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.team_ops_north.projects.add(self.proj_prod)
|
||||
self.team_ops_north.users.add(self.user_greg)
|
||||
|
||||
# The testers team are interns that can only check playbooks but can't
|
||||
# run them
|
||||
self.team_ops_testers = self.org_ops.teams.create(
|
||||
name='testers',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.team_ops_testers.projects.add(self.proj_prod)
|
||||
self.team_ops_testers.users.add(self.user_randall)
|
||||
self.team_ops_testers.users.add(self.user_billybob)
|
||||
|
||||
# Each user has his/her own set of credentials.
|
||||
from awx.main.tests.tasks import (TEST_SSH_KEY_DATA,
|
||||
TEST_SSH_KEY_DATA_LOCKED,
|
||||
TEST_SSH_KEY_DATA_UNLOCK)
|
||||
self.cred_sue = self.user_sue.credentials.create(
|
||||
username='sue',
|
||||
password=TEST_SSH_KEY_DATA,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_bob = self.user_bob.credentials.create(
|
||||
username='bob',
|
||||
password='ASK',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_chuck = self.user_chuck.credentials.create(
|
||||
username='chuck',
|
||||
ssh_key_data=TEST_SSH_KEY_DATA,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_doug = self.user_doug.credentials.create(
|
||||
username='doug',
|
||||
password='doug doesn\'t mind his password being saved. this '
|
||||
'is why we dont\'t let doug actually run jobs.',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_eve = self.user_eve.credentials.create(
|
||||
username='eve',
|
||||
password='ASK',
|
||||
sudo_username='root',
|
||||
sudo_password='ASK',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_frank = self.user_frank.credentials.create(
|
||||
username='frank',
|
||||
password='fr@nk the t@nk',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_greg = self.user_greg.credentials.create(
|
||||
username='greg',
|
||||
ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
|
||||
ssh_key_unlock='ASK',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_holly = self.user_holly.credentials.create(
|
||||
username='holly',
|
||||
password='holly rocks',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_iris = self.user_iris.credentials.create(
|
||||
username='iris',
|
||||
password='ASK',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
|
||||
# Each operations team also has shared credentials they can use.
|
||||
self.cred_ops_east = self.team_ops_east.credentials.create(
|
||||
username='east',
|
||||
ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
|
||||
ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK,
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
self.cred_ops_west = self.team_ops_west.credentials.create(
|
||||
username='west',
|
||||
password='Heading270',
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
self.cred_ops_south = self.team_ops_south.credentials.create(
|
||||
username='south',
|
||||
password='Heading180',
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
|
||||
self.cred_ops_north = self.team_ops_north.credentials.create(
|
||||
username='north',
|
||||
password='Heading0',
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
|
||||
self.cred_ops_test = self.team_ops_testers.credentials.create(
|
||||
username='testers',
|
||||
password='HeadingNone',
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
|
||||
self.ops_testers_permission = Permission.objects.create(
|
||||
inventory = self.inv_ops_west,
|
||||
project = self.proj_prod,
|
||||
team = self.team_ops_testers,
|
||||
permission_type = PERM_INVENTORY_CHECK,
|
||||
created_by = self.user_sue
|
||||
)
|
||||
|
||||
self.doug_check_permission = Permission.objects.create(
|
||||
inventory = self.inv_eng,
|
||||
project = self.proj_dev,
|
||||
user = self.user_doug,
|
||||
permission_type = PERM_INVENTORY_CHECK,
|
||||
created_by = self.user_sue
|
||||
)
|
||||
|
||||
self.juan_deploy_permission = Permission.objects.create(
|
||||
inventory = self.inv_eng,
|
||||
project = self.proj_dev,
|
||||
user = self.user_juan,
|
||||
permission_type = PERM_INVENTORY_DEPLOY,
|
||||
created_by = self.user_sue
|
||||
)
|
||||
|
||||
self.hannibal_create_permission = Permission.objects.create(
|
||||
inventory = self.inv_eng,
|
||||
project = self.proj_dev,
|
||||
user = self.user_hannibal,
|
||||
permission_type = PERM_JOBTEMPLATE_CREATE,
|
||||
created_by = self.user_sue
|
||||
)
|
||||
|
||||
# FIXME: Define explicit permissions for tests.
|
||||
# other django user is on the project team and can deploy
|
||||
#self.permission1 = Permission.objects.create(
|
||||
# inventory = self.inventory,
|
||||
# project = self.project,
|
||||
# team = self.team,
|
||||
# permission_type = PERM_INVENTORY_DEPLOY,
|
||||
# created_by = self.normal_django_user
|
||||
#)
|
||||
# individual permission granted to other2 user, can run check mode
|
||||
#self.permission2 = Permission.objects.create(
|
||||
# inventory = self.inventory,
|
||||
# project = self.project,
|
||||
# user = self.other2_django_user,
|
||||
# permission_type = PERM_INVENTORY_CHECK,
|
||||
# created_by = self.normal_django_user
|
||||
#)
|
||||
|
||||
# Engineering has job templates to check/run the dev project onto
|
||||
# their own inventory.
|
||||
self.jt_eng_check = JobTemplate.objects.create(
|
||||
name='eng-dev-check',
|
||||
job_type='check',
|
||||
inventory= self.inv_eng,
|
||||
project=self.proj_dev,
|
||||
playbook=self.proj_dev.playbooks[0],
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_eng_check = self.jt_eng_check.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# credential=self.cred_doug,
|
||||
# )
|
||||
self.jt_eng_run = JobTemplate.objects.create(
|
||||
name='eng-dev-run',
|
||||
job_type='run',
|
||||
inventory= self.inv_eng,
|
||||
project=self.proj_dev,
|
||||
playbook=self.proj_dev.playbooks[0],
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_eng_run = self.jt_eng_run.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# credential=self.cred_chuck,
|
||||
# )
|
||||
|
||||
# Support has job templates to check/run the test project onto
|
||||
# their own inventory.
|
||||
self.jt_sup_check = JobTemplate.objects.create(
|
||||
name='sup-test-check',
|
||||
job_type='check',
|
||||
inventory= self.inv_sup,
|
||||
project=self.proj_test,
|
||||
playbook=self.proj_test.playbooks[0],
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_sup_check = self.jt_sup_check.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# credential=self.cred_frank,
|
||||
# )
|
||||
self.jt_sup_run = JobTemplate.objects.create(
|
||||
name='sup-test-run',
|
||||
job_type='run',
|
||||
inventory= self.inv_sup,
|
||||
project=self.proj_test,
|
||||
playbook=self.proj_test.playbooks[0],
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
credential=self.cred_eve,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_sup_run = self.jt_sup_run.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# )
|
||||
|
||||
# Operations has job templates to check/run the prod project onto
|
||||
# both east and west inventories, by default using the team credential.
|
||||
self.jt_ops_east_check = JobTemplate.objects.create(
|
||||
name='ops-east-prod-check',
|
||||
job_type='check',
|
||||
inventory= self.inv_ops_east,
|
||||
project=self.proj_prod,
|
||||
playbook=self.proj_prod.playbooks[0],
|
||||
credential=self.cred_ops_east,
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_ops_east_check = self.jt_ops_east_check.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# )
|
||||
self.jt_ops_east_run = JobTemplate.objects.create(
|
||||
name='ops-east-prod-run',
|
||||
job_type='run',
|
||||
inventory= self.inv_ops_east,
|
||||
project=self.proj_prod,
|
||||
playbook=self.proj_prod.playbooks[0],
|
||||
credential=self.cred_ops_east,
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_ops_east_run = self.jt_ops_east_run.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# )
|
||||
self.jt_ops_west_check = JobTemplate.objects.create(
|
||||
name='ops-west-prod-check',
|
||||
job_type='check',
|
||||
inventory= self.inv_ops_west,
|
||||
project=self.proj_prod,
|
||||
playbook=self.proj_prod.playbooks[0],
|
||||
credential=self.cred_ops_west,
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_ops_west_check = self.jt_ops_west_check.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# )
|
||||
self.jt_ops_west_run = JobTemplate.objects.create(
|
||||
name='ops-west-prod-run',
|
||||
job_type='run',
|
||||
inventory= self.inv_ops_west,
|
||||
project=self.proj_prod,
|
||||
playbook=self.proj_prod.playbooks[0],
|
||||
credential=self.cred_ops_west,
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_ops_west_run = self.jt_ops_west_run.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# )
|
||||
|
||||
def setUp(self):
|
||||
super(BaseJobTestMixin, self).setUp()
|
||||
self.start_redis()
|
||||
self.setup_instances()
|
||||
self.populate()
|
||||
self.start_queue()
|
||||
|
||||
def tearDown(self):
|
||||
super(BaseJobTestMixin, self).tearDown()
|
||||
self.stop_redis()
|
||||
self.terminate_queue()
|
||||
@ -9,7 +9,6 @@ import struct
|
||||
import threading
|
||||
import time
|
||||
import urlparse
|
||||
import uuid
|
||||
|
||||
# Django
|
||||
import django.test
|
||||
@ -24,17 +23,10 @@ import requests
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.tests.base import BaseTestMixin
|
||||
from base import BaseJobTestMixin
|
||||
|
||||
__all__ = ['JobTemplateTest', 'JobTest', 'JobStartCancelTest',
|
||||
'JobTemplateCallbackTest', 'JobTransactionTest']
|
||||
|
||||
TEST_PLAYBOOK = '''- hosts: all
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: woohoo
|
||||
command: test 1 = 1
|
||||
'''
|
||||
'JobTemplateCallbackTest', 'JobTransactionTest', 'JobTemplateSurveyTest']
|
||||
|
||||
TEST_ASYNC_PLAYBOOK = '''
|
||||
- hosts: all
|
||||
@ -193,508 +185,6 @@ TEST_SURVEY_REQUIREMENTS = '''
|
||||
}
|
||||
'''
|
||||
|
||||
class BaseJobTestMixin(BaseTestMixin):
|
||||
|
||||
|
||||
def _create_inventory(self, name, organization, created_by,
|
||||
groups_hosts_dict):
|
||||
'''Helper method for creating inventory with groups and hosts.'''
|
||||
inventory = organization.inventories.create(
|
||||
name=name,
|
||||
created_by=created_by,
|
||||
)
|
||||
for group_name, host_names in groups_hosts_dict.items():
|
||||
group = inventory.groups.create(
|
||||
name=group_name,
|
||||
created_by=created_by,
|
||||
)
|
||||
for host_name in host_names:
|
||||
host = inventory.hosts.create(
|
||||
name=host_name,
|
||||
created_by=created_by,
|
||||
)
|
||||
group.hosts.add(host)
|
||||
return inventory
|
||||
|
||||
def populate(self):
|
||||
# Here's a little story about the Ansible Bread Company, or ABC. They
|
||||
# make machines that make bread - bakers, slicers, and packagers - and
|
||||
# these machines are each controlled by a Linux boxes, which is in turn
|
||||
# managed by Ansible Commander.
|
||||
|
||||
# Sue is the super user. You don't mess with Sue or you're toast. Ha.
|
||||
self.user_sue = self.make_user('sue', super_user=True)
|
||||
|
||||
# There are three organizations in ABC using Ansible, since it's the
|
||||
# best thing for dev ops automation since, well, sliced bread.
|
||||
|
||||
# Engineering - They design and build the machines.
|
||||
self.org_eng = Organization.objects.create(
|
||||
name='engineering',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# Support - They fix it when it's not working.
|
||||
self.org_sup = Organization.objects.create(
|
||||
name='support',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# Operations - They implement the production lines using the machines.
|
||||
self.org_ops = Organization.objects.create(
|
||||
name='operations',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
|
||||
# Alex is Sue's IT assistant who can also administer all of the
|
||||
# organizations.
|
||||
self.user_alex = self.make_user('alex')
|
||||
self.org_eng.admins.add(self.user_alex)
|
||||
self.org_sup.admins.add(self.user_alex)
|
||||
self.org_ops.admins.add(self.user_alex)
|
||||
|
||||
# Bob is the head of engineering. He's an admin for engineering, but
|
||||
# also a user within the operations organization (so he can see the
|
||||
# results if things go wrong in production).
|
||||
self.user_bob = self.make_user('bob')
|
||||
self.org_eng.admins.add(self.user_bob)
|
||||
self.org_ops.users.add(self.user_bob)
|
||||
|
||||
# Chuck is the lead engineer. He has full reign over engineering, but
|
||||
# no other organizations.
|
||||
self.user_chuck = self.make_user('chuck')
|
||||
self.org_eng.admins.add(self.user_chuck)
|
||||
|
||||
# Doug is the other engineer working under Chuck. He can write
|
||||
# playbooks and check them, but Chuck doesn't quite think he's ready to
|
||||
# run them yet. Poor Doug.
|
||||
self.user_doug = self.make_user('doug')
|
||||
self.org_eng.users.add(self.user_doug)
|
||||
|
||||
# Juan is another engineer working under Chuck. He has a little more freedom
|
||||
# to run playbooks but can't create job templates
|
||||
self.user_juan = self.make_user('juan')
|
||||
self.org_eng.users.add(self.user_juan)
|
||||
|
||||
# Hannibal is Chuck's right-hand man. Chuck usually has him create the job
|
||||
# templates that the rest of the team will use
|
||||
self.user_hannibal = self.make_user('hannibal')
|
||||
self.org_eng.users.add(self.user_hannibal)
|
||||
|
||||
# Eve is the head of support. She can also see what goes on in
|
||||
# operations to help them troubleshoot problems.
|
||||
self.user_eve = self.make_user('eve')
|
||||
self.org_sup.admins.add(self.user_eve)
|
||||
self.org_ops.users.add(self.user_eve)
|
||||
|
||||
# Frank is the other support guy.
|
||||
self.user_frank = self.make_user('frank')
|
||||
self.org_sup.users.add(self.user_frank)
|
||||
|
||||
# Greg is the head of operations.
|
||||
self.user_greg = self.make_user('greg')
|
||||
self.org_ops.admins.add(self.user_greg)
|
||||
|
||||
# Holly is an operations engineer.
|
||||
self.user_holly = self.make_user('holly')
|
||||
self.org_ops.users.add(self.user_holly)
|
||||
|
||||
# Iris is another operations engineer.
|
||||
self.user_iris = self.make_user('iris')
|
||||
self.org_ops.users.add(self.user_iris)
|
||||
|
||||
# Randall and Billybob are new ops interns that ops uses to test
|
||||
# their playbooks and inventory
|
||||
self.user_randall = self.make_user('randall')
|
||||
self.org_ops.users.add(self.user_randall)
|
||||
|
||||
# He works with Randall
|
||||
self.user_billybob = self.make_user('billybob')
|
||||
self.org_ops.users.add(self.user_billybob)
|
||||
|
||||
# Jim is the newest intern. He can login, but can't do anything quite yet
|
||||
# except make everyone else fresh coffee.
|
||||
self.user_jim = self.make_user('jim')
|
||||
|
||||
# There are three main projects, one each for the development, test and
|
||||
# production branches of the playbook repository. All three orgs can
|
||||
# use the production branch, support can use the production and testing
|
||||
# branches, and operations can only use the production branch.
|
||||
self.proj_dev = self.make_project('dev', 'development branch',
|
||||
self.user_sue, TEST_PLAYBOOK)
|
||||
self.org_eng.projects.add(self.proj_dev)
|
||||
self.proj_test = self.make_project('test', 'testing branch',
|
||||
self.user_sue, TEST_PLAYBOOK)
|
||||
self.org_eng.projects.add(self.proj_test)
|
||||
self.org_sup.projects.add(self.proj_test)
|
||||
self.proj_prod = self.make_project('prod', 'production branch',
|
||||
self.user_sue, TEST_PLAYBOOK)
|
||||
self.org_eng.projects.add(self.proj_prod)
|
||||
self.org_sup.projects.add(self.proj_prod)
|
||||
self.org_ops.projects.add(self.proj_prod)
|
||||
|
||||
# Operations also has 2 additional projects specific to the east/west
|
||||
# production environments.
|
||||
self.proj_prod_east = self.make_project('prod-east',
|
||||
'east production branch',
|
||||
self.user_sue, TEST_PLAYBOOK)
|
||||
self.org_ops.projects.add(self.proj_prod_east)
|
||||
self.proj_prod_west = self.make_project('prod-west',
|
||||
'west production branch',
|
||||
self.user_sue, TEST_PLAYBOOK)
|
||||
self.org_ops.projects.add(self.proj_prod_west)
|
||||
|
||||
# The engineering organization has a set of servers to use for
|
||||
# development and testing (2 bakers, 1 slicer, 1 packager).
|
||||
self.inv_eng = self._create_inventory(
|
||||
name='engineering environment',
|
||||
organization=self.org_eng,
|
||||
created_by=self.user_sue,
|
||||
groups_hosts_dict={
|
||||
'bakers': ['eng-baker1', 'eng-baker2'],
|
||||
'slicers': ['eng-slicer1'],
|
||||
'packagers': ['eng-packager1'],
|
||||
},
|
||||
)
|
||||
|
||||
# The support organization has a set of servers to use for
|
||||
# testing and reproducing problems from operations (1 baker, 1 slicer,
|
||||
# 1 packager).
|
||||
self.inv_sup = self._create_inventory(
|
||||
name='support environment',
|
||||
organization=self.org_sup,
|
||||
created_by=self.user_sue,
|
||||
groups_hosts_dict={
|
||||
'bakers': ['sup-baker1'],
|
||||
'slicers': ['sup-slicer1'],
|
||||
'packagers': ['sup-packager1'],
|
||||
},
|
||||
)
|
||||
|
||||
# The operations organization manages multiple sets of servers for the
|
||||
# east and west production facilities.
|
||||
self.inv_ops_east = self._create_inventory(
|
||||
name='east production environment',
|
||||
organization=self.org_ops,
|
||||
created_by=self.user_sue,
|
||||
groups_hosts_dict={
|
||||
'bakers': ['east-baker%d' % n for n in range(1, 4)],
|
||||
'slicers': ['east-slicer%d' % n for n in range(1, 3)],
|
||||
'packagers': ['east-packager%d' % n for n in range(1, 3)],
|
||||
},
|
||||
)
|
||||
self.inv_ops_west = self._create_inventory(
|
||||
name='west production environment',
|
||||
organization=self.org_ops,
|
||||
created_by=self.user_sue,
|
||||
groups_hosts_dict={
|
||||
'bakers': ['west-baker%d' % n for n in range(1, 6)],
|
||||
'slicers': ['west-slicer%d' % n for n in range(1, 4)],
|
||||
'packagers': ['west-packager%d' % n for n in range(1, 3)],
|
||||
},
|
||||
)
|
||||
|
||||
# Operations is divided into teams to work on the east/west servers.
|
||||
# Greg and Holly work on east, Greg and iris work on west.
|
||||
self.team_ops_east = self.org_ops.teams.create(
|
||||
name='easterners',
|
||||
created_by=self.user_sue)
|
||||
self.team_ops_east.projects.add(self.proj_prod)
|
||||
self.team_ops_east.projects.add(self.proj_prod_east)
|
||||
self.team_ops_east.users.add(self.user_greg)
|
||||
self.team_ops_east.users.add(self.user_holly)
|
||||
self.team_ops_west = self.org_ops.teams.create(
|
||||
name='westerners',
|
||||
created_by=self.user_sue)
|
||||
self.team_ops_west.projects.add(self.proj_prod)
|
||||
self.team_ops_west.projects.add(self.proj_prod_west)
|
||||
self.team_ops_west.users.add(self.user_greg)
|
||||
self.team_ops_west.users.add(self.user_iris)
|
||||
|
||||
# The south team is no longer active having been folded into the east team
|
||||
self.team_ops_south = self.org_ops.teams.create(
|
||||
name='southerners',
|
||||
created_by=self.user_sue,
|
||||
active=False,
|
||||
)
|
||||
self.team_ops_south.projects.add(self.proj_prod)
|
||||
self.team_ops_south.users.add(self.user_greg)
|
||||
|
||||
# The north team is going to be deleted
|
||||
self.team_ops_north = self.org_ops.teams.create(
|
||||
name='northerners',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.team_ops_north.projects.add(self.proj_prod)
|
||||
self.team_ops_north.users.add(self.user_greg)
|
||||
|
||||
# The testers team are interns that can only check playbooks but can't
|
||||
# run them
|
||||
self.team_ops_testers = self.org_ops.teams.create(
|
||||
name='testers',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.team_ops_testers.projects.add(self.proj_prod)
|
||||
self.team_ops_testers.users.add(self.user_randall)
|
||||
self.team_ops_testers.users.add(self.user_billybob)
|
||||
|
||||
# Each user has his/her own set of credentials.
|
||||
from awx.main.tests.tasks import (TEST_SSH_KEY_DATA,
|
||||
TEST_SSH_KEY_DATA_LOCKED,
|
||||
TEST_SSH_KEY_DATA_UNLOCK)
|
||||
self.cred_sue = self.user_sue.credentials.create(
|
||||
username='sue',
|
||||
password=TEST_SSH_KEY_DATA,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_bob = self.user_bob.credentials.create(
|
||||
username='bob',
|
||||
password='ASK',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_chuck = self.user_chuck.credentials.create(
|
||||
username='chuck',
|
||||
ssh_key_data=TEST_SSH_KEY_DATA,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_doug = self.user_doug.credentials.create(
|
||||
username='doug',
|
||||
password='doug doesn\'t mind his password being saved. this '
|
||||
'is why we dont\'t let doug actually run jobs.',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_eve = self.user_eve.credentials.create(
|
||||
username='eve',
|
||||
password='ASK',
|
||||
sudo_username='root',
|
||||
sudo_password='ASK',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_frank = self.user_frank.credentials.create(
|
||||
username='frank',
|
||||
password='fr@nk the t@nk',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_greg = self.user_greg.credentials.create(
|
||||
username='greg',
|
||||
ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
|
||||
ssh_key_unlock='ASK',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_holly = self.user_holly.credentials.create(
|
||||
username='holly',
|
||||
password='holly rocks',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
self.cred_iris = self.user_iris.credentials.create(
|
||||
username='iris',
|
||||
password='ASK',
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
|
||||
# Each operations team also has shared credentials they can use.
|
||||
self.cred_ops_east = self.team_ops_east.credentials.create(
|
||||
username='east',
|
||||
ssh_key_data=TEST_SSH_KEY_DATA_LOCKED,
|
||||
ssh_key_unlock=TEST_SSH_KEY_DATA_UNLOCK,
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
self.cred_ops_west = self.team_ops_west.credentials.create(
|
||||
username='west',
|
||||
password='Heading270',
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
self.cred_ops_south = self.team_ops_south.credentials.create(
|
||||
username='south',
|
||||
password='Heading180',
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
|
||||
self.cred_ops_north = self.team_ops_north.credentials.create(
|
||||
username='north',
|
||||
password='Heading0',
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
|
||||
self.cred_ops_test = self.team_ops_testers.credentials.create(
|
||||
username='testers',
|
||||
password='HeadingNone',
|
||||
created_by = self.user_sue,
|
||||
)
|
||||
|
||||
self.ops_testers_permission = Permission.objects.create(
|
||||
inventory = self.inv_ops_west,
|
||||
project = self.proj_prod,
|
||||
team = self.team_ops_testers,
|
||||
permission_type = PERM_INVENTORY_CHECK,
|
||||
created_by = self.user_sue
|
||||
)
|
||||
|
||||
self.doug_check_permission = Permission.objects.create(
|
||||
inventory = self.inv_eng,
|
||||
project = self.proj_dev,
|
||||
user = self.user_doug,
|
||||
permission_type = PERM_INVENTORY_CHECK,
|
||||
created_by = self.user_sue
|
||||
)
|
||||
|
||||
self.juan_deploy_permission = Permission.objects.create(
|
||||
inventory = self.inv_eng,
|
||||
project = self.proj_dev,
|
||||
user = self.user_juan,
|
||||
permission_type = PERM_INVENTORY_DEPLOY,
|
||||
created_by = self.user_sue
|
||||
)
|
||||
|
||||
self.hannibal_create_permission = Permission.objects.create(
|
||||
inventory = self.inv_eng,
|
||||
project = self.proj_dev,
|
||||
user = self.user_hannibal,
|
||||
permission_type = PERM_JOBTEMPLATE_CREATE,
|
||||
created_by = self.user_sue
|
||||
)
|
||||
|
||||
# FIXME: Define explicit permissions for tests.
|
||||
# other django user is on the project team and can deploy
|
||||
#self.permission1 = Permission.objects.create(
|
||||
# inventory = self.inventory,
|
||||
# project = self.project,
|
||||
# team = self.team,
|
||||
# permission_type = PERM_INVENTORY_DEPLOY,
|
||||
# created_by = self.normal_django_user
|
||||
#)
|
||||
# individual permission granted to other2 user, can run check mode
|
||||
#self.permission2 = Permission.objects.create(
|
||||
# inventory = self.inventory,
|
||||
# project = self.project,
|
||||
# user = self.other2_django_user,
|
||||
# permission_type = PERM_INVENTORY_CHECK,
|
||||
# created_by = self.normal_django_user
|
||||
#)
|
||||
|
||||
# Engineering has job templates to check/run the dev project onto
|
||||
# their own inventory.
|
||||
self.jt_eng_check = JobTemplate.objects.create(
|
||||
name='eng-dev-check',
|
||||
job_type='check',
|
||||
inventory= self.inv_eng,
|
||||
project=self.proj_dev,
|
||||
playbook=self.proj_dev.playbooks[0],
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_eng_check = self.jt_eng_check.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# credential=self.cred_doug,
|
||||
# )
|
||||
self.jt_eng_run = JobTemplate.objects.create(
|
||||
name='eng-dev-run',
|
||||
job_type='run',
|
||||
inventory= self.inv_eng,
|
||||
project=self.proj_dev,
|
||||
playbook=self.proj_dev.playbooks[0],
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_eng_run = self.jt_eng_run.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# credential=self.cred_chuck,
|
||||
# )
|
||||
|
||||
# Support has job templates to check/run the test project onto
|
||||
# their own inventory.
|
||||
self.jt_sup_check = JobTemplate.objects.create(
|
||||
name='sup-test-check',
|
||||
job_type='check',
|
||||
inventory= self.inv_sup,
|
||||
project=self.proj_test,
|
||||
playbook=self.proj_test.playbooks[0],
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_sup_check = self.jt_sup_check.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# credential=self.cred_frank,
|
||||
# )
|
||||
self.jt_sup_run = JobTemplate.objects.create(
|
||||
name='sup-test-run',
|
||||
job_type='run',
|
||||
inventory= self.inv_sup,
|
||||
project=self.proj_test,
|
||||
playbook=self.proj_test.playbooks[0],
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
credential=self.cred_eve,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_sup_run = self.jt_sup_run.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# )
|
||||
|
||||
# Operations has job templates to check/run the prod project onto
|
||||
# both east and west inventories, by default using the team credential.
|
||||
self.jt_ops_east_check = JobTemplate.objects.create(
|
||||
name='ops-east-prod-check',
|
||||
job_type='check',
|
||||
inventory= self.inv_ops_east,
|
||||
project=self.proj_prod,
|
||||
playbook=self.proj_prod.playbooks[0],
|
||||
credential=self.cred_ops_east,
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_ops_east_check = self.jt_ops_east_check.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# )
|
||||
self.jt_ops_east_run = JobTemplate.objects.create(
|
||||
name='ops-east-prod-run',
|
||||
job_type='run',
|
||||
inventory= self.inv_ops_east,
|
||||
project=self.proj_prod,
|
||||
playbook=self.proj_prod.playbooks[0],
|
||||
credential=self.cred_ops_east,
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_ops_east_run = self.jt_ops_east_run.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# )
|
||||
self.jt_ops_west_check = JobTemplate.objects.create(
|
||||
name='ops-west-prod-check',
|
||||
job_type='check',
|
||||
inventory= self.inv_ops_west,
|
||||
project=self.proj_prod,
|
||||
playbook=self.proj_prod.playbooks[0],
|
||||
credential=self.cred_ops_west,
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_ops_west_check = self.jt_ops_west_check.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# )
|
||||
self.jt_ops_west_run = JobTemplate.objects.create(
|
||||
name='ops-west-prod-run',
|
||||
job_type='run',
|
||||
inventory= self.inv_ops_west,
|
||||
project=self.proj_prod,
|
||||
playbook=self.proj_prod.playbooks[0],
|
||||
credential=self.cred_ops_west,
|
||||
host_config_key=uuid.uuid4().hex,
|
||||
created_by=self.user_sue,
|
||||
)
|
||||
# self.job_ops_west_run = self.jt_ops_west_run.create_job(
|
||||
# created_by=self.user_sue,
|
||||
# )
|
||||
|
||||
def setUp(self):
|
||||
super(BaseJobTestMixin, self).setUp()
|
||||
self.start_redis()
|
||||
self.setup_instances()
|
||||
self.populate()
|
||||
self.start_queue()
|
||||
|
||||
def tearDown(self):
|
||||
super(BaseJobTestMixin, self).tearDown()
|
||||
self.stop_redis()
|
||||
self.terminate_queue()
|
||||
|
||||
class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
|
||||
|
||||
JOB_TEMPLATE_FIELDS = ('id', 'type', 'url', 'related', 'summary_fields',
|
||||
@ -933,112 +423,6 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
|
||||
|
||||
# FIXME: Check other credentials and optional fields.
|
||||
|
||||
def test_post_job_template_survey(self):
|
||||
url = reverse('api:job_template_list')
|
||||
data = dict(
|
||||
name = 'launched job template',
|
||||
job_type = PERM_INVENTORY_DEPLOY,
|
||||
inventory = self.inv_eng.pk,
|
||||
project = self.proj_dev.pk,
|
||||
playbook = self.proj_dev.playbooks[0],
|
||||
credential = self.cred_sue.pk,
|
||||
survey_enabled = True,
|
||||
)
|
||||
with self.current_user(self.user_sue):
|
||||
response = self.post(url, data, expect=201)
|
||||
new_jt_id = response['id']
|
||||
detail_url = reverse('api:job_template_detail',
|
||||
args=(new_jt_id,))
|
||||
self.assertEquals(response['url'], detail_url)
|
||||
url = reverse('api:job_template_survey_spec', args=(new_jt_id,))
|
||||
with self.current_user(self.user_sue):
|
||||
response = self.post(url, json.loads(TEST_SIMPLE_REQUIRED_SURVEY), expect=200)
|
||||
launch_url = reverse('api:job_template_launch', args=(new_jt_id,))
|
||||
response = self.get(launch_url)
|
||||
self.assertTrue('favorite_color' in response['variables_needed_to_start'])
|
||||
response = self.post(launch_url, dict(extra_vars=dict(favorite_color="green")), expect=202)
|
||||
job = Job.objects.get(pk=response["job"])
|
||||
job_extra = json.loads(job.extra_vars)
|
||||
self.assertTrue("favorite_color" in job_extra)
|
||||
|
||||
with self.current_user(self.user_sue):
|
||||
response = self.post(url, json.loads(TEST_SIMPLE_NONREQUIRED_SURVEY), expect=200)
|
||||
launch_url = reverse('api:job_template_launch', args=(new_jt_id,))
|
||||
response = self.get(launch_url)
|
||||
self.assertTrue(len(response['variables_needed_to_start']) == 0)
|
||||
|
||||
with self.current_user(self.user_sue):
|
||||
response = self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200)
|
||||
launch_url = reverse('api:job_template_launch', args=(new_jt_id,))
|
||||
# Just the required answer should work
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo")), expect=202)
|
||||
# Short answer but requires a long answer
|
||||
self.post(launch_url, dict(extra_vars=dict(long_answer='a', reqd_answer="foo")), expect=400)
|
||||
# Long answer but requires a short answer
|
||||
self.post(launch_url, dict(extra_vars=dict(short_answer='thisissomelongtext', reqd_answer="foo")), expect=400)
|
||||
# Long answer but missing required answer
|
||||
self.post(launch_url, dict(extra_vars=dict(long_answer='thisissomelongtext')), expect=400)
|
||||
# Integer that's not big enough
|
||||
self.post(launch_url, dict(extra_vars=dict(int_answer=0, reqd_answer="foo")), expect=400)
|
||||
# Integer that's too big
|
||||
self.post(launch_url, dict(extra_vars=dict(int_answer=10, reqd_answer="foo")), expect=400)
|
||||
# Integer that's just riiiiight
|
||||
self.post(launch_url, dict(extra_vars=dict(int_answer=3, reqd_answer="foo")), expect=202)
|
||||
# Integer bigger than min with no max defined
|
||||
self.post(launch_url, dict(extra_vars=dict(int_answer_no_max=3, reqd_answer="foo")), expect=202)
|
||||
# Integer answer that's the wrong type
|
||||
self.post(launch_url, dict(extra_vars=dict(int_answer="test", reqd_answer="foo")), expect=400)
|
||||
# Float that's too big
|
||||
self.post(launch_url, dict(extra_vars=dict(float_answer=10.5, reqd_answer="foo")), expect=400)
|
||||
# Float that's too small
|
||||
self.post(launch_url, dict(extra_vars=dict(float_answer=1.995, reqd_answer="foo")), expect=400)
|
||||
# float that's just riiiiight
|
||||
self.post(launch_url, dict(extra_vars=dict(float_answer=2.01, reqd_answer="foo")), expect=202)
|
||||
# float answer that's the wrong type
|
||||
self.post(launch_url, dict(extra_vars=dict(float_answer="test", reqd_answer="foo")), expect=400)
|
||||
# Wrong choice in single choice
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", single_choice="three")), expect=400)
|
||||
# Wrong choice in multi choice
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice=["four"])), expect=400)
|
||||
# Wrong type for multi choicen
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice="two")), expect=400)
|
||||
# Right choice in single choice
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", single_choice="two")), expect=202)
|
||||
# Right choices in multi choice
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice=["one", "two"])), expect=202)
|
||||
# Nested json
|
||||
self.post(launch_url, dict(extra_vars=dict(json_answer=dict(test="val", num=1), reqd_answer="foo")), expect=202)
|
||||
|
||||
# Bob can access and update the survey because he's an org-admin
|
||||
with self.current_user(self.user_bob):
|
||||
self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200)
|
||||
|
||||
# Chuck is the lead engineer and has the right permissions to edit it also
|
||||
with self.current_user(self.user_chuck):
|
||||
self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200)
|
||||
|
||||
# Doug shouldn't be able to access this playbook
|
||||
with self.current_user(self.user_doug):
|
||||
self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=403)
|
||||
|
||||
# Neither can juan because he doesn't have the job template create permission
|
||||
with self.current_user(self.user_juan):
|
||||
self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=403)
|
||||
|
||||
# Bob and chuck can read the template
|
||||
with self.current_user(self.user_bob):
|
||||
self.get(url, expect=200)
|
||||
|
||||
with self.current_user(self.user_chuck):
|
||||
self.get(url, expect=200)
|
||||
|
||||
# Doug and Juan can't
|
||||
with self.current_user(self.user_doug):
|
||||
self.get(url, expect=403)
|
||||
|
||||
with self.current_user(self.user_juan):
|
||||
self.get(url, expect=403)
|
||||
|
||||
def test_launch_job_template(self):
|
||||
url = reverse('api:job_template_list')
|
||||
data = dict(
|
||||
@ -1945,3 +1329,115 @@ class JobTransactionTest(BaseJobTestMixin, django.test.LiveServerTestCase):
|
||||
self.assertEqual(job.status, 'successful', job.result_stdout)
|
||||
self.assertFalse(errors)
|
||||
|
||||
class JobTemplateSurveyTest(BaseJobTestMixin, django.test.TestCase):
|
||||
def setUp(self):
|
||||
super(JobTemplateSurveyTest, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super(JobTemplateSurveyTest, self).tearDown()
|
||||
|
||||
def test_post_job_template_survey(self):
|
||||
url = reverse('api:job_template_list')
|
||||
data = dict(
|
||||
name = 'launched job template',
|
||||
job_type = PERM_INVENTORY_DEPLOY,
|
||||
inventory = self.inv_eng.pk,
|
||||
project = self.proj_dev.pk,
|
||||
playbook = self.proj_dev.playbooks[0],
|
||||
credential = self.cred_sue.pk,
|
||||
survey_enabled = True,
|
||||
)
|
||||
with self.current_user(self.user_sue):
|
||||
response = self.post(url, data, expect=201)
|
||||
new_jt_id = response['id']
|
||||
detail_url = reverse('api:job_template_detail',
|
||||
args=(new_jt_id,))
|
||||
self.assertEquals(response['url'], detail_url)
|
||||
url = reverse('api:job_template_survey_spec', args=(new_jt_id,))
|
||||
with self.current_user(self.user_sue):
|
||||
response = self.post(url, json.loads(TEST_SIMPLE_REQUIRED_SURVEY), expect=200)
|
||||
launch_url = reverse('api:job_template_launch', args=(new_jt_id,))
|
||||
response = self.get(launch_url)
|
||||
self.assertTrue('favorite_color' in response['variables_needed_to_start'])
|
||||
response = self.post(launch_url, dict(extra_vars=dict(favorite_color="green")), expect=202)
|
||||
job = Job.objects.get(pk=response["job"])
|
||||
job_extra = json.loads(job.extra_vars)
|
||||
self.assertTrue("favorite_color" in job_extra)
|
||||
|
||||
with self.current_user(self.user_sue):
|
||||
response = self.post(url, json.loads(TEST_SIMPLE_NONREQUIRED_SURVEY), expect=200)
|
||||
launch_url = reverse('api:job_template_launch', args=(new_jt_id,))
|
||||
response = self.get(launch_url)
|
||||
self.assertTrue(len(response['variables_needed_to_start']) == 0)
|
||||
|
||||
with self.current_user(self.user_sue):
|
||||
response = self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200)
|
||||
launch_url = reverse('api:job_template_launch', args=(new_jt_id,))
|
||||
# Just the required answer should work
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo")), expect=202)
|
||||
# Short answer but requires a long answer
|
||||
self.post(launch_url, dict(extra_vars=dict(long_answer='a', reqd_answer="foo")), expect=400)
|
||||
# Long answer but requires a short answer
|
||||
self.post(launch_url, dict(extra_vars=dict(short_answer='thisissomelongtext', reqd_answer="foo")), expect=400)
|
||||
# Long answer but missing required answer
|
||||
self.post(launch_url, dict(extra_vars=dict(long_answer='thisissomelongtext')), expect=400)
|
||||
# Integer that's not big enough
|
||||
self.post(launch_url, dict(extra_vars=dict(int_answer=0, reqd_answer="foo")), expect=400)
|
||||
# Integer that's too big
|
||||
self.post(launch_url, dict(extra_vars=dict(int_answer=10, reqd_answer="foo")), expect=400)
|
||||
# Integer that's just riiiiight
|
||||
self.post(launch_url, dict(extra_vars=dict(int_answer=3, reqd_answer="foo")), expect=202)
|
||||
# Integer bigger than min with no max defined
|
||||
self.post(launch_url, dict(extra_vars=dict(int_answer_no_max=3, reqd_answer="foo")), expect=202)
|
||||
# Integer answer that's the wrong type
|
||||
self.post(launch_url, dict(extra_vars=dict(int_answer="test", reqd_answer="foo")), expect=400)
|
||||
# Float that's too big
|
||||
self.post(launch_url, dict(extra_vars=dict(float_answer=10.5, reqd_answer="foo")), expect=400)
|
||||
# Float that's too small
|
||||
self.post(launch_url, dict(extra_vars=dict(float_answer=1.995, reqd_answer="foo")), expect=400)
|
||||
# float that's just riiiiight
|
||||
self.post(launch_url, dict(extra_vars=dict(float_answer=2.01, reqd_answer="foo")), expect=202)
|
||||
# float answer that's the wrong type
|
||||
self.post(launch_url, dict(extra_vars=dict(float_answer="test", reqd_answer="foo")), expect=400)
|
||||
# Wrong choice in single choice
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", single_choice="three")), expect=400)
|
||||
# Wrong choice in multi choice
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice=["four"])), expect=400)
|
||||
# Wrong type for multi choicen
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice="two")), expect=400)
|
||||
# Right choice in single choice
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", single_choice="two")), expect=202)
|
||||
# Right choices in multi choice
|
||||
self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice=["one", "two"])), expect=202)
|
||||
# Nested json
|
||||
self.post(launch_url, dict(extra_vars=dict(json_answer=dict(test="val", num=1), reqd_answer="foo")), expect=202)
|
||||
|
||||
# Bob can access and update the survey because he's an org-admin
|
||||
with self.current_user(self.user_bob):
|
||||
self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200)
|
||||
|
||||
# Chuck is the lead engineer and has the right permissions to edit it also
|
||||
with self.current_user(self.user_chuck):
|
||||
self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200)
|
||||
|
||||
# Doug shouldn't be able to access this playbook
|
||||
with self.current_user(self.user_doug):
|
||||
self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=403)
|
||||
|
||||
# Neither can juan because he doesn't have the job template create permission
|
||||
with self.current_user(self.user_juan):
|
||||
self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=403)
|
||||
|
||||
# Bob and chuck can read the template
|
||||
with self.current_user(self.user_bob):
|
||||
self.get(url, expect=200)
|
||||
|
||||
with self.current_user(self.user_chuck):
|
||||
self.get(url, expect=200)
|
||||
|
||||
# Doug and Juan can't
|
||||
with self.current_user(self.user_doug):
|
||||
self.get(url, expect=403)
|
||||
|
||||
with self.current_user(self.user_juan):
|
||||
self.get(url, expect=403)
|
||||
237
awx/main/tests/jobs/survey_password.py
Normal file
@ -0,0 +1,237 @@
|
||||
# Python
|
||||
import json
|
||||
|
||||
# Django
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.tests.base import BaseTest, QueueStartStopTestMixin
|
||||
|
||||
__all__ = ['SurveyPasswordRedactedTest']
|
||||
|
||||
PASSWORD="5m/h"
|
||||
ENCRYPTED_STR='$encrypted$'
|
||||
|
||||
TEST_PLAYBOOK = u'''---
|
||||
- name: test success
|
||||
hosts: test-group
|
||||
gather_facts: True
|
||||
tasks:
|
||||
- name: should pass
|
||||
command: echo {{ %s }}
|
||||
''' % ('spot_speed')
|
||||
|
||||
|
||||
TEST_SIMPLE_SURVEY = '''
|
||||
{
|
||||
"name": "Simple",
|
||||
"description": "Description",
|
||||
"spec": [
|
||||
{
|
||||
"type": "password",
|
||||
"question_name": "spots speed",
|
||||
"question_description": "How fast can spot run?",
|
||||
"variable": "%s",
|
||||
"choices": "",
|
||||
"min": "",
|
||||
"max": "",
|
||||
"required": false,
|
||||
"default": "%s"
|
||||
}
|
||||
]
|
||||
}
|
||||
''' % ('spot_speed', PASSWORD)
|
||||
|
||||
TEST_COMPLEX_SURVEY = '''
|
||||
{
|
||||
"name": "Simple",
|
||||
"description": "Description",
|
||||
"spec": [
|
||||
{
|
||||
"type": "password",
|
||||
"question_name": "spots speed",
|
||||
"question_description": "How fast can spot run?",
|
||||
"variable": "spot_speed",
|
||||
"choices": "",
|
||||
"min": "",
|
||||
"max": "",
|
||||
"required": false,
|
||||
"default": "0m/h"
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"question_name": "ssn",
|
||||
"question_description": "What's your social security number?",
|
||||
"variable": "ssn",
|
||||
"choices": "",
|
||||
"min": "",
|
||||
"max": "",
|
||||
"required": false,
|
||||
"default": "999-99-9999"
|
||||
},
|
||||
{
|
||||
"type": "password",
|
||||
"question_name": "bday",
|
||||
"question_description": "What's your birth day?",
|
||||
"variable": "bday",
|
||||
"choices": "",
|
||||
"min": "",
|
||||
"max": "",
|
||||
"required": false,
|
||||
"default": "1/1/1970"
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
TEST_SINGLE_PASSWORDS = [
|
||||
{
|
||||
'description': 'Single instance with a . after',
|
||||
'text' : 'See spot. See spot run. See spot run %s. That is a fast run.' % PASSWORD,
|
||||
'passwords': [PASSWORD],
|
||||
'occurances': 1,
|
||||
},
|
||||
{
|
||||
'description': 'Single instance with , after',
|
||||
'text': 'Spot goes %s, at a fast pace' % PASSWORD,
|
||||
'passwords': [PASSWORD],
|
||||
'occurances': 1,
|
||||
},
|
||||
{
|
||||
'description': 'Single instance with a space after',
|
||||
'text': 'Is %s very fast?' % PASSWORD,
|
||||
'passwords': [PASSWORD],
|
||||
'occurances': 1,
|
||||
},
|
||||
{
|
||||
'description': 'Many instances, also with newline',
|
||||
'text': 'I think %s is very very fast. If I ran %s for 4 hours how many hours would I run?.\nTrick question. %s for 4 hours would result in running for 4 hours' % (PASSWORD, PASSWORD, PASSWORD),
|
||||
'passwords': [PASSWORD],
|
||||
'occurances': 3,
|
||||
},
|
||||
]
|
||||
passwd = 'my!@#$%^pass&*()_+'
|
||||
TEST_SINGLE_PASSWORDS.append({
|
||||
'description': 'password includes characters not in a-z 0-9 range',
|
||||
'passwords': [passwd],
|
||||
'text': 'Text is fun yeah with passwords %s.' % passwd,
|
||||
'occurances': 1
|
||||
})
|
||||
|
||||
# 3 because 3 password fields in spec TEST_COMPLEX_SURVEY
|
||||
TEST_MULTIPLE_PASSWORDS = []
|
||||
passwds = [ '65km/s', '545-83-4534', '7/4/2002']
|
||||
TEST_MULTIPLE_PASSWORDS.append({
|
||||
'description': '3 different passwords each used once',
|
||||
'text': 'Spot runs %s. John has an ss of %s and is born on %s.' % (passwds[0], passwds[1], passwds[2]),
|
||||
'passwords': passwds,
|
||||
'occurances': 3,
|
||||
})
|
||||
|
||||
TESTS = {
|
||||
'simple': {
|
||||
'survey' : json.loads(TEST_SIMPLE_SURVEY),
|
||||
'tests' : TEST_SINGLE_PASSWORDS,
|
||||
},
|
||||
'complex': {
|
||||
'survey' : json.loads(TEST_COMPLEX_SURVEY),
|
||||
'tests' : TEST_MULTIPLE_PASSWORDS,
|
||||
}
|
||||
}
|
||||
|
||||
class SurveyPasswordBaseTest(BaseTest, QueueStartStopTestMixin):
|
||||
def setUp(self):
|
||||
super(SurveyPasswordBaseTest, self).setUp()
|
||||
self.setup_instances()
|
||||
self.setup_users()
|
||||
|
||||
def check_passwords_redacted(self, test, response):
|
||||
self.assertIsNotNone(response['content'])
|
||||
for password in test['passwords']:
|
||||
self.check_not_found(response['content'], password, test['description'], word_boundary=True)
|
||||
|
||||
self.check_found(response['content'], ENCRYPTED_STR, test['occurances'], test['description'])
|
||||
|
||||
# TODO: A more complete test would ensure that the variable value isn't found
|
||||
def check_extra_vars_redacted(self, test, response):
|
||||
self.assertIsNotNone(response)
|
||||
# Ensure that all extra_vars of type password have the value '$encrypted$'
|
||||
vars = []
|
||||
for question in test['survey']['spec']:
|
||||
if question['type'] == 'password':
|
||||
vars.append(question['variable'])
|
||||
|
||||
extra_vars = json.loads(response['extra_vars'])
|
||||
for var in vars:
|
||||
self.assertIn(var, extra_vars, 'Variable "%s" should exist in "%s"' % (var, extra_vars))
|
||||
self.assertEqual(extra_vars[var], ENCRYPTED_STR)
|
||||
|
||||
def _get_url_job_stdout(self, job):
|
||||
url = reverse('api:job_stdout', args=(job.pk,))
|
||||
return self.get(url, expect=200, auth=self.get_super_credentials(), accept='application/json')
|
||||
|
||||
def _get_url_job_details(self, job):
|
||||
url = reverse('api:job_detail', args=(job.pk,))
|
||||
return self.get(url, expect=200, auth=self.get_super_credentials(), accept='application/json')
|
||||
|
||||
class SurveyPasswordRedactedTest(SurveyPasswordBaseTest):
|
||||
'''
|
||||
Transpose TEST[]['tests'] to the below format. A more flat format."
|
||||
[
|
||||
{
|
||||
'text': '...',
|
||||
'description': '...',
|
||||
...,
|
||||
'job': '...',
|
||||
'survey': '...'
|
||||
},
|
||||
]
|
||||
'''
|
||||
def setup_test(self, test_name):
|
||||
blueprint = TESTS[test_name]
|
||||
self.tests[test_name] = []
|
||||
|
||||
job_template = self.make_job_template(survey_enabled=True, survey_spec=blueprint['survey'])
|
||||
for test in blueprint['tests']:
|
||||
test = dict(test)
|
||||
extra_vars = {}
|
||||
|
||||
# build extra_vars from spec variables and passwords
|
||||
for x in range(0, len(blueprint['survey']['spec'])):
|
||||
question = blueprint['survey']['spec'][x]
|
||||
extra_vars[question['variable']] = test['passwords'][x]
|
||||
|
||||
job = self.make_job(job_template=job_template)
|
||||
job.extra_vars = json.dumps(extra_vars)
|
||||
job.result_stdout_text = test['text']
|
||||
job.save()
|
||||
test['job'] = job
|
||||
test['survey'] = blueprint['survey']
|
||||
self.tests[test_name].append(test)
|
||||
|
||||
def setUp(self):
|
||||
super(SurveyPasswordRedactedTest, self).setUp()
|
||||
|
||||
self.tests = {}
|
||||
self.setup_test('simple')
|
||||
self.setup_test('complex')
|
||||
|
||||
# should redact single variable survey
|
||||
def test_redact_stdout_simple_survey(self):
|
||||
for test in self.tests['simple']:
|
||||
response = self._get_url_job_stdout(test['job'])
|
||||
self.check_passwords_redacted(test, response)
|
||||
|
||||
# should redact multiple variables survey
|
||||
def test_redact_stdout_complex_survey(self):
|
||||
for test in self.tests['complex']:
|
||||
response = self._get_url_job_stdout(test['job'])
|
||||
self.check_passwords_redacted(test, response)
|
||||
|
||||
# should redact values in extra_vars
|
||||
def test_redact_job_extra_vars(self):
|
||||
for test in self.tests['simple']:
|
||||
response = self._get_url_job_details(test['job'])
|
||||
self.check_extra_vars_redacted(test, response)
|
||||
@ -816,9 +816,15 @@ class ProjectUpdatesTest(BaseTransactionTest):
|
||||
# - rsync://host.xz/path/to/repo.git/
|
||||
('git', 'rsync://host.xz/path/to/repo.git/', ValueError, ValueError, ValueError),
|
||||
# - [user@]host.xz:path/to/repo.git/ (SCP style)
|
||||
('git', 'host.xz:path/to/repo.git/', 'ssh://host.xz/path/to/repo.git/', 'ssh://testuser@host.xz/path/to/repo.git/', 'ssh://testuser:testpass@host.xz/path/to/repo.git/'),
|
||||
('git', 'user@host.xz:path/to/repo.git/', 'ssh://user@host.xz/path/to/repo.git/', 'ssh://testuser@host.xz/path/to/repo.git/', 'ssh://testuser:testpass@host.xz/path/to/repo.git/'),
|
||||
('git', 'user:pass@host.xz:path/to/repo.git/', 'ssh://user:pass@host.xz/path/to/repo.git/', 'ssh://testuser:pass@host.xz/path/to/repo.git/', 'ssh://testuser:testpass@host.xz/path/to/repo.git/'),
|
||||
('git', 'host.xz:path/to/repo.git/', 'git+ssh://host.xz/path/to/repo.git/', 'git+ssh://testuser@host.xz/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/path/to/repo.git/'),
|
||||
('git', 'user@host.xz:path/to/repo.git/', 'git+ssh://user@host.xz/path/to/repo.git/', 'git+ssh://testuser@host.xz/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/path/to/repo.git/'),
|
||||
('git', 'user:pass@host.xz:path/to/repo.git/', 'git+ssh://user:pass@host.xz/path/to/repo.git/', 'git+ssh://testuser:pass@host.xz/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/path/to/repo.git/'),
|
||||
('git', 'host.xz:~/path/to/repo.git/', 'git+ssh://host.xz/~/path/to/repo.git/', 'git+ssh://testuser@host.xz/~/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/~/path/to/repo.git/'),
|
||||
('git', 'user@host.xz:~/path/to/repo.git/', 'git+ssh://user@host.xz/~/path/to/repo.git/', 'git+ssh://testuser@host.xz/~/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/~/path/to/repo.git/'),
|
||||
('git', 'user:pass@host.xz:~/path/to/repo.git/', 'git+ssh://user:pass@host.xz/~/path/to/repo.git/', 'git+ssh://testuser:pass@host.xz/~/path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz/~/path/to/repo.git/'),
|
||||
('git', 'host.xz:/path/to/repo.git/', 'git+ssh://host.xz//path/to/repo.git/', 'git+ssh://testuser@host.xz//path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz//path/to/repo.git/'),
|
||||
('git', 'user@host.xz:/path/to/repo.git/', 'git+ssh://user@host.xz//path/to/repo.git/', 'git+ssh://testuser@host.xz//path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz//path/to/repo.git/'),
|
||||
('git', 'user:pass@host.xz:/path/to/repo.git/', 'git+ssh://user:pass@host.xz//path/to/repo.git/', 'git+ssh://testuser:pass@host.xz//path/to/repo.git/', 'git+ssh://testuser:testpass@host.xz//path/to/repo.git/'),
|
||||
# - /path/to/repo.git/ (local file)
|
||||
('git', '/path/to/repo.git', ValueError, ValueError, ValueError),
|
||||
('git', 'path/to/repo.git', ValueError, ValueError, ValueError),
|
||||
@ -829,7 +835,7 @@ class ProjectUpdatesTest(BaseTransactionTest):
|
||||
('git', 'ssh:github.com:ansible/ansible-examples.git', ValueError, ValueError, ValueError),
|
||||
('git', 'ssh://github.com:ansible/ansible-examples.git', ValueError, ValueError, ValueError),
|
||||
# Special case for github URLs:
|
||||
('git', 'git@github.com:ansible/ansible-examples.git', 'ssh://git@github.com/ansible/ansible-examples.git', ValueError, ValueError),
|
||||
('git', 'git@github.com:ansible/ansible-examples.git', 'git+ssh://git@github.com/ansible/ansible-examples.git', ValueError, ValueError),
|
||||
('git', 'bob@github.com:ansible/ansible-examples.git', ValueError, ValueError, ValueError),
|
||||
# Special case for bitbucket URLs:
|
||||
('git', 'ssh://git@bitbucket.org/foo/bar.git', None, ValueError, ValueError),
|
||||
@ -926,39 +932,56 @@ class ProjectUpdatesTest(BaseTransactionTest):
|
||||
('svn', 'svn+ssh://user@host.xz:1022/path/to/repo', None, 'svn+ssh://testuser@host.xz:1022/path/to/repo', 'svn+ssh://testuser:testpass@host.xz:1022/path/to/repo'),
|
||||
('svn', 'svn+ssh://user:pass@host.xz/path/to/repo/', None, 'svn+ssh://testuser:pass@host.xz/path/to/repo/', 'svn+ssh://testuser:testpass@host.xz/path/to/repo/'),
|
||||
('svn', 'svn+ssh://user:pass@host.xz:1022/path/to/repo', None, 'svn+ssh://testuser:pass@host.xz:1022/path/to/repo', 'svn+ssh://testuser:testpass@host.xz:1022/path/to/repo'),
|
||||
|
||||
# FIXME: Add some invalid URLs.
|
||||
]
|
||||
|
||||
# Some invalid URLs.
|
||||
for scm_type in ('git', 'svn', 'hg'):
|
||||
urls_to_test.append((scm_type, 'host', ValueError, ValueError, ValueError))
|
||||
urls_to_test.append((scm_type, '/path', ValueError, ValueError, ValueError))
|
||||
urls_to_test.append((scm_type, 'mailto:joe@example.com', ValueError, ValueError, ValueError))
|
||||
urls_to_test.append((scm_type, 'telnet://host.xz/path/to/repo', ValueError, ValueError, ValueError))
|
||||
|
||||
def is_exception(e):
|
||||
return bool(isinstance(e, Exception) or
|
||||
(isinstance(e, type) and issubclass(e, Exception)))
|
||||
return bool(isinstance(e, Exception) or (isinstance(e, type) and issubclass(e, Exception)))
|
||||
|
||||
for url_opts in urls_to_test:
|
||||
scm_type, url, new_url, new_url_u, new_url_up = url_opts
|
||||
#print scm_type, url
|
||||
new_url = new_url or url
|
||||
new_url_u = new_url_u or url
|
||||
new_url_up = new_url_up or url
|
||||
|
||||
# Check existing URL as-is.
|
||||
if is_exception(new_url):
|
||||
self.assertRaises(new_url, update_scm_url, scm_type, url)
|
||||
else:
|
||||
updated_url = update_scm_url(scm_type, url)
|
||||
self.assertEqual(new_url, updated_url)
|
||||
if updated_url.startswith('git+ssh://'):
|
||||
new_url2 = new_url.replace('git+ssh://', '', 1).replace('/', ':', 1)
|
||||
updated_url2 = update_scm_url(scm_type, url, scp_format=True)
|
||||
self.assertEqual(new_url2, updated_url2)
|
||||
|
||||
# Check URL with username replaced.
|
||||
if is_exception(new_url_u):
|
||||
self.assertRaises(new_url_u, update_scm_url, scm_type,
|
||||
url, username='testuser')
|
||||
self.assertRaises(new_url_u, update_scm_url, scm_type, url, username='testuser')
|
||||
else:
|
||||
updated_url = update_scm_url(scm_type, url,
|
||||
username='testuser')
|
||||
updated_url = update_scm_url(scm_type, url, username='testuser')
|
||||
self.assertEqual(new_url_u, updated_url)
|
||||
if updated_url.startswith('git+ssh://'):
|
||||
new_url2 = new_url_u.replace('git+ssh://', '', 1).replace('/', ':', 1)
|
||||
updated_url2 = update_scm_url(scm_type, url, username='testuser', scp_format=True)
|
||||
self.assertEqual(new_url2, updated_url2)
|
||||
|
||||
# Check URL with username and password replaced.
|
||||
if is_exception(new_url_up):
|
||||
self.assertRaises(new_url_up, update_scm_url, scm_type,
|
||||
url, username='testuser', password='testpass')
|
||||
self.assertRaises(new_url_up, update_scm_url, scm_type, url, username='testuser', password='testpass')
|
||||
else:
|
||||
updated_url = update_scm_url(scm_type, url,
|
||||
username='testuser',
|
||||
password='testpass')
|
||||
updated_url = update_scm_url(scm_type, url, username='testuser', password='testpass')
|
||||
self.assertEqual(new_url_up, updated_url)
|
||||
if updated_url.startswith('git+ssh://'):
|
||||
new_url2 = new_url_up.replace('git+ssh://', '', 1).replace('/', ':', 1)
|
||||
updated_url2 = update_scm_url(scm_type, url, username='testuser', password='testpass', scp_format=True)
|
||||
self.assertEqual(new_url2, updated_url2)
|
||||
|
||||
def is_public_key_in_authorized_keys(self):
|
||||
auth_keys = set()
|
||||
|
||||
@ -87,9 +87,9 @@ class UriCleanTests(BaseTest):
|
||||
for uri in TEST_URIS:
|
||||
redacted_str = UriCleaner.remove_sensitive(str(uri))
|
||||
if uri.username:
|
||||
self.check_not_found(redacted_str, uri.username)
|
||||
self.check_not_found(redacted_str, uri.username, uri.description)
|
||||
if uri.password:
|
||||
self.check_not_found(redacted_str, uri.password)
|
||||
self.check_not_found(redacted_str, uri.password, uri.description)
|
||||
|
||||
# should replace secret data with safe string, UriCleaner.REPLACE_STR
|
||||
def test_uri_scm_simple_replaced(self):
|
||||
@ -107,9 +107,9 @@ class UriCleanTests(BaseTest):
|
||||
|
||||
redacted_str = UriCleaner.remove_sensitive(str(uri))
|
||||
if uri.username:
|
||||
self.check_not_found(redacted_str, uri.username)
|
||||
self.check_not_found(redacted_str, uri.username, uri.description)
|
||||
if uri.password:
|
||||
self.check_not_found(redacted_str, uri.password)
|
||||
self.check_not_found(redacted_str, uri.password, uri.description)
|
||||
|
||||
# should replace multiple secret data with safe string
|
||||
def test_uri_scm_multiple_replaced(self):
|
||||
@ -131,8 +131,8 @@ class UriCleanTests(BaseTest):
|
||||
for test_data in TEST_CLEARTEXT:
|
||||
uri = test_data['uri']
|
||||
redacted_str = UriCleaner.remove_sensitive(test_data['text'])
|
||||
self.check_not_found(redacted_str, uri.username)
|
||||
self.check_not_found(redacted_str, uri.password)
|
||||
self.check_not_found(redacted_str, uri.username, uri.description)
|
||||
self.check_not_found(redacted_str, uri.password, uri.description)
|
||||
# Ensure the host didn't get redacted
|
||||
self.check_found(redacted_str, uri.host, count=test_data['host_occurrences'])
|
||||
self.check_found(redacted_str, uri.host, test_data['host_occurrences'], uri.description)
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ import unittest
|
||||
|
||||
# Django
|
||||
from django.conf import settings
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.timezone import now
|
||||
|
||||
# Django-CRUM
|
||||
@ -21,7 +20,7 @@ from crum import impersonate
|
||||
|
||||
# AWX
|
||||
from awx.main.models import * # noqa
|
||||
from awx.main.tests.base import BaseLiveServerTest
|
||||
from awx.main.tests.base import BaseJobExecutionTest
|
||||
|
||||
TEST_PLAYBOOK = u'''
|
||||
- name: test success
|
||||
@ -341,15 +340,7 @@ L5Hj+B02+FAiz8zVGumbVykvPtzgTb0E+0rJKNO0/EgGqWsk/oC0
|
||||
|
||||
TEST_SSH_KEY_DATA_UNLOCK = 'unlockme'
|
||||
|
||||
@override_settings(CELERY_ALWAYS_EAGER=True,
|
||||
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
|
||||
class BaseCeleryTest(BaseLiveServerTest):
|
||||
'''
|
||||
Base class for celery task tests.
|
||||
'''
|
||||
|
||||
@override_settings(ANSIBLE_TRANSPORT='local')
|
||||
class RunJobTest(BaseCeleryTest):
|
||||
class RunJobTest(BaseJobExecutionTest):
|
||||
'''
|
||||
Test cases for RunJob celery task.
|
||||
'''
|
||||
@ -371,31 +362,14 @@ class RunJobTest(BaseCeleryTest):
|
||||
self.credential = None
|
||||
self.cloud_credential = None
|
||||
settings.INTERNAL_API_URL = self.live_server_url
|
||||
self.start_queue()
|
||||
|
||||
def tearDown(self):
|
||||
super(RunJobTest, self).tearDown()
|
||||
if self.test_project_path:
|
||||
shutil.rmtree(self.test_project_path, True)
|
||||
self.terminate_queue()
|
||||
|
||||
def create_test_credential(self, **kwargs):
|
||||
opts = {
|
||||
'name': 'test-creds',
|
||||
'kind': 'ssh',
|
||||
'user': self.super_django_user,
|
||||
'username': '',
|
||||
'ssh_key_data': '',
|
||||
'ssh_key_unlock': '',
|
||||
'password': '',
|
||||
'sudo_username': '',
|
||||
'sudo_password': '',
|
||||
'su_username': '',
|
||||
'su_password': '',
|
||||
'vault_password': '',
|
||||
}
|
||||
opts.update(kwargs)
|
||||
self.credential = Credential.objects.create(**opts)
|
||||
self.credential = self.make_credential(**kwargs)
|
||||
return self.credential
|
||||
|
||||
def create_test_cloud_credential(self, **kwargs):
|
||||
@ -429,7 +403,7 @@ class RunJobTest(BaseCeleryTest):
|
||||
except (AttributeError, IndexError):
|
||||
pass
|
||||
opts.update(kwargs)
|
||||
self.job_template = JobTemplate.objects.create(**opts)
|
||||
self.job_template = self.make_job_template(**opts)
|
||||
return self.job_template
|
||||
|
||||
def create_test_job(self, **kwargs):
|
||||
@ -453,32 +427,6 @@ class RunJobTest(BaseCeleryTest):
|
||||
self.job = Job.objects.create(**opts)
|
||||
return self.job
|
||||
|
||||
def check_job_result(self, job, expected='successful', expect_stdout=True,
|
||||
expect_traceback=False):
|
||||
msg = u'job status is %s, expected %s' % (job.status, expected)
|
||||
msg = u'%s\nargs:\n%s' % (msg, job.job_args)
|
||||
msg = u'%s\nenv:\n%s' % (msg, job.job_env)
|
||||
if job.result_traceback:
|
||||
msg = u'%s\ngot traceback:\n%s' % (msg, job.result_traceback)
|
||||
if job.result_stdout:
|
||||
msg = u'%s\ngot stdout:\n%s' % (msg, job.result_stdout)
|
||||
if isinstance(expected, (list, tuple)):
|
||||
self.assertTrue(job.status in expected)
|
||||
else:
|
||||
self.assertEqual(job.status, expected, msg)
|
||||
if expect_stdout:
|
||||
self.assertTrue(job.result_stdout)
|
||||
else:
|
||||
self.assertTrue(job.result_stdout in ('', 'stdout capture is missing'),
|
||||
u'expected no stdout, got:\n%s' %
|
||||
job.result_stdout)
|
||||
if expect_traceback:
|
||||
self.assertTrue(job.result_traceback)
|
||||
else:
|
||||
self.assertFalse(job.result_traceback,
|
||||
u'expected no traceback, got:\n%s' %
|
||||
job.result_traceback)
|
||||
|
||||
def check_job_events(self, job, runner_status='ok', plays=1, tasks=1,
|
||||
async=False, async_timeout=False, async_nowait=False,
|
||||
check_ignore_errors=False, async_tasks=0,
|
||||
|
||||
@ -5,29 +5,30 @@ from django.core.urlresolvers import reverse
|
||||
from awx.main.tests.base import BaseLiveServerTest, QueueStartStopTestMixin
|
||||
from awx.main.tests.base import URI
|
||||
|
||||
__all__ = ['UnifiedJobStdoutTests']
|
||||
__all__ = ['UnifiedJobStdoutRedactedTests']
|
||||
|
||||
|
||||
TEST_STDOUTS = []
|
||||
uri = URI(scheme="https", username="Dhh3U47nmC26xk9PKscV", password="PXPfWW8YzYrgS@E5NbQ2H@", host="github.ginger.com/theirrepo.git/info/refs")
|
||||
TEST_STDOUTS.append({
|
||||
'description': 'uri in a plain text document',
|
||||
'uri' : uri,
|
||||
'text' : 'hello world %s goodbye world' % uri,
|
||||
'host_occurrences' : 1
|
||||
'occurrences' : 1
|
||||
})
|
||||
|
||||
uri = URI(scheme="https", username="applepie@@@", password="thatyouknow@@@@", host="github.ginger.com/theirrepo.git/info/refs")
|
||||
TEST_STDOUTS.append({
|
||||
'description': 'uri appears twice in a multiline plain text document',
|
||||
'uri' : uri,
|
||||
'text' : 'hello world %s \n\nyoyo\n\nhello\n%s' % (uri, uri),
|
||||
'host_occurrences' : 2
|
||||
'occurrences' : 2
|
||||
})
|
||||
|
||||
|
||||
class UnifiedJobStdoutTests(BaseLiveServerTest, QueueStartStopTestMixin):
|
||||
class UnifiedJobStdoutRedactedTests(BaseLiveServerTest, QueueStartStopTestMixin):
|
||||
|
||||
def setUp(self):
|
||||
super(UnifiedJobStdoutTests, self).setUp()
|
||||
super(UnifiedJobStdoutRedactedTests, self).setUp()
|
||||
self.setup_instances()
|
||||
self.setup_users()
|
||||
self.test_cases = []
|
||||
@ -40,15 +41,38 @@ class UnifiedJobStdoutTests(BaseLiveServerTest, QueueStartStopTestMixin):
|
||||
|
||||
# This is more of a functional test than a unit test.
|
||||
# should filter out username and password
|
||||
def test_redaction_enabled(self):
|
||||
def check_sensitive_redacted(self, test_data, response):
|
||||
uri = test_data['uri']
|
||||
self.assertIsNotNone(response['content'])
|
||||
self.check_not_found(response['content'], uri.username, test_data['description'])
|
||||
self.check_not_found(response['content'], uri.password, test_data['description'])
|
||||
# Ensure the host didn't get redacted
|
||||
self.check_found(response['content'], uri.host, test_data['occurrences'], test_data['description'])
|
||||
|
||||
def _get_url_job_stdout(self, job, format='json'):
|
||||
formats = {
|
||||
'json': 'application/json',
|
||||
'ansi': 'text/plain',
|
||||
'txt': 'text/plain',
|
||||
'html': 'text/html',
|
||||
}
|
||||
content_type = formats[format]
|
||||
job_stdout_url = reverse('api:job_stdout', args=(job.pk,)) + "?format=" + format
|
||||
return self.get(job_stdout_url, expect=200, auth=self.get_super_credentials(), accept=content_type)
|
||||
|
||||
def _test_redaction_enabled(self, format):
|
||||
for test_data in self.test_cases:
|
||||
uri = test_data['uri']
|
||||
job_stdout_url = reverse('api:job_stdout', args=(test_data['job'].pk,))
|
||||
response = self._get_url_job_stdout(test_data['job'], format=format)
|
||||
self.check_sensitive_redacted(test_data, response)
|
||||
|
||||
response = self.get(job_stdout_url, expect=200, auth=self.get_super_credentials(), accept='application/json')
|
||||
def test_redaction_enabled_json(self):
|
||||
self._test_redaction_enabled('json')
|
||||
|
||||
self.assertIsNotNone(response['content'])
|
||||
self.check_not_found(response['content'], uri.username)
|
||||
self.check_not_found(response['content'], uri.password)
|
||||
# Ensure the host didn't get redacted
|
||||
self.check_found(response['content'], uri.host, count=test_data['host_occurrences'])
|
||||
def test_redaction_enabled_ansi(self):
|
||||
self._test_redaction_enabled('ansi')
|
||||
|
||||
def test_redaction_enabled_html(self):
|
||||
self._test_redaction_enabled('html')
|
||||
|
||||
def test_redaction_enabled_txt(self):
|
||||
self._test_redaction_enabled('txt')
|
||||
|
||||
@ -163,7 +163,7 @@ def decrypt_field(instance, field_name):
|
||||
|
||||
|
||||
def update_scm_url(scm_type, url, username=True, password=True,
|
||||
check_special_cases=True):
|
||||
check_special_cases=True, scp_format=False):
|
||||
'''
|
||||
Update the given SCM URL to add/replace/remove the username/password. When
|
||||
username/password is True, preserve existing username/password, when
|
||||
@ -183,20 +183,28 @@ def update_scm_url(scm_type, url, username=True, password=True,
|
||||
parts.port
|
||||
except ValueError:
|
||||
raise ValueError('Invalid %s URL' % scm_type)
|
||||
#print parts
|
||||
if parts.scheme == 'git+ssh' and not scp_format:
|
||||
raise ValueError('Unsupported %s URL' % scm_type)
|
||||
|
||||
if '://' not in url:
|
||||
# Handle SCP-style URLs for git (e.g. [user@]host.xz:path/to/repo.git/).
|
||||
if scm_type == 'git' and '@' in url:
|
||||
userpass, hostpath = url.split('@', 1)
|
||||
hostpath = '/'.join(hostpath.split(':', 1))
|
||||
modified_url = '@'.join([userpass, hostpath])
|
||||
parts = urlparse.urlsplit('ssh://%s' % modified_url)
|
||||
elif scm_type == 'git' and ':' in url:
|
||||
if url.count(':') > 1:
|
||||
if scm_type == 'git' and ':' in url:
|
||||
if '@' in url:
|
||||
userpass, hostpath = url.split('@', 1)
|
||||
else:
|
||||
userpass, hostpath = '', url
|
||||
if hostpath.count(':') > 1:
|
||||
raise ValueError('Invalid %s URL' % scm_type)
|
||||
|
||||
modified_url = '/'.join(url.split(':', 1))
|
||||
parts = urlparse.urlsplit('ssh://%s' % modified_url)
|
||||
host, path = hostpath.split(':', 1)
|
||||
#if not path.startswith('/') and not path.startswith('~/'):
|
||||
# path = '~/%s' % path
|
||||
#if path.startswith('/'):
|
||||
# path = path.lstrip('/')
|
||||
hostpath = '/'.join([host, path])
|
||||
modified_url = '@'.join(filter(None, [userpass, hostpath]))
|
||||
# git+ssh scheme identifies URLs that should be converted back to
|
||||
# SCP style before passed to git module.
|
||||
parts = urlparse.urlsplit('git+ssh://%s' % modified_url)
|
||||
# Handle local paths specified without file scheme (e.g. /path/to/foo).
|
||||
# Only supported by git and hg. (not currently allowed)
|
||||
elif scm_type in ('git', 'hg'):
|
||||
@ -206,10 +214,10 @@ def update_scm_url(scm_type, url, username=True, password=True,
|
||||
parts = urlparse.urlsplit('file://%s' % url)
|
||||
else:
|
||||
raise ValueError('Invalid %s URL' % scm_type)
|
||||
#print parts
|
||||
|
||||
# Validate that scheme is valid for given scm_type.
|
||||
scm_type_schemes = {
|
||||
'git': ('ssh', 'git', 'http', 'https', 'ftp', 'ftps'),
|
||||
'git': ('ssh', 'git', 'git+ssh', 'http', 'https', 'ftp', 'ftps'),
|
||||
'hg': ('http', 'https', 'ssh'),
|
||||
'svn': ('http', 'https', 'svn', 'svn+ssh'),
|
||||
}
|
||||
@ -235,9 +243,9 @@ def update_scm_url(scm_type, url, username=True, password=True,
|
||||
# Special handling for github/bitbucket SSH URLs.
|
||||
if check_special_cases:
|
||||
special_git_hosts = ('github.com', 'bitbucket.org', 'altssh.bitbucket.org')
|
||||
if scm_type == 'git' and parts.scheme == 'ssh' and parts.hostname in special_git_hosts and netloc_username != 'git':
|
||||
if scm_type == 'git' and parts.scheme.endswith('ssh') and parts.hostname in special_git_hosts and netloc_username != 'git':
|
||||
raise ValueError('Username must be "git" for SSH access to %s.' % parts.hostname)
|
||||
if scm_type == 'git' and parts.scheme == 'ssh' and parts.hostname in special_git_hosts and netloc_password:
|
||||
if scm_type == 'git' and parts.scheme.endswith('ssh') and parts.hostname in special_git_hosts and netloc_password:
|
||||
#raise ValueError('Password not allowed for SSH access to %s.' % parts.hostname)
|
||||
netloc_password = ''
|
||||
special_hg_hosts = ('bitbucket.org', 'altssh.bitbucket.org')
|
||||
@ -256,6 +264,8 @@ def update_scm_url(scm_type, url, username=True, password=True,
|
||||
netloc = u':'.join([netloc, unicode(parts.port)])
|
||||
new_url = urlparse.urlunsplit([parts.scheme, netloc, parts.path,
|
||||
parts.query, parts.fragment])
|
||||
if scp_format and parts.scheme == 'git+ssh':
|
||||
new_url = new_url.replace('git+ssh://', '', 1).replace('/', ':', 1)
|
||||
return new_url
|
||||
|
||||
|
||||
|
||||
4
awx/playbooks/scan_facts.yml
Normal file
@ -0,0 +1,4 @@
|
||||
- hosts: all
|
||||
tasks:
|
||||
- scan_packages:
|
||||
|
||||
80
awx/plugins/fact_caching/tower.py
Executable file
@ -0,0 +1,80 @@
|
||||
# Copyright (c) 2015 Ansible, Inc.
|
||||
# This file is a utility Ansible plugin that is not part of the AWX or Ansible
|
||||
# packages. It does not import any code from either package, nor does its
|
||||
# license apply to Ansible or AWX.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
#
|
||||
# Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# Neither the name of the <ORGANIZATION> nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import sys
|
||||
import time
|
||||
import datetime
|
||||
from ansible import constants as C
|
||||
from ansible.cache.base import BaseCacheModule
|
||||
|
||||
try:
|
||||
import zmq
|
||||
except ImportError:
|
||||
print("pyzmq is required")
|
||||
sys.exit(1)
|
||||
|
||||
class CacheModule(BaseCacheModule):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
# This is the local tower zmq connection
|
||||
self._tower_connection = C.CACHE_PLUGIN_CONNECTION
|
||||
self.date_key = time.mktime(datetime.datetime.utcnow().timetuple())
|
||||
try:
|
||||
self.context = zmq.Context()
|
||||
self.socket = self.context.socket(zmq.REQ)
|
||||
self.socket.connect(self._tower_connection)
|
||||
except Exception, e:
|
||||
print("Connection to zeromq failed at %s with error: %s" % (str(self._tower_connection),
|
||||
str(e)))
|
||||
sys.exit(1)
|
||||
|
||||
def get(self, key):
|
||||
return {} # Temporary until we have some tower retrieval endpoints
|
||||
|
||||
def set(self, key, value):
|
||||
self.socket.send_json(dict(host=key, facts=value, date_key=self.date_key))
|
||||
self.socket.recv()
|
||||
|
||||
def keys(self):
|
||||
return []
|
||||
|
||||
def contains(self, key):
|
||||
return False
|
||||
|
||||
def delete(self, key):
|
||||
pass
|
||||
|
||||
def flush(self):
|
||||
pass
|
||||
|
||||
def copy(self):
|
||||
return dict()
|
||||
49
awx/plugins/library/scan_packages.py
Executable file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
def rpm_package_list():
|
||||
import rpm
|
||||
trans_set = rpm.TransactionSet()
|
||||
installed_packages = {}
|
||||
for package in trans_set.dbMatch():
|
||||
package_details = dict(name=package[rpm.RPMTAG_NAME],
|
||||
version=package[rpm.RPMTAG_VERSION],
|
||||
release=package[rpm.RPMTAG_RELEASE],
|
||||
epoch=package[rpm.RPMTAG_EPOCH],
|
||||
arch=package[rpm.RPMTAG_ARCH],
|
||||
source='rpm')
|
||||
if package['name'] not in installed_packages:
|
||||
installed_packages[package['name']] = [package_details]
|
||||
else:
|
||||
installed_packages[package['name']].append(package_details)
|
||||
return installed_packages
|
||||
|
||||
def deb_package_list():
|
||||
import apt
|
||||
apt_cache = apt.Cache()
|
||||
installed_packages = {}
|
||||
apt_installed_packages = [pk for pk in apt_cache.keys() if apt_cache[pk].is_installed]
|
||||
for package in apt_installed_packages:
|
||||
ac_pkg = apt_cache[package].installed
|
||||
package_details = dict(name=package,
|
||||
version=ac_pkg.version,
|
||||
architecture=ac_pkg.architecture,
|
||||
source='apt')
|
||||
installed_packages[package] = [package_details]
|
||||
return installed_packages
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec = dict())
|
||||
|
||||
packages = []
|
||||
if os.path.exists("/etc/redhat-release"):
|
||||
packages = rpm_package_list()
|
||||
elif os.path.exists("/etc/os-release"):
|
||||
packages = deb_package_list()
|
||||
results = dict(ansible_facts=dict(packages=packages))
|
||||
module.exit_json(**results)
|
||||
|
||||
main()
|
||||
@ -64,6 +64,7 @@ USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, 'ui', 'dist'),
|
||||
os.path.join(BASE_DIR, 'static'),
|
||||
)
|
||||
|
||||
@ -510,6 +511,8 @@ TASK_COMMAND_PORT = 6559
|
||||
SOCKETIO_NOTIFICATION_PORT = 6557
|
||||
SOCKETIO_LISTEN_PORT = 8080
|
||||
|
||||
FACT_CACHE_PORT = 6564
|
||||
|
||||
ORG_ADMINS_CAN_SEE_ALL_USERS = True
|
||||
|
||||
# Logging configuration.
|
||||
|
||||
@ -37,9 +37,10 @@ except ImportError:
|
||||
if 'django_jenkins' in INSTALLED_APPS:
|
||||
JENKINS_TASKS = (
|
||||
'django_jenkins.tasks.run_pylint',
|
||||
'django_jenkins.tasks.run_pep8',
|
||||
'django_jenkins.tasks.run_pyflakes',
|
||||
'django_jenkins.tasks.run_flake8',
|
||||
# The following are not needed when including run_flake8
|
||||
# 'django_jenkins.tasks.run_pep8',
|
||||
# 'django_jenkins.tasks.run_pyflakes',
|
||||
'django_jenkins.tasks.run_jshint',
|
||||
'django_jenkins.tasks.run_csslint',
|
||||
)
|
||||
|
||||
@ -45,6 +45,17 @@ import {UsersList, UsersAdd, UsersEdit} from 'tower/controllers/Users';
|
||||
import {TeamsList, TeamsAdd, TeamsEdit} from 'tower/controllers/Teams';
|
||||
import {PermissionsAdd, PermissionsList, PermissionsEdit} from 'tower/controllers/Permissions';
|
||||
|
||||
import 'tower/shared/RestServices';
|
||||
import 'tower/shared/api-loader';
|
||||
import 'tower/shared/form-generator';
|
||||
import 'tower/shared/Modal';
|
||||
import 'tower/shared/prompt-dialog';
|
||||
import 'tower/shared/directives';
|
||||
import 'tower/shared/filters';
|
||||
import 'tower/shared/InventoryTree';
|
||||
import 'tower/shared/Timer';
|
||||
import 'tower/shared/Socket';
|
||||
|
||||
|
||||
|
||||
var tower = angular.module('Tower', [
|
||||
|
||||
@ -200,7 +200,7 @@ export function JobDetailController ($location, $rootScope, $scope, $compile, $r
|
||||
scope.removeLoadHostSummaries();
|
||||
}
|
||||
scope.removeHostSummaries = scope.$on('LoadHostSummaries', function() {
|
||||
if(scope.job.related){
|
||||
if(scope.job){
|
||||
var url = scope.job.related.job_host_summaries + '?';
|
||||
url += '&page_size=' + scope.hostSummariesMaxRows + '&order=host_name';
|
||||
|
||||
@ -247,9 +247,11 @@ export function JobDetailController ($location, $rootScope, $scope, $compile, $r
|
||||
if (scope.activeTask) {
|
||||
|
||||
var play = scope.jobData.plays[scope.activePlay],
|
||||
task = play.tasks[scope.activeTask],
|
||||
task, // = play.tasks[scope.activeTask],
|
||||
url;
|
||||
|
||||
if(play){
|
||||
task = play.tasks[scope.activeTask];
|
||||
}
|
||||
if (play && task) {
|
||||
url = scope.job.related.job_events + '?parent=' + task.id + '&';
|
||||
url += 'event__startswith=runner&page_size=' + scope.hostResultsMaxRows + '&order=host_name,counter';
|
||||
|
||||
@ -422,6 +422,7 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $
|
||||
$scope.removeSurveySaved = $scope.$on('SurveySaved', function() {
|
||||
Wait('stop');
|
||||
$scope.survey_exists = true;
|
||||
$scope.invalid_survey = false;
|
||||
$('#job_templates_survey_enabled_chbox').attr('checked', true);
|
||||
$('#job_templates_delete_survey_btn').show();
|
||||
$('#job_templates_edit_survey_btn').show();
|
||||
@ -451,6 +452,7 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $
|
||||
|
||||
// Save
|
||||
$scope.formSave = function () {
|
||||
$scope.invalid_survey = false;
|
||||
if ($scope.removeGatherFormFields) {
|
||||
$scope.removeGatherFormFields();
|
||||
}
|
||||
@ -525,7 +527,14 @@ export function JobTemplatesAdd($scope, $rootScope, $compile, $location, $log, $
|
||||
});
|
||||
|
||||
if($scope.survey_enabled === true && $scope.survey_exists!==true){
|
||||
$scope.$emit("PromptForSurvey");
|
||||
// $scope.$emit("PromptForSurvey");
|
||||
|
||||
// The original design for this was a pop up that would prompt the user if they wanted to create a
|
||||
// survey, because they had enabled one but not created it yet. We switched this for now so that
|
||||
// an error message would be displayed by the survey buttons that tells the user to add a survey or disabled
|
||||
// surveys.
|
||||
$scope.invalid_survey = true;
|
||||
return;
|
||||
} else {
|
||||
$scope.$emit("GatherFormFields");
|
||||
}
|
||||
@ -837,6 +846,7 @@ export function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log,
|
||||
$scope.removeSurveySaved = $scope.$on('SurveySaved', function() {
|
||||
Wait('stop');
|
||||
$scope.survey_exists = true;
|
||||
$scope.invalid_survey = false;
|
||||
$('#job_templates_survey_enabled_chbox').attr('checked', true);
|
||||
$('#job_templates_delete_survey_btn').show();
|
||||
$('#job_templates_edit_survey_btn').show();
|
||||
@ -905,7 +915,7 @@ export function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log,
|
||||
|
||||
// Save changes to the parent
|
||||
$scope.formSave = function () {
|
||||
|
||||
$scope.invalid_survey = false;
|
||||
if ($scope.removeGatherFormFields) {
|
||||
$scope.removeGatherFormFields();
|
||||
}
|
||||
@ -965,7 +975,14 @@ export function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log,
|
||||
});
|
||||
|
||||
if($scope.survey_enabled === true && $scope.survey_exists!==true){
|
||||
$scope.$emit("PromptForSurvey");
|
||||
// $scope.$emit("PromptForSurvey");
|
||||
|
||||
// The original design for this was a pop up that would prompt the user if they wanted to create a
|
||||
// survey, because they had enabled one but not created it yet. We switched this for now so that
|
||||
// an error message would be displayed by the survey buttons that tells the user to add a survey or disabled
|
||||
// surveys.
|
||||
$scope.invalid_survey = true;
|
||||
return;
|
||||
} else {
|
||||
$scope.$emit("GatherFormFields");
|
||||
}
|
||||
|
||||
@ -202,19 +202,25 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
|
||||
}
|
||||
$scope.teamLoadedRemove = $scope.$on('teamLoaded', function () {
|
||||
CheckAccess({ scope: $scope });
|
||||
Rest.setUrl($scope.organization_url);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
$scope.organization_name = data.name;
|
||||
master.organization_name = data.name;
|
||||
Wait('stop');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve organization: ' +
|
||||
$scope.orgnization_url + '. GET status: ' + status });
|
||||
});
|
||||
for (var set in relatedSets) {
|
||||
$scope.search(relatedSets[set].iterator);
|
||||
if ($scope.organization_url) {
|
||||
Rest.setUrl($scope.organization_url);
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
$scope.organization_name = data.name;
|
||||
master.organization_name = data.name;
|
||||
Wait('stop');
|
||||
})
|
||||
.error(function (data, status) {
|
||||
ProcessErrors($scope, data, status, null, { hdr: 'Error!', msg: 'Failed to retrieve organization: ' +
|
||||
$scope.orgnization_url + '. GET status: ' + status });
|
||||
});
|
||||
for (var set in relatedSets) {
|
||||
$scope.search(relatedSets[set].iterator);
|
||||
}
|
||||
} else {
|
||||
$scope.organization_name = "";
|
||||
master.organization_name = "";
|
||||
Wait('stop');
|
||||
}
|
||||
});
|
||||
|
||||
@ -387,4 +393,4 @@ export function TeamsEdit($scope, $rootScope, $compile, $location, $log, $routeP
|
||||
TeamsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'TeamForm',
|
||||
'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'RelatedSearchInit', 'RelatedPaginateInit',
|
||||
'ReturnToCaller', 'ClearScope', 'LookUpInit', 'Prompt', 'GetBasePath', 'CheckAccess', 'OrganizationList', 'Wait', 'Stream'
|
||||
];
|
||||
];
|
||||
|
||||
@ -273,19 +273,9 @@ export default
|
||||
column: 2,
|
||||
control: '<button type="button" class="btn btn-sm btn-primary" id="job_templates_create_survey_btn" ng-show="survey_enabled" ng-click="addSurvey()"><i class="fa fa-pencil"></i> Create Survey</button>'+
|
||||
'<button style="display:none;" type="button" class="btn btn-sm btn-primary" id="job_templates_edit_survey_btn" ng-show="survey_enabled" ng-click="editSurvey()"><i class="fa fa-pencil"></i> Edit Survey</button>'+
|
||||
'<button style="display:none;margin-left:5px" type="button" class="btn btn-sm btn-primary" id="job_templates_delete_survey_btn" ng-show="survey_enabled" ng-click="deleteSurvey()"><i class="fa fa-trash-o"></i> Delete Survey</button>'
|
||||
// label: 'Create Survey',
|
||||
// type: 'text',
|
||||
// addRequired: false,
|
||||
// editRequired: false,
|
||||
// // readonly: true,
|
||||
// // ngShow: "survey_enabled",
|
||||
// column: 2,
|
||||
// awPopOver: "survey_help",
|
||||
// awPopOverWatch: "survey_help",
|
||||
// dataPlacement: 'right',
|
||||
// dataTitle: 'Provisioning Callback URL',
|
||||
// dataContainer: "body"
|
||||
'<button style="display:none;margin-left:5px" type="button" class="btn btn-sm btn-primary" id="job_templates_delete_survey_btn" ng-show="survey_enabled" ng-click="deleteSurvey()"><i class="fa fa-trash-o"></i> Delete Survey</button>'+
|
||||
// '<div class="error ng-hide" id="job-template-survey-error" ng-show="survey_enabled === true && survey_exists!==true">A survey is enabled but it does not exist. Create a survey or disable the survey. </div>'
|
||||
'<div class="error ng-hide" id="job-template-survey-error" ng-show="invalid_survey">A survey is enabled but it does not exist. Create a survey or uncheck the Enable Survey box to disable the survey. </div>'
|
||||
},
|
||||
allow_callbacks: {
|
||||
label: 'Allow Provisioning Callbacks',
|
||||
|
||||
@ -63,8 +63,8 @@ export default
|
||||
'user_id<br>host_name<br><div class="popover-footer"><span class="key">esc</span> or click to close</div>" '+
|
||||
'data-placement="right" data-container="body" data-title="Answer Variable Name" class="help-link" data-original-title="" title="" tabindex="-1"><i class="fa fa-question-circle"></i></a> </label>'+
|
||||
'<div><input type="text" ng-model="variable" name="variable" id="survey_question_variable" class="form-control ng-pristine ng-invalid ng-invalid-required" required="" aw-survey-variable-name>'+
|
||||
'<div class="error ng-hide" id="survey_question-variable-required-error" ng-show="survey_question_form.variable.$dirty && survey_question_form.variable.$error.required">Please enter an answer variable name.</div>'+
|
||||
'<div class="error ng-hide" id="survey_question-variable-variable-error" ng-show="survey_question_form.variable.$dirty && survey_question_form.variable.$error.variable">Please remove the illegal character from the survey question variable name.</div>'+
|
||||
'<div class="error ng-hide" id="survey_question-variable-required-error" ng-show="survey_question_form.variable.$dirty && survey_question_form.variable.$error.required">Please enter an answer variable name.</div>'+
|
||||
'<div class="error ng-hide" id="survey_question-variable-variable-error" ng-show="survey_question_form.variable.$dirty && survey_question_form.variable.$error.variable">Please remove the illegal character from the survey question variable name.</div>'+
|
||||
'<div class="error ng-hide" id=survey_question-variable-duplicate-error" ng-show="duplicate">This question variable is already in use. Please enter a different variable name.</div>' +
|
||||
'<div class="error api-error ng-binding" id="survey_question-variable-api-error" ng-bind="variable_api_error"></div>'+
|
||||
'</div>',
|
||||
@ -106,13 +106,13 @@ export default
|
||||
control:'<div class="row">'+
|
||||
'<div class="col-xs-6">'+
|
||||
'<label for="text_min"><span class="label-text">Minimum Length</span></label><input id="text_min" type="number" name="text_min" ng-model="text_min" min=0 aw-min="0" aw-max="text_max" class="form-control" integer />'+
|
||||
'<div class="error" ng-show="survey_question_form.text_min.$error.number">The minimum length you entered is not a number. Please enter a number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.text_min.$error.integer || survey_question_form.text_min.$error.number">The minimum length you entered is not a valid number. Please enter a whole number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.text_min.$error.awMax">The minimium length is too high. Please enter a lower number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.text_min.$error.awMin">The minimum length is too low. Please enter a positive number.</div>'+
|
||||
'</div>'+
|
||||
'<div class="col-xs-6">'+
|
||||
'<label for="text_max"><span class="label-text">Maximum Length</span></label><input id="text_max" type="number" name="text_max" ng-model="text_max" aw-min="text_min || 0" min=0 class="form-control" integer >'+
|
||||
'<div class="error" ng-show="survey_question_form.text_max.$error.number">The maximum length you entered is not a number. Please enter a number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.text_max.$error.integer || survey_question_form.text_max.$error.number">The maximum length you entered is not a valid number. Please enter a whole nnumber.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.text_max.$error.awMin">The maximum length is too low. Please enter a number larger than the minimum length you set.</div>'+
|
||||
'</div>'+
|
||||
'</div>',
|
||||
@ -127,13 +127,13 @@ export default
|
||||
control:'<div class="row">'+
|
||||
'<div class="col-xs-6">'+
|
||||
'<label for="textarea_min"><span class="label-text">Minimum Length</span></label><input id="textarea_min" type="number" name="textarea_min" ng-model="textarea_min" min=0 aw-min="0" aw-max="textarea_max" class="form-control" integer />'+
|
||||
'<div class="error" ng-show="survey_question_form.textarea_min.$error.number">The minimum length you entered is not a number. Please enter a number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.textarea_min.$error.integer || survey_question_form.textarea_min.$error.number">The minimum length you entered is not a valid number. Please enter a whole number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.textarea_min.$error.awMax">The minimium length is too high. Please enter a lower number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.textarea_min.$error.awMin">The minimum length is too low. Please enter a positive number.</div>'+
|
||||
'</div>'+
|
||||
'<div class="col-xs-6">'+
|
||||
'<label for="textarea_max"><span class="label-text">Maximum Length</span></label><input id="textarea_max" type="number" name="textarea_max" ng-model="textarea_max" aw-min="textarea_min || 0" min=0 class="form-control" integer >'+
|
||||
'<div class="error" ng-show="survey_question_form.textarea_max.$error.number">The maximum length you entered is not a number. Please enter a number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.textarea_max.$error.integer || survey_question_form.textarea_max.$error.number">The maximum length you entered is not a valid number. Please enter a whole number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.textarea_max.$error.awMin">The maximum length is too low. Please enter a number larger than the minimum length you set.</div>'+
|
||||
'</div>'+
|
||||
'</div>',
|
||||
@ -148,13 +148,13 @@ export default
|
||||
control:'<div class="row">'+
|
||||
'<div class="col-xs-6">'+
|
||||
'<label for="password_min"><span class="label-text">Minimum Length</span></label><input id="password_min" type="number" name="password_min" ng-model="password_min" min=0 aw-min="0" aw-max="password_max" class="form-control" integer />'+
|
||||
'<div class="error" ng-show="survey_question_form.password_min.$error.number">The minimum length you entered is not a number. Please enter a number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.password_min.$error.integer || survey_question_form.password_min.$error.number">The minimum length you entered is not a valid number. Please enter a whole number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.password_min.$error.awMax">The minimium length is too high. Please enter a lower number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.password_min.$error.awMin">The minimum length is too low. Please enter a positive number.</div>'+
|
||||
'</div>'+
|
||||
'<div class="col-xs-6">'+
|
||||
'<label for="password_max"><span class="label-text">Maximum Length</span></label><input id="password_max" type="number" name="password_max" ng-model="password_max" aw-min="password_min || 0" min=0 class="form-control" integer >'+
|
||||
'<div class="error" ng-show="survey_question_form.password_max.$error.number">The maximum length you entered is not a number. Please enter a number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.password_max.$error.integer || survey_question_form.password_max.$error.number">The maximum length you entered is not a valid number. Please enter a whole number.</div>'+
|
||||
'<div class="error" ng-show="survey_question_form.password_max.$error.awMin">The maximum length is too low. Please enter a number larger than the minimum length you set.</div>'+
|
||||
'</div>'+
|
||||
'</div>',
|
||||
|
||||
@ -1,11 +1,4 @@
|
||||
import ChromeSocketHelp from "tower/help/ChromeSocketHelp";
|
||||
import FirefoxSocketHelp from "tower/help/FirefoxSocketHelp";
|
||||
import InventoryGroups from "tower/help/InventoryGroups";
|
||||
import SafariSocketHelp from "tower/help/SafariSocketHelp";
|
||||
|
||||
export
|
||||
{ ChromeSocketHelp,
|
||||
FirefoxSocketHelp,
|
||||
InventoryGroups,
|
||||
SafariSocketHelp
|
||||
};
|
||||
import "tower/help/ChromeSocketHelp";
|
||||
import "tower/help/FirefoxSocketHelp";
|
||||
import "tower/help/InventoryGroups";
|
||||
import "tower/help/SafariSocketHelp";
|
||||
|
||||
@ -151,9 +151,22 @@ export default
|
||||
e = angular.element(document.getElementById('prompt_for_days_form'));
|
||||
scope.prompt_for_days_form.days_to_keep.$setViewValue(30);
|
||||
$compile(e)(scope);
|
||||
$('#prompt-for-days-launch').attr("ng-disabled", 'prompt_for_days_form.$invalid');
|
||||
e = angular.element(document.getElementById('prompt-for-days-launch'));
|
||||
$compile(e)(scope);
|
||||
|
||||
// this is a work-around for getting awMax to work (without
|
||||
// clearing out the form)
|
||||
scope.$watch('days_to_keep', function(newVal) { // oldVal, scope) { // unused params get caught by jshint
|
||||
if (!newVal) {
|
||||
$('#prompt-for-days-launch').prop("disabled", true);
|
||||
} else if (isNaN(newVal)) {
|
||||
$('#prompt-for-days-launch').prop("disabled", true);
|
||||
} else if (newVal <= 0) {
|
||||
$('#prompt-for-days-launch').prop("disabled", true);
|
||||
} else if (newVal > 9999) {
|
||||
$('#prompt-for-days-launch').prop("disabled", true);
|
||||
} else {
|
||||
$('#prompt-for-days-launch').prop("disabled", false);
|
||||
}
|
||||
});
|
||||
},
|
||||
buttons: [{
|
||||
"label": "Cancel",
|
||||
|
||||
@ -39,7 +39,7 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential
|
||||
Rest.get()
|
||||
.success(function (data) {
|
||||
if(!Empty(data.extra_vars)){
|
||||
data.extra_vars = ToJSON('json', data.extra_vars, false);
|
||||
data.extra_vars = ToJSON('yaml', data.extra_vars, false);
|
||||
$.each(data.extra_vars, function(key,value){
|
||||
job_launch_data.extra_vars[key] = value;
|
||||
});
|
||||
@ -77,11 +77,11 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential
|
||||
for (var i=0; i < scope.survey_questions.length; i++){
|
||||
var fld = scope.survey_questions[i].variable;
|
||||
// grab all survey questions that have answers
|
||||
if(scope[fld]) {
|
||||
if(scope.survey_questions[i].required || (scope.survey_questions[i].required === false && scope[fld].toString()!=="")) {
|
||||
job_launch_data.extra_vars[fld] = scope[fld];
|
||||
}
|
||||
// for optional text and text-areas, submit a blank string if min length is 0
|
||||
if(scope.survey_questions[i].required === false && (scope.survey_questions[i].type === "text" || scope.survey_questions[i].type === "textarea") && scope.survey_questions[i].min === 0 && scope[fld] ===""){
|
||||
if(scope.survey_questions[i].required === false && (scope.survey_questions[i].type === "text" || scope.survey_questions[i].type === "textarea") && scope.survey_questions[i].min === 0 && (scope[fld] === "" || scope[fld] === undefined)){
|
||||
job_launch_data.extra_vars[fld] = "";
|
||||
}
|
||||
}
|
||||
@ -544,7 +544,7 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential
|
||||
}
|
||||
|
||||
if(question.type === "textarea"){
|
||||
scope[question.variable] = question.default || question.default_textarea;
|
||||
scope[question.variable] = (question.default_textarea) ? question.default_textarea : (question.default) ? question.default : "";
|
||||
minlength = (!Empty(question.min)) ? Number(question.min) : "";
|
||||
maxlength =(!Empty(question.max)) ? Number(question.max) : "" ;
|
||||
html+='<textarea id="'+question.variable+'" name="'+question.variable+'" ng-model="'+question.variable+'" '+
|
||||
|
||||
@ -577,11 +577,14 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper',
|
||||
scope.int_max = null;
|
||||
scope.float_min = null;
|
||||
scope.float_max = null;
|
||||
scope.duplicate = false;
|
||||
scope.invalidChoice = false;
|
||||
scope.minTextError = false;
|
||||
scope.maxTextError = false;
|
||||
};
|
||||
|
||||
scope.addNewQuestion = function(){
|
||||
// $('#add_question_btn').on("click" , function(){
|
||||
scope.duplicate = false;
|
||||
scope.addQuestion();
|
||||
$('#survey_question_question_name').focus();
|
||||
$('#add_question_btn').attr('disabled', 'disabled');
|
||||
@ -753,6 +756,7 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper',
|
||||
scope.default_float = "";
|
||||
scope.default_int = "";
|
||||
scope.default_textarea = "";
|
||||
scope.default_password = "" ;
|
||||
scope.choices = "";
|
||||
scope.text_min = "";
|
||||
scope.text_max = "" ;
|
||||
|
||||
@ -10,9 +10,10 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import Utilities from './Utilities';
|
||||
|
||||
|
||||
angular.module('AuthService', ['ngCookies', 'Utilities'])
|
||||
export default
|
||||
angular.module('AuthService', ['ngCookies', Utilities.name])
|
||||
|
||||
.factory('Authorization', ['$http', '$rootScope', '$location', '$cookieStore', 'GetBasePath', 'Store',
|
||||
function ($http, $rootScope, $location, $cookieStore, GetBasePath, Store) {
|
||||
@ -155,4 +156,4 @@ angular.module('AuthService', ['ngCookies', 'Utilities'])
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
]);
|
||||
@ -13,7 +13,7 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export default
|
||||
angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'PromptDialog'])
|
||||
|
||||
.factory('SortNodes', [
|
||||
@ -603,4 +603,4 @@ angular.module('InventoryTree', ['Utilities', 'RestServices', 'GroupsHelper', 'P
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
]);
|
||||
@ -14,7 +14,7 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export default
|
||||
angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
|
||||
|
||||
/**
|
||||
@ -51,9 +51,10 @@
|
||||
*
|
||||
*/
|
||||
|
||||
import AuthService from './AuthService';
|
||||
|
||||
|
||||
angular.module('RestServices', ['ngCookies', 'AuthService'])
|
||||
export default
|
||||
angular.module('RestServices', ['ngCookies', AuthService.name])
|
||||
.factory('Rest', ['$http', '$rootScope', '$cookieStore', '$q', 'Authorization',
|
||||
function ($http, $rootScope, $cookieStore, $q, Authorization) {
|
||||
return {
|
||||
@ -267,4 +268,4 @@ angular.module('RestServices', ['ngCookies', 'AuthService'])
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
]);
|
||||
@ -19,6 +19,7 @@
|
||||
* @methodOf lib.ansible.function:Socket
|
||||
* @description
|
||||
*/
|
||||
export default
|
||||
angular.module('SocketIO', ['AuthService', 'Utilities'])
|
||||
|
||||
.factory('Socket', ['$rootScope', '$location', '$log', 'Authorization', 'Store', function ($rootScope, $location, $log, Authorization, Store) {
|
||||
@ -71,6 +72,7 @@ angular.module('SocketIO', ['AuthService', 'Utilities'])
|
||||
$log.debug('Socket connecting to: ' + url);
|
||||
self.scope.socket_url = url;
|
||||
self.socket = io.connect(url, {
|
||||
query: "Token="+token,
|
||||
headers:
|
||||
{
|
||||
'Authorization': 'Token ' + token, // i don't think these are actually inserted into the header--jt
|
||||
@ -78,7 +80,7 @@ angular.module('SocketIO', ['AuthService', 'Utilities'])
|
||||
},
|
||||
'connect timeout': 3000,
|
||||
'try multiple transports': false,
|
||||
'max reconneciton attemps': 3,
|
||||
'max reconnection attempts': 3,
|
||||
'reconnection limit': 3000
|
||||
});
|
||||
|
||||
@ -114,6 +116,7 @@ angular.module('SocketIO', ['AuthService', 'Utilities'])
|
||||
});
|
||||
self.socket.on('error', function(reason) {
|
||||
var r = reason || 'connection refused by host';
|
||||
|
||||
$log.debug('Socket error: ' + r);
|
||||
$log.error('Socket error: ' + r);
|
||||
self.scope.$apply(function() {
|
||||
@ -155,7 +158,7 @@ angular.module('SocketIO', ['AuthService', 'Utilities'])
|
||||
if (self.socket.socket.connected) {
|
||||
self.scope.socketStatus = 'ok';
|
||||
}
|
||||
else if (self.socket.socket.connecting || self.socket.socket.reconnecting) {
|
||||
else if (self.socket.socket.connecting || self.socket.reconnecting) {
|
||||
self.scope.socketStatus = 'connecting';
|
||||
}
|
||||
else {
|
||||
@ -18,6 +18,7 @@
|
||||
* @methodOf lib.ansible.function:Timer
|
||||
* @description
|
||||
*/
|
||||
export default
|
||||
angular.module('TimerService', ['ngCookies', 'Utilities'])
|
||||
.factory('Timer', ['$rootScope', '$cookieStore', '$location', 'GetBasePath', 'Empty',
|
||||
function ($rootScope, $cookieStore) {
|
||||
@ -65,4 +66,4 @@ angular.module('TimerService', ['ngCookies', 'Utilities'])
|
||||
}
|
||||
};
|
||||
}
|
||||
]);
|
||||
]);
|
||||
@ -14,6 +14,7 @@
|
||||
|
||||
|
||||
|
||||
export default
|
||||
angular.module('Utilities', ['RestServices', 'Utilities'])
|
||||
|
||||
/**
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
|
||||
|
||||
export default
|
||||
angular.module('ApiLoader', ['Utilities'])
|
||||
|
||||
.factory('LoadBasePaths', ['$http', '$rootScope', 'Store', 'ProcessErrors',
|
||||
@ -72,4 +73,4 @@ angular.module('ApiLoader', ['Utilities'])
|
||||
return $rootScope.defaultUrls[set];
|
||||
};
|
||||
}
|
||||
]);
|
||||
]);
|
||||
@ -13,6 +13,9 @@
|
||||
|
||||
/* global chkPass:false */
|
||||
|
||||
import {chkPass} from './pwdmeter';
|
||||
|
||||
export default
|
||||
angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'JobsHelper'])
|
||||
|
||||
// awpassmatch: Add to password_confirm field. Will test if value
|
||||
@ -70,7 +73,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
|
||||
require: 'ngModel',
|
||||
scope: { ngModel: '=ngModel' },
|
||||
template: '<div class="survey_taker_input" ng-repeat="option in ngModel.options">' +
|
||||
'<label><input type="checkbox" ng-model="cbModel[option.value]" ' +
|
||||
'<label style="font-weight:normal"><input type="checkbox" ng-model="cbModel[option.value]" ' +
|
||||
'value="{{option.value}}" class="mc" ng-change="update(this.value)" />' +
|
||||
'<span>'+
|
||||
'{{option.value}}'+
|
||||
@ -150,7 +153,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
|
||||
var min = (attr.awMin) ? scope.$eval(attr.awMin) : -Infinity;
|
||||
if (!Empty(min) && !Empty(viewValue) && Number(viewValue) < min) {
|
||||
ctrl.$setValidity('awMin', false);
|
||||
return undefined;
|
||||
return viewValue;
|
||||
} else {
|
||||
ctrl.$setValidity('awMin', true);
|
||||
return viewValue;
|
||||
@ -217,17 +220,17 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
|
||||
if ( elm.attr('min') &&
|
||||
( viewValue === '' || viewValue === null || parseInt(viewValue,10) < parseInt(elm.attr('min'),10) ) ) {
|
||||
ctrl.$setValidity('min', false);
|
||||
return undefined;
|
||||
return viewValue;
|
||||
}
|
||||
if ( elm.attr('max') && ( parseInt(viewValue,10) > parseInt(elm.attr('max'),10) ) ) {
|
||||
ctrl.$setValidity('max', false);
|
||||
return undefined;
|
||||
return viewValue;
|
||||
}
|
||||
return viewValue;
|
||||
}
|
||||
// Invalid, return undefined (no model update)
|
||||
ctrl.$setValidity('integer', false);
|
||||
return undefined;
|
||||
return viewValue;
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -238,16 +241,25 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
|
||||
.directive('awSurveyVariableName', function() {
|
||||
var FLOAT_REGEXP = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function(scope, elm, attrs, ctrl) {
|
||||
ctrl.$setValidity('required', true); // we only want the error message for incorrect characters to be displayed
|
||||
ctrl.$parsers.unshift(function(viewValue) {
|
||||
if (FLOAT_REGEXP.test(viewValue) && viewValue.indexOf(' ') === -1) { //check for a spaces
|
||||
ctrl.$setValidity('variable', true);
|
||||
if(viewValue.length !== 0){
|
||||
if (FLOAT_REGEXP.test(viewValue) && viewValue.indexOf(' ') === -1) { //check for a spaces
|
||||
ctrl.$setValidity('variable', true);
|
||||
return viewValue;
|
||||
}
|
||||
else{
|
||||
ctrl.$setValidity('variable', false); // spaces found, therefore throw error.
|
||||
return viewValue;
|
||||
}
|
||||
}
|
||||
else{
|
||||
ctrl.$setValidity('variable', true); // spaces found, therefore throw error.
|
||||
return viewValue;
|
||||
} else {
|
||||
ctrl.$setValidity('variable', false); // spaces found, therefore throw error
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -525,15 +537,23 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
|
||||
|
||||
$(element).one('click', showPopover);
|
||||
|
||||
$(element).on('shown.bs.popover', function() {
|
||||
function bindPopoverDismiss() {
|
||||
$('body').one('click.popover' + id_to_close, function(e) {
|
||||
if ($(e.target).parents(id_to_close).length === 0) {
|
||||
// case: you clicked to open the popover and then you
|
||||
// clicked outside of it...hide it.
|
||||
$(element).popover('hide');
|
||||
} else {
|
||||
// case: you clicked to open the popover and then you
|
||||
// clicked inside the popover
|
||||
bindPopoverDismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(element).on('shown.bs.popover', function() {
|
||||
bindPopoverDismiss();
|
||||
$(document).on('keydown.popover', dismissOnEsc);
|
||||
|
||||
});
|
||||
|
||||
$(element).on('hidden.bs.popover', function() {
|
||||
@ -11,6 +11,7 @@
|
||||
|
||||
|
||||
|
||||
export default
|
||||
angular.module('AWFilters', [])
|
||||
|
||||
//
|
||||
@ -91,4 +92,4 @@ angular.module('AWFilters', [])
|
||||
}
|
||||
return input;
|
||||
};
|
||||
}]);
|
||||
}]);
|
||||
@ -132,9 +132,11 @@
|
||||
* Applying CodeMirror to the text area is handled by ParseTypeChange() found in helpers/Parse.js. Within the controller will be a call to ParseTypeChange that creates the CodeMirror object and sets up the required $scope methods for handles getting, settting and type conversion.
|
||||
*/
|
||||
|
||||
import GeneratorHelpers from './generator-helpers';
|
||||
import ListGenerator from './list-generator';
|
||||
|
||||
|
||||
angular.module('FormGenerator', ['GeneratorHelpers', 'Utilities', 'ListGenerator'])
|
||||
export default
|
||||
angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', ListGenerator.name])
|
||||
|
||||
.factory('GenerateForm', ['$rootScope', '$location', '$compile', 'GenerateList', 'SearchWidget', 'PaginateWidget', 'Attr',
|
||||
'Icon', 'Column', 'NavigationLink', 'HelpCollapse', 'Button', 'DropDown', 'Empty', 'SelectIcon', 'Store',
|
||||
@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export default
|
||||
angular.module('GeneratorHelpers', [])
|
||||
|
||||
.factory('Attr', function () {
|
||||
@ -97,7 +97,7 @@
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export default
|
||||
angular.module('ListGenerator', ['GeneratorHelpers'])
|
||||
.factory('GenerateList', ['$location', '$compile', '$rootScope', 'SearchWidget', 'PaginateWidget', 'Attr', 'Icon',
|
||||
'Column', 'DropDown', 'NavigationLink', 'Button', 'SelectIcon', 'Breadcrumbs',
|
||||
@ -25,6 +25,8 @@
|
||||
* @methodOf lib.ansible.function:prompt-dialog
|
||||
* @description discuss difference b/t this and other modals
|
||||
*/
|
||||
|
||||
export default
|
||||
angular.module('PromptDialog', ['Utilities'])
|
||||
.factory('Prompt', ['$sce',
|
||||
function ($sce) {
|
||||
@ -42,7 +42,7 @@ String.prototype.strReverse = function () {
|
||||
|
||||
var nScore = 0;
|
||||
|
||||
function chkPass(pwd) {
|
||||
export function chkPass(pwd) {
|
||||
// Simultaneous variable declaration and value assignment aren't supported in IE apparently
|
||||
// so I'm forced to assign the same value individually per var to support a crappy browser *sigh*
|
||||
var nLength = 0,
|
||||
@ -324,4 +324,4 @@ function chkPass(pwd) {
|
||||
}
|
||||
|
||||
return nScore;
|
||||
}
|
||||
}
|
||||
@ -1,25 +1,12 @@
|
||||
import DashboardCounts from "tower/widgets/DashboardCounts";
|
||||
import DashboardJobs from "tower/widgets/DashboardJobs";
|
||||
import HostGraph from "tower/widgets/HostGraph";
|
||||
import HostPieChart from "tower/widgets/HostPieChart";
|
||||
import InventorySyncStatus from "tower/widgets/InventorySyncStatus";
|
||||
import JobStatus from "tower/widgets/JobStatus";
|
||||
import JobStatusGraph from "tower/widgets/JobStatusGraph";
|
||||
import ObjectCount from "tower/widgets/ObjectCount";
|
||||
import PortalJobs from "tower/widgets/PortalJobs";
|
||||
import SCMSyncStatus from "tower/widgets/SCMSyncStatus";
|
||||
import Stream from "tower/widgets/Stream";
|
||||
import "tower/widgets/DashboardCounts";
|
||||
import "tower/widgets/DashboardJobs";
|
||||
import "tower/widgets/HostGraph";
|
||||
import "tower/widgets/HostPieChart";
|
||||
import "tower/widgets/InventorySyncStatus";
|
||||
import "tower/widgets/JobStatus";
|
||||
import "tower/widgets/JobStatusGraph";
|
||||
import "tower/widgets/ObjectCount";
|
||||
import "tower/widgets/PortalJobs";
|
||||
import "tower/widgets/SCMSyncStatus";
|
||||
import "tower/widgets/Stream";
|
||||
|
||||
export
|
||||
{ DashboardCounts,
|
||||
DashboardJobs,
|
||||
HostGraph,
|
||||
HostPieChart,
|
||||
InventorySyncStatus,
|
||||
JobStatus,
|
||||
JobStatusGraph,
|
||||
ObjectCount,
|
||||
PortalJobs,
|
||||
SCMSyncStatus,
|
||||
Stream
|
||||
};
|
||||
|
||||
21
awx/ui/static/lib/jquery-ui/.bower.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "jquery-ui",
|
||||
"version": "1.11.3",
|
||||
"main": [
|
||||
"jquery-ui.js"
|
||||
],
|
||||
"ignore": [],
|
||||
"dependencies": {
|
||||
"jquery": ">=1.6"
|
||||
},
|
||||
"homepage": "https://github.com/components/jqueryui",
|
||||
"_release": "1.11.3",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "1.11.3",
|
||||
"commit": "7e2d6bec1c729925d9ad7c0c9ab4b561174efa99"
|
||||
},
|
||||
"_source": "git://github.com/components/jqueryui.git",
|
||||
"_target": "~1.11.1",
|
||||
"_originalSource": "jquery-ui"
|
||||
}
|
||||
4
awx/ui/static/lib/jquery-ui/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
components
|
||||
composer.lock
|
||||
vendor
|
||||
.DS_Store
|
||||
284
awx/ui/static/lib/jquery-ui/AUTHORS.txt
Normal file
@ -0,0 +1,284 @@
|
||||
Authors ordered by first contribution
|
||||
A list of current team members is available at http://jqueryui.com/about
|
||||
|
||||
Paul Bakaus <paul.bakaus@gmail.com>
|
||||
Richard Worth <rdworth@gmail.com>
|
||||
Yehuda Katz <wycats@gmail.com>
|
||||
Sean Catchpole <sean@sunsean.com>
|
||||
John Resig <jeresig@gmail.com>
|
||||
Tane Piper <piper.tane@gmail.com>
|
||||
Dmitri Gaskin <dmitrig01@gmail.com>
|
||||
Klaus Hartl <klaus.hartl@gmail.com>
|
||||
Stefan Petre <stefan.petre@gmail.com>
|
||||
Gilles van den Hoven <gilles@webunity.nl>
|
||||
Micheil Bryan Smith <micheil@brandedcode.com>
|
||||
Jörn Zaefferer <joern.zaefferer@gmail.com>
|
||||
Marc Grabanski <m@marcgrabanski.com>
|
||||
Keith Wood <kbwood@iinet.com.au>
|
||||
Brandon Aaron <brandon.aaron@gmail.com>
|
||||
Scott González <scott.gonzalez@gmail.com>
|
||||
Eduardo Lundgren <eduardolundgren@gmail.com>
|
||||
Aaron Eisenberger <aaronchi@gmail.com>
|
||||
Joan Piedra <theneojp@gmail.com>
|
||||
Bruno Basto <b.basto@gmail.com>
|
||||
Remy Sharp <remy@leftlogic.com>
|
||||
Bohdan Ganicky <bohdan.ganicky@gmail.com>
|
||||
David Bolter <david.bolter@gmail.com>
|
||||
Chi Cheng <cloudream@gmail.com>
|
||||
Ca-Phun Ung <pazu2k@gmail.com>
|
||||
Ariel Flesler <aflesler@gmail.com>
|
||||
Maggie Wachs <maggie@filamentgroup.com>
|
||||
Scott Jehl <scott@scottjehl.com>
|
||||
Todd Parker <todd@filamentgroup.com>
|
||||
Andrew Powell <andrew@shellscape.org>
|
||||
Brant Burnett <btburnett3@gmail.com>
|
||||
Douglas Neiner <doug@dougneiner.com>
|
||||
Paul Irish <paul.irish@gmail.com>
|
||||
Ralph Whitbeck <ralph.whitbeck@gmail.com>
|
||||
Thibault Duplessis <thibault.duplessis@gmail.com>
|
||||
Dominique Vincent <dominique.vincent@toitl.com>
|
||||
Jack Hsu <jack.hsu@gmail.com>
|
||||
Adam Sontag <ajpiano@ajpiano.com>
|
||||
Carl Fürstenberg <carl@excito.com>
|
||||
Kevin Dalman <development@allpro.net>
|
||||
Alberto Fernández Capel <afcapel@gmail.com>
|
||||
Jacek Jędrzejewski (http://jacek.jedrzejewski.name)
|
||||
Ting Kuei <ting@kuei.com>
|
||||
Samuel Cormier-Iijima <sam@chide.it>
|
||||
Jon Palmer <jonspalmer@gmail.com>
|
||||
Ben Hollis <bhollis@amazon.com>
|
||||
Justin MacCarthy <Justin@Rubystars.biz>
|
||||
Eyal Kobrigo <kobrigo@hotmail.com>
|
||||
Tiago Freire <tiago.freire@gmail.com>
|
||||
Diego Tres <diegotres@gmail.com>
|
||||
Holger Rüprich <holger@rueprich.de>
|
||||
Ziling Zhao <zizhao@cisco.com>
|
||||
Mike Alsup <malsup@gmail.com>
|
||||
Robson Braga Araujo <robsonbraga@gmail.com>
|
||||
Pierre-Henri Ausseil <ph.ausseil@gmail.com>
|
||||
Christopher McCulloh <cmcculloh@gmail.com>
|
||||
Andrew Newcomb <ext.github@preceptsoftware.co.uk>
|
||||
Lim Chee Aun <cheeaun@gmail.com>
|
||||
Jorge Barreiro <yortx.barry@gmail.com>
|
||||
Daniel Steigerwald <daniel@steigerwald.cz>
|
||||
John Firebaugh <john_firebaugh@bigfix.com>
|
||||
John Enters <github@darkdark.net>
|
||||
Andrey Kapitcyn <ru.m157y@gmail.com>
|
||||
Dmitry Petrov <dpetroff@gmail.com>
|
||||
Eric Hynds <eric@hynds.net>
|
||||
Chairat Sunthornwiphat <pipo@sixhead.com>
|
||||
Josh Varner <josh.varner@gmail.com>
|
||||
Stéphane Raimbault <stephane.raimbault@gmail.com>
|
||||
Jay Merrifield <fracmak@gmail.com>
|
||||
J. Ryan Stinnett <jryans@gmail.com>
|
||||
Peter Heiberg <peter@heiberg.se>
|
||||
Alex Dovenmuehle <adovenmuehle@gmail.com>
|
||||
Jamie Gegerson <git@jamiegegerson.com>
|
||||
Raymond Schwartz <skeetergraphics@gmail.com>
|
||||
Phillip Barnes <philbar@gmail.com>
|
||||
Kyle Wilkinson <kai@wikyd.org>
|
||||
Khaled AlHourani <me@khaledalhourani.com>
|
||||
Marian Rudzynski <mr@impaled.org>
|
||||
Jean-Francois Remy <jeff@melix.org>
|
||||
Doug Blood
|
||||
Filippo Cavallarin <filippo.cavallarin@codseq.it>
|
||||
Heiko Henning <heiko@thehennings.ch>
|
||||
Aliaksandr Rahalevich <saksmlz@gmail.com>
|
||||
Mario Visic <mario@mariovisic.com>
|
||||
Xavi Ramirez <xavi.rmz@gmail.com>
|
||||
Max Schnur <max.schnur@gmail.com>
|
||||
Saji Nediyanchath <saji89@gmail.com>
|
||||
Corey Frang <gnarf37@gmail.com>
|
||||
Aaron Peterson <aaronp123@yahoo.com>
|
||||
Ivan Peters <ivan@ivanpeters.com>
|
||||
Mohamed Cherif Bouchelaghem <cherifbouchelaghem@yahoo.fr>
|
||||
Marcos Sousa <falecomigo@marcossousa.com>
|
||||
Michael DellaNoce <mdellanoce@mailtrust.com>
|
||||
George Marshall <echosx@gmail.com>
|
||||
Tobias Brunner <tobias@strongswan.org>
|
||||
Martin Solli <msolli@gmail.com>
|
||||
David Petersen <public@petersendidit.com>
|
||||
Dan Heberden <danheberden@gmail.com>
|
||||
William Kevin Manire <williamkmanire@gmail.com>
|
||||
Gilmore Davidson <gilmoreorless@gmail.com>
|
||||
Michael Wu <michaelmwu@gmail.com>
|
||||
Adam Parod <mystic414@gmail.com>
|
||||
Guillaume Gautreau <guillaume+github@ghusse.com>
|
||||
Marcel Toele <EleotleCram@gmail.com>
|
||||
Dan Streetman <ddstreet@ieee.org>
|
||||
Matt Hoskins <matt@nipltd.com>
|
||||
Giovanni Giacobbi <giovanni@giacobbi.net>
|
||||
Kyle Florence <kyle.florence@gmail.com>
|
||||
Pavol Hluchý <lopo@losys.sk>
|
||||
Hans Hillen <hans.hillen@gmail.com>
|
||||
Mark Johnson <virgofx@live.com>
|
||||
Trey Hunner <treyhunner@gmail.com>
|
||||
Shane Whittet <whittet@gmail.com>
|
||||
Edward A Faulkner <ef@alum.mit.edu>
|
||||
Adam Baratz <adam@adambaratz.com>
|
||||
Kato Kazuyoshi <kato.kazuyoshi@gmail.com>
|
||||
Eike Send <eike.send@gmail.com>
|
||||
Kris Borchers <kris.borchers@gmail.com>
|
||||
Eddie Monge <eddie@eddiemonge.com>
|
||||
Israel Tsadok <itsadok@gmail.com>
|
||||
Carson McDonald <carson@ioncannon.net>
|
||||
Jason Davies <jason@jasondavies.com>
|
||||
Garrison Locke <gplocke@gmail.com>
|
||||
David Murdoch <david@davidmurdoch.com>
|
||||
Benjamin Scott Boyle <benjamins.boyle@gmail.com>
|
||||
Jesse Baird <jebaird@gmail.com>
|
||||
Jonathan Vingiano <jvingiano@gmail.com>
|
||||
Dylan Just <dev@ephox.com>
|
||||
Hiroshi Tomita <tomykaira@gmail.com>
|
||||
Glenn Goodrich <glenn.goodrich@gmail.com>
|
||||
Tarafder Ashek-E-Elahi <mail.ashek@gmail.com>
|
||||
Ryan Neufeld <ryan@neufeldmail.com>
|
||||
Marc Neuwirth <marc.neuwirth@gmail.com>
|
||||
Philip Graham <philip.robert.graham@gmail.com>
|
||||
Benjamin Sterling <benjamin.sterling@kenzomedia.com>
|
||||
Wesley Walser <waw325@gmail.com>
|
||||
Kouhei Sutou <kou@clear-code.com>
|
||||
Karl Kirch <karlkrch@gmail.com>
|
||||
Chris Kelly <ckdake@ckdake.com>
|
||||
Jay Oster <jay@loyalize.com>
|
||||
Alexander Polomoshnov <alex.polomoshnov@gmail.com>
|
||||
David Leal <dgleal@gmail.com>
|
||||
Igor Milla <igor.fsp.milla@gmail.com>
|
||||
Dave Methvin <dave.methvin@gmail.com>
|
||||
Florian Gutmann <f.gutmann@chronimo.com>
|
||||
Marwan Al Jubeh <marwan.aljubeh@gmail.com>
|
||||
Milan Broum <midlis@googlemail.com>
|
||||
Sebastian Sauer <info@dynpages.de>
|
||||
Gaëtan Muller <m.gaetan89@gmail.com>
|
||||
William Griffiths <william@ycymro.com>
|
||||
Stojce Slavkovski <stojce@gmail.com>
|
||||
David Soms <david.soms@gmail.com>
|
||||
David De Sloovere <david.desloovere@outlook.com>
|
||||
Michael P. Jung <michael.jung@terreon.de>
|
||||
Shannon Pekary <spekary@gmail.com>
|
||||
Matthew Edward Hutton <meh@corefiling.co.uk>
|
||||
James Khoury <james@jameskhoury.com>
|
||||
Rob Loach <robloach@gmail.com>
|
||||
Alberto Monteiro <betimbrasil@gmail.com>
|
||||
Alex Rhea <alex.rhea@gmail.com>
|
||||
Krzysztof Rosiński <rozwell69@gmail.com>
|
||||
Ryan Olton <oltonr@gmail.com>
|
||||
Genie <386@mail.com>
|
||||
Rick Waldron <waldron.rick@gmail.com>
|
||||
Ian Simpson <spoonlikesham@gmail.com>
|
||||
Lev Kitsis <spam4lev@gmail.com>
|
||||
TJ VanToll <tj.vantoll@gmail.com>
|
||||
Justin Domnitz <jdomnitz@gmail.com>
|
||||
Douglas Cerna <douglascerna@yahoo.com>
|
||||
Bert ter Heide <bertjh@hotmail.com>
|
||||
Jasvir Nagra <jasvir@gmail.com>
|
||||
Yuriy Khabarov <13real008@gmail.com>
|
||||
Harri Kilpiö <harri.kilpio@gmail.com>
|
||||
Lado Lomidze <lado.lomidze@gmail.com>
|
||||
Amir E. Aharoni <amir.aharoni@mail.huji.ac.il>
|
||||
Simon Sattes <simon.sattes@gmail.com>
|
||||
Jo Liss <joliss42@gmail.com>
|
||||
Guntupalli Karunakar <karunakarg@yahoo.com>
|
||||
Shahyar Ghobadpour <shahyar@gmail.com>
|
||||
Lukasz Lipinski <uzza17@gmail.com>
|
||||
Timo Tijhof <krinklemail@gmail.com>
|
||||
Jason Moon <jmoon@socialcast.com>
|
||||
Martin Frost <martinf55@hotmail.com>
|
||||
Eneko Illarramendi <eneko@illarra.com>
|
||||
EungJun Yi <semtlenori@gmail.com>
|
||||
Courtland Allen <courtlandallen@gmail.com>
|
||||
Viktar Varvanovich <non4eg@gmail.com>
|
||||
Danny Trunk <dtrunk90@gmail.com>
|
||||
Pavel Stetina <pavel.stetina@nangu.tv>
|
||||
Michael Stay <metaweta@gmail.com>
|
||||
Steven Roussey <sroussey@gmail.com>
|
||||
Michael Hollis <hollis21@gmail.com>
|
||||
Lee Rowlands <lee.rowlands@previousnext.com.au>
|
||||
Timmy Willison <timmywillisn@gmail.com>
|
||||
Karl Swedberg <kswedberg@gmail.com>
|
||||
Baoju Yuan <the_guy_1987@hotmail.com>
|
||||
Maciej Mroziński <maciej.k.mrozinski@gmail.com>
|
||||
Luis Dalmolin <luis.nh@gmail.com>
|
||||
Mark Aaron Shirley <maspwr@gmail.com>
|
||||
Martin Hoch <martin@fidion.de>
|
||||
Jiayi Yang <tr870829@gmail.com>
|
||||
Philipp Benjamin Köppchen <xgxtpbk@gws.ms>
|
||||
Sindre Sorhus <sindresorhus@gmail.com>
|
||||
Bernhard Sirlinger <bernhard.sirlinger@tele2.de>
|
||||
Jared A. Scheel <jared@jaredscheel.com>
|
||||
Rafael Xavier de Souza <rxaviers@gmail.com>
|
||||
John Chen <zhang.z.chen@intel.com>
|
||||
Dale Kocian <dale.kocian@gmail.com>
|
||||
Mike Sherov <mike.sherov@gmail.com>
|
||||
Andrew Couch <andy@couchand.com>
|
||||
Marc-Andre Lafortune <github@marc-andre.ca>
|
||||
Nate Eagle <nate.eagle@teamaol.com>
|
||||
David Souther <davidsouther@gmail.com>
|
||||
Mathias Stenbom <mathias@stenbom.com>
|
||||
Sergey Kartashov <ebishkek@yandex.ru>
|
||||
Avinash R <nashpapa@gmail.com>
|
||||
Ethan Romba <ethanromba@gmail.com>
|
||||
Cory Gackenheimer <cory.gack@gmail.com>
|
||||
Juan Pablo Kaniefsky <jpkaniefsky@gmail.com>
|
||||
Roman Salnikov <bardt.dz@gmail.com>
|
||||
Anika Henke <anika@selfthinker.org>
|
||||
Samuel Bovée <samycookie2000@yahoo.fr>
|
||||
Fabrício Matté <ult_combo@hotmail.com>
|
||||
Viktor Kojouharov <vkojouharov@gmail.com>
|
||||
Pawel Maruszczyk (http://hrabstwo.net)
|
||||
Pavel Selitskas <p.selitskas@gmail.com>
|
||||
Bjørn Johansen <post@bjornjohansen.no>
|
||||
Matthieu Penant <thieum22@hotmail.com>
|
||||
Dominic Barnes <dominic@dbarnes.info>
|
||||
David Sullivan <david.sullivan@gmail.com>
|
||||
Thomas Jaggi <thomas@responsive.ch>
|
||||
Vahid Sohrabloo <vahid4134@gmail.com>
|
||||
Travis Carden <travis.carden@gmail.com>
|
||||
Bruno M. Custódio <bruno@brunomcustodio.com>
|
||||
Nathanael Silverman <nathanael.silverman@gmail.com>
|
||||
Christian Wenz <christian@wenz.org>
|
||||
Steve Urmston <steve@urm.st>
|
||||
Zaven Muradyan <megalivoithos@gmail.com>
|
||||
Woody Gilk <shadowhand@deviantart.com>
|
||||
Zbigniew Motyka <zbigniew.motyka@gmail.com>
|
||||
Suhail Alkowaileet <xsoh.k7@gmail.com>
|
||||
Toshi MARUYAMA <marutosijp2@yahoo.co.jp>
|
||||
David Hansen <hansede@gmail.com>
|
||||
Brian Grinstead <briangrinstead@gmail.com>
|
||||
Christian Klammer <christian314159@gmail.com>
|
||||
Steven Luscher <jquerycla@steveluscher.com>
|
||||
Gan Eng Chin <engchin.gan@gmail.com>
|
||||
Gabriel Schulhof <gabriel.schulhof@intel.com>
|
||||
Alexander Schmitz <arschmitz@gmail.com>
|
||||
Vilhjálmur Skúlason <vis@dmm.is>
|
||||
Siebrand Mazeland <s.mazeland@xs4all.nl>
|
||||
Mohsen Ekhtiari <mohsenekhtiari@yahoo.com>
|
||||
Pere Orga <gotrunks@gmail.com>
|
||||
Jasper de Groot <mail@ugomobi.com>
|
||||
Stephane Deschamps <stephane.deschamps@gmail.com>
|
||||
Jyoti Deka <dekajp@gmail.com>
|
||||
Andrei Picus <office.nightcrawler@gmail.com>
|
||||
Ondrej Novy <novy@ondrej.org>
|
||||
Jacob McCutcheon <jacob.mccutcheon@gmail.com>
|
||||
Monika Piotrowicz <monika.piotrowicz@gmail.com>
|
||||
Imants Horsts <imants.horsts@inbox.lv>
|
||||
Eric Dahl <eric.c.dahl@gmail.com>
|
||||
Dave Stein <dave@behance.com>
|
||||
Dylan Barrell <dylan@barrell.com>
|
||||
Daniel DeGroff <djdegroff@gmail.com>
|
||||
Michael Wiencek <mwtuea@gmail.com>
|
||||
Thomas Meyer <meyertee@gmail.com>
|
||||
Ruslan Yakhyaev <ruslan@ruslan.io>
|
||||
Brian J. Dowling <bjd-dev@simplicity.net>
|
||||
Ben Higgins <ben@extrahop.com>
|
||||
Yermo Lamers <yml@yml.com>
|
||||
Patrick Stapleton <github@gdi2290.com>
|
||||
Trisha Crowley <trisha.crowley@gmail.com>
|
||||
Usman Akeju <akeju00+github@gmail.com>
|
||||
Rodrigo Menezes <rod333@gmail.com>
|
||||
Jacques Perrault <jacques_perrault@us.ibm.com>
|
||||
Frederik Elvhage <frederik.elvhage@googlemail.com>
|
||||
Will Holley <willholley@gmail.com>
|
||||
Uri Gilad <antishok@gmail.com>
|
||||
Richard Gibson <richard.gibson@gmail.com>
|
||||
Simen Bekkhus <sbekkhus91@gmail.com>
|
||||
44
awx/ui/static/lib/jquery-ui/LICENSE.txt
Normal file
@ -0,0 +1,44 @@
|
||||
Copyright 2007, 2014 jQuery Foundation and other contributors,
|
||||
https://jquery.org/
|
||||
|
||||
This software consists of voluntary contributions made by many
|
||||
individuals. For exact contribution history, see the revision history
|
||||
available at https://github.com/jquery/jquery-ui
|
||||
|
||||
The following license applies to all parts of this software except as
|
||||
documented below:
|
||||
|
||||
====
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
====
|
||||
|
||||
Copyright and related rights for sample code are waived via CC0. Sample
|
||||
code is defined as all source code contained within the demos directory.
|
||||
|
||||
CC0: http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
====
|
||||
|
||||
All files located in the node_modules and external directories are
|
||||
externally maintained libraries used by this software which have their
|
||||
own licenses; we recommend you read them, as their terms may differ from
|
||||
the terms above.
|
||||
11
awx/ui/static/lib/jquery-ui/README.md
Normal file
@ -0,0 +1,11 @@
|
||||
[jQuery UI](http://jqueryui.com/) - Interactions and Widgets for the web
|
||||
================================
|
||||
|
||||
jQuery UI provides interactions like Drag and Drop and widgets like Autocomplete, Tabs and Slider and makes these as easy to use as jQuery itself.
|
||||
|
||||
If you want to use jQuery UI, go to [jqueryui.com](http://jqueryui.com) to get started. Or visit the [Using jQuery UI Forum](http://forum.jquery.com/using-jquery-ui) for discussions and questions.
|
||||
|
||||
If you are interested in helping develop jQuery UI, you are in the right place.
|
||||
To discuss development with team members and the community, visit the [Developing jQuery UI Forum](http://forum.jquery.com/developing-jquery-ui) or in #jquery on irc.freednode.net.
|
||||
|
||||
## This repo only holds precompiled files.
|
||||
12
awx/ui/static/lib/jquery-ui/bower.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "jquery-ui",
|
||||
"version": "1.11.3",
|
||||
"main": [
|
||||
"jquery-ui.js"
|
||||
],
|
||||
"ignore": [
|
||||
],
|
||||
"dependencies": {
|
||||
"jquery": ">=1.6"
|
||||
}
|
||||
}
|
||||
13
awx/ui/static/lib/jquery-ui/component.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "jquery-ui",
|
||||
"repo": "components/jqueryui",
|
||||
"version": "1.11.3",
|
||||
"license": "MIT",
|
||||
"scripts": [
|
||||
"jquery-ui.js"
|
||||
],
|
||||
"main": "jquery-ui.js",
|
||||
"dependencies": {
|
||||
"components/jquery": "*"
|
||||
}
|
||||
}
|
||||
69
awx/ui/static/lib/jquery-ui/composer.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "components/jqueryui",
|
||||
"type": "component",
|
||||
"description": "jQuery UI is a curated set of user interface interactions, effects, widgets, and themes built on top of the jQuery JavaScript Library. Whether you're building highly interactive web applications or you just need to add a date picker to a form control, jQuery UI is the perfect choice.",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"components/jquery": ">=1.6"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "jQuery UI Team",
|
||||
"homepage": "http://jqueryui.com/about"
|
||||
},
|
||||
{
|
||||
"name": "Scott Gonzalez",
|
||||
"email": "scott.gonzalez@gmail.com",
|
||||
"homepage": "http://scottgonzalez.com"
|
||||
},
|
||||
{
|
||||
"name": "Joern Zaefferer",
|
||||
"email": "joern.zaefferer@gmail.com",
|
||||
"homepage": "http://bassistance.de"
|
||||
},
|
||||
{
|
||||
"name": "Kris Borchers",
|
||||
"email": "kris.borchers@gmail.com",
|
||||
"homepage": "http://krisborchers.com"
|
||||
},
|
||||
{
|
||||
"name": "Corey Frang",
|
||||
"email": "gnarf37@gmail.com",
|
||||
"homepage": "http://gnarf.net"
|
||||
},
|
||||
{
|
||||
"name": "Mike Sherov",
|
||||
"email": "mike.sherov@gmail.com",
|
||||
"homepage": "http://mike.sherov.com"
|
||||
},
|
||||
{
|
||||
"name": "TJ VanToll",
|
||||
"email": "tj.vantoll@gmail.com",
|
||||
"homepage": "http://tjvantoll.com"
|
||||
},
|
||||
{
|
||||
"name": "Felix Nagel",
|
||||
"email": "info@felixnagel.com",
|
||||
"homepage": "http://www.felixnagel.com"
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"component": {
|
||||
"name": "jquery-ui",
|
||||
"scripts": [
|
||||
"jquery-ui.js"
|
||||
],
|
||||
"files": [
|
||||
"ui/**",
|
||||
"themes/**",
|
||||
"jquery-ui.min.js"
|
||||
],
|
||||
"shim": {
|
||||
"deps": [
|
||||
"jquery"
|
||||
],
|
||||
"exports": "jQuery"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16608
awx/ui/static/lib/jquery-ui/jquery-ui.js
vendored
Normal file
13
awx/ui/static/lib/jquery-ui/jquery-ui.min.js
vendored
Normal file
71
awx/ui/static/lib/jquery-ui/package.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "jquery-ui",
|
||||
"title": "jQuery UI",
|
||||
"description": "A curated set of user interface interactions, effects, widgets, and themes built on top of the jQuery JavaScript Library.",
|
||||
"version": "1.11.3",
|
||||
"homepage": "http://jqueryui.com",
|
||||
"author": {
|
||||
"name": "jQuery Foundation and other contributors",
|
||||
"url": "https://github.com/jquery/jquery-ui/blob/1-11-stable/AUTHORS.txt"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Scott González",
|
||||
"email": "scott.gonzalez@gmail.com",
|
||||
"url": "http://scottgonzalez.com"
|
||||
},
|
||||
{
|
||||
"name": "Jörn Zaefferer",
|
||||
"email": "joern.zaefferer@gmail.com",
|
||||
"url": "http://bassistance.de"
|
||||
},
|
||||
{
|
||||
"name": "Kris Borchers",
|
||||
"email": "kris.borchers@gmail.com",
|
||||
"url": "http://krisborchers.com"
|
||||
},
|
||||
{
|
||||
"name": "Corey Frang",
|
||||
"email": "gnarf37@gmail.com",
|
||||
"url": "http://gnarf.net"
|
||||
},
|
||||
{
|
||||
"name": "Mike Sherov",
|
||||
"email": "mike.sherov@gmail.com",
|
||||
"url": "http://mike.sherov.com"
|
||||
},
|
||||
{
|
||||
"name": "TJ VanToll",
|
||||
"email": "tj.vantoll@gmail.com",
|
||||
"url": "http://tjvantoll.com"
|
||||
},
|
||||
{
|
||||
"name": "Felix Nagel",
|
||||
"email": "info@felixnagel.com",
|
||||
"url": "http://www.felixnagel.com"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/jquery/jquery-ui.git"
|
||||
},
|
||||
"bugs": "http://bugs.jqueryui.com/",
|
||||
"licenses": [
|
||||
{
|
||||
"type": "MIT",
|
||||
"url": "https://github.com/jquery/jquery-ui/blob/1-11-stable/LICENSE.txt"
|
||||
}
|
||||
],
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"grunt": "~0.3.17",
|
||||
"grunt-css": "0.2.0",
|
||||
"grunt-compare-size": "0.1.4",
|
||||
"grunt-html": "0.1.1",
|
||||
"grunt-junit": "0.1.5",
|
||||
"grunt-git-authors": "1.0.0",
|
||||
"rimraf": "2.0.1",
|
||||
"testswarm": "0.3.0"
|
||||
},
|
||||
"keywords": []
|
||||
}
|
||||
36
awx/ui/static/lib/jquery-ui/themes/base/accordion.css
Normal file
@ -0,0 +1,36 @@
|
||||
/*!
|
||||
* jQuery UI Accordion 1.11.3
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://api.jqueryui.com/accordion/#theming
|
||||
*/
|
||||
.ui-accordion .ui-accordion-header {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
margin: 2px 0 0 0;
|
||||
padding: .5em .5em .5em .7em;
|
||||
min-height: 0; /* support: IE7 */
|
||||
font-size: 100%;
|
||||
}
|
||||
.ui-accordion .ui-accordion-icons {
|
||||
padding-left: 2.2em;
|
||||
}
|
||||
.ui-accordion .ui-accordion-icons .ui-accordion-icons {
|
||||
padding-left: 2.2em;
|
||||
}
|
||||
.ui-accordion .ui-accordion-header .ui-accordion-header-icon {
|
||||
position: absolute;
|
||||
left: .5em;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
}
|
||||
.ui-accordion .ui-accordion-content {
|
||||
padding: 1em 2.2em;
|
||||
border-top: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
12
awx/ui/static/lib/jquery-ui/themes/base/all.css
Normal file
@ -0,0 +1,12 @@
|
||||
/*!
|
||||
* jQuery UI CSS Framework 1.11.3
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://api.jqueryui.com/category/theming/
|
||||
*/
|
||||
@import "base.css";
|
||||
@import "theme.css";
|
||||
16
awx/ui/static/lib/jquery-ui/themes/base/autocomplete.css
Normal file
@ -0,0 +1,16 @@
|
||||
/*!
|
||||
* jQuery UI Autocomplete 1.11.3
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://api.jqueryui.com/autocomplete/#theming
|
||||
*/
|
||||
.ui-autocomplete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
cursor: default;
|
||||
}
|
||||
28
awx/ui/static/lib/jquery-ui/themes/base/base.css
Normal file
@ -0,0 +1,28 @@
|
||||
/*!
|
||||
* jQuery UI CSS Framework 1.11.3
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://api.jqueryui.com/category/theming/
|
||||
*/
|
||||
@import url("core.css");
|
||||
|
||||
@import url("accordion.css");
|
||||
@import url("autocomplete.css");
|
||||
@import url("button.css");
|
||||
@import url("datepicker.css");
|
||||
@import url("dialog.css");
|
||||
@import url("draggable.css");
|
||||
@import url("menu.css");
|
||||
@import url("progressbar.css");
|
||||
@import url("resizable.css");
|
||||
@import url("selectable.css");
|
||||
@import url("selectmenu.css");
|
||||
@import url("sortable.css");
|
||||
@import url("slider.css");
|
||||
@import url("spinner.css");
|
||||
@import url("tabs.css");
|
||||
@import url("tooltip.css");
|
||||
114
awx/ui/static/lib/jquery-ui/themes/base/button.css
Normal file
@ -0,0 +1,114 @@
|
||||
/*!
|
||||
* jQuery UI Button 1.11.3
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://api.jqueryui.com/button/#theming
|
||||
*/
|
||||
.ui-button {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
margin-right: .1em;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
overflow: visible; /* removes extra width in IE */
|
||||
}
|
||||
.ui-button,
|
||||
.ui-button:link,
|
||||
.ui-button:visited,
|
||||
.ui-button:hover,
|
||||
.ui-button:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
/* to make room for the icon, a width needs to be set here */
|
||||
.ui-button-icon-only {
|
||||
width: 2.2em;
|
||||
}
|
||||
/* button elements seem to need a little more width */
|
||||
button.ui-button-icon-only {
|
||||
width: 2.4em;
|
||||
}
|
||||
.ui-button-icons-only {
|
||||
width: 3.4em;
|
||||
}
|
||||
button.ui-button-icons-only {
|
||||
width: 3.7em;
|
||||
}
|
||||
|
||||
/* button text element */
|
||||
.ui-button .ui-button-text {
|
||||
display: block;
|
||||
line-height: normal;
|
||||
}
|
||||
.ui-button-text-only .ui-button-text {
|
||||
padding: .4em 1em;
|
||||
}
|
||||
.ui-button-icon-only .ui-button-text,
|
||||
.ui-button-icons-only .ui-button-text {
|
||||
padding: .4em;
|
||||
text-indent: -9999999px;
|
||||
}
|
||||
.ui-button-text-icon-primary .ui-button-text,
|
||||
.ui-button-text-icons .ui-button-text {
|
||||
padding: .4em 1em .4em 2.1em;
|
||||
}
|
||||
.ui-button-text-icon-secondary .ui-button-text,
|
||||
.ui-button-text-icons .ui-button-text {
|
||||
padding: .4em 2.1em .4em 1em;
|
||||
}
|
||||
.ui-button-text-icons .ui-button-text {
|
||||
padding-left: 2.1em;
|
||||
padding-right: 2.1em;
|
||||
}
|
||||
/* no icon support for input elements, provide padding by default */
|
||||
input.ui-button {
|
||||
padding: .4em 1em;
|
||||
}
|
||||
|
||||
/* button icon element(s) */
|
||||
.ui-button-icon-only .ui-icon,
|
||||
.ui-button-text-icon-primary .ui-icon,
|
||||
.ui-button-text-icon-secondary .ui-icon,
|
||||
.ui-button-text-icons .ui-icon,
|
||||
.ui-button-icons-only .ui-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
}
|
||||
.ui-button-icon-only .ui-icon {
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
}
|
||||
.ui-button-text-icon-primary .ui-button-icon-primary,
|
||||
.ui-button-text-icons .ui-button-icon-primary,
|
||||
.ui-button-icons-only .ui-button-icon-primary {
|
||||
left: .5em;
|
||||
}
|
||||
.ui-button-text-icon-secondary .ui-button-icon-secondary,
|
||||
.ui-button-text-icons .ui-button-icon-secondary,
|
||||
.ui-button-icons-only .ui-button-icon-secondary {
|
||||
right: .5em;
|
||||
}
|
||||
|
||||
/* button sets */
|
||||
.ui-buttonset {
|
||||
margin-right: 7px;
|
||||
}
|
||||
.ui-buttonset .ui-button {
|
||||
margin-left: 0;
|
||||
margin-right: -.3em;
|
||||
}
|
||||
|
||||
/* workarounds */
|
||||
/* reset extra padding in Firefox, see h5bp.com/l */
|
||||
input.ui-button::-moz-focus-inner,
|
||||
button.ui-button::-moz-focus-inner {
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
93
awx/ui/static/lib/jquery-ui/themes/base/core.css
Normal file
@ -0,0 +1,93 @@
|
||||
/*!
|
||||
* jQuery UI CSS Framework 1.11.3
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://api.jqueryui.com/category/theming/
|
||||
*/
|
||||
|
||||
/* Layout helpers
|
||||
----------------------------------*/
|
||||
.ui-helper-hidden {
|
||||
display: none;
|
||||
}
|
||||
.ui-helper-hidden-accessible {
|
||||
border: 0;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
}
|
||||
.ui-helper-reset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
line-height: 1.3;
|
||||
text-decoration: none;
|
||||
font-size: 100%;
|
||||
list-style: none;
|
||||
}
|
||||
.ui-helper-clearfix:before,
|
||||
.ui-helper-clearfix:after {
|
||||
content: "";
|
||||
display: table;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.ui-helper-clearfix:after {
|
||||
clear: both;
|
||||
}
|
||||
.ui-helper-clearfix {
|
||||
min-height: 0; /* support: IE7 */
|
||||
}
|
||||
.ui-helper-zfix {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
filter:Alpha(Opacity=0); /* support: IE8 */
|
||||
}
|
||||
|
||||
.ui-front {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
/* Interaction Cues
|
||||
----------------------------------*/
|
||||
.ui-state-disabled {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
|
||||
/* Icons
|
||||
----------------------------------*/
|
||||
|
||||
/* states and images */
|
||||
.ui-icon {
|
||||
display: block;
|
||||
text-indent: -99999px;
|
||||
overflow: hidden;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
|
||||
/* Misc visuals
|
||||
----------------------------------*/
|
||||
|
||||
/* Overlays */
|
||||
.ui-widget-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
175
awx/ui/static/lib/jquery-ui/themes/base/datepicker.css
Normal file
@ -0,0 +1,175 @@
|
||||
/*!
|
||||
* jQuery UI Datepicker 1.11.3
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://api.jqueryui.com/datepicker/#theming
|
||||
*/
|
||||
.ui-datepicker {
|
||||
width: 17em;
|
||||
padding: .2em .2em 0;
|
||||
display: none;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-header {
|
||||
position: relative;
|
||||
padding: .2em 0;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-prev,
|
||||
.ui-datepicker .ui-datepicker-next {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
width: 1.8em;
|
||||
height: 1.8em;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-prev-hover,
|
||||
.ui-datepicker .ui-datepicker-next-hover {
|
||||
top: 1px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-prev {
|
||||
left: 2px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-next {
|
||||
right: 2px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-prev-hover {
|
||||
left: 1px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-next-hover {
|
||||
right: 1px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-prev span,
|
||||
.ui-datepicker .ui-datepicker-next span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
top: 50%;
|
||||
margin-top: -8px;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-title {
|
||||
margin: 0 2.3em;
|
||||
line-height: 1.8em;
|
||||
text-align: center;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-title select {
|
||||
font-size: 1em;
|
||||
margin: 1px 0;
|
||||
}
|
||||
.ui-datepicker select.ui-datepicker-month,
|
||||
.ui-datepicker select.ui-datepicker-year {
|
||||
width: 45%;
|
||||
}
|
||||
.ui-datepicker table {
|
||||
width: 100%;
|
||||
font-size: .9em;
|
||||
border-collapse: collapse;
|
||||
margin: 0 0 .4em;
|
||||
}
|
||||
.ui-datepicker th {
|
||||
padding: .7em .3em;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
border: 0;
|
||||
}
|
||||
.ui-datepicker td {
|
||||
border: 0;
|
||||
padding: 1px;
|
||||
}
|
||||
.ui-datepicker td span,
|
||||
.ui-datepicker td a {
|
||||
display: block;
|
||||
padding: .2em;
|
||||
text-align: right;
|
||||
text-decoration: none;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-buttonpane {
|
||||
background-image: none;
|
||||
margin: .7em 0 0 0;
|
||||
padding: 0 .2em;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-buttonpane button {
|
||||
float: right;
|
||||
margin: .5em .2em .4em;
|
||||
cursor: pointer;
|
||||
padding: .2em .6em .3em .6em;
|
||||
width: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/* with multiple calendars */
|
||||
.ui-datepicker.ui-datepicker-multi {
|
||||
width: auto;
|
||||
}
|
||||
.ui-datepicker-multi .ui-datepicker-group {
|
||||
float: left;
|
||||
}
|
||||
.ui-datepicker-multi .ui-datepicker-group table {
|
||||
width: 95%;
|
||||
margin: 0 auto .4em;
|
||||
}
|
||||
.ui-datepicker-multi-2 .ui-datepicker-group {
|
||||
width: 50%;
|
||||
}
|
||||
.ui-datepicker-multi-3 .ui-datepicker-group {
|
||||
width: 33.3%;
|
||||
}
|
||||
.ui-datepicker-multi-4 .ui-datepicker-group {
|
||||
width: 25%;
|
||||
}
|
||||
.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,
|
||||
.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header {
|
||||
border-left-width: 0;
|
||||
}
|
||||
.ui-datepicker-multi .ui-datepicker-buttonpane {
|
||||
clear: left;
|
||||
}
|
||||
.ui-datepicker-row-break {
|
||||
clear: both;
|
||||
width: 100%;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
/* RTL support */
|
||||
.ui-datepicker-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-prev {
|
||||
right: 2px;
|
||||
left: auto;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-next {
|
||||
left: 2px;
|
||||
right: auto;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-prev:hover {
|
||||
right: 1px;
|
||||
left: auto;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-next:hover {
|
||||
left: 1px;
|
||||
right: auto;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-buttonpane {
|
||||
clear: right;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-buttonpane button {
|
||||
float: left;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,
|
||||
.ui-datepicker-rtl .ui-datepicker-group {
|
||||
float: right;
|
||||
}
|
||||
.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,
|
||||
.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header {
|
||||
border-right-width: 0;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
70
awx/ui/static/lib/jquery-ui/themes/base/dialog.css
Normal file
@ -0,0 +1,70 @@
|
||||
/*!
|
||||
* jQuery UI Dialog 1.11.3
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*
|
||||
* http://api.jqueryui.com/dialog/#theming
|
||||
*/
|
||||
.ui-dialog {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: .2em;
|
||||
outline: 0;
|
||||
}
|
||||
.ui-dialog .ui-dialog-titlebar {
|
||||
padding: .4em 1em;
|
||||
position: relative;
|
||||
}
|
||||
.ui-dialog .ui-dialog-title {
|
||||
float: left;
|
||||
margin: .1em 0;
|
||||
white-space: nowrap;
|
||||
width: 90%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ui-dialog .ui-dialog-titlebar-close {
|
||||
position: absolute;
|
||||
right: .3em;
|
||||
top: 50%;
|
||||
width: 20px;
|
||||
margin: -10px 0 0 0;
|
||||
padding: 1px;
|
||||
height: 20px;
|
||||
}
|
||||
.ui-dialog .ui-dialog-content {
|
||||
position: relative;
|
||||
border: 0;
|
||||
padding: .5em 1em;
|
||||
background: none;
|
||||
overflow: auto;
|
||||
}
|
||||
.ui-dialog .ui-dialog-buttonpane {
|
||||
text-align: left;
|
||||
border-width: 1px 0 0 0;
|
||||
background-image: none;
|
||||
margin-top: .5em;
|
||||
padding: .3em 1em .5em .4em;
|
||||
}
|
||||
.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
|
||||
float: right;
|
||||
}
|
||||
.ui-dialog .ui-dialog-buttonpane button {
|
||||
margin: .5em .4em .5em 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ui-dialog .ui-resizable-se {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
background-position: 16px 16px;
|
||||
}
|
||||
.ui-draggable .ui-dialog-titlebar {
|
||||
cursor: move;
|
||||
}
|
||||
12
awx/ui/static/lib/jquery-ui/themes/base/draggable.css
Normal file
@ -0,0 +1,12 @@
|
||||
/*!
|
||||
* jQuery UI Draggable 1.11.3
|
||||
* http://jqueryui.com
|
||||
*
|
||||
* Copyright jQuery Foundation and other contributors
|
||||
* Released under the MIT license.
|
||||
* http://jquery.org/license
|
||||
*/
|
||||
.ui-draggable-handle {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
After Width: | Height: | Size: 180 B |
|
After Width: | Height: | Size: 178 B |
|
After Width: | Height: | Size: 120 B |
|
After Width: | Height: | Size: 105 B |
|
After Width: | Height: | Size: 111 B |
|
After Width: | Height: | Size: 110 B |
|
After Width: | Height: | Size: 119 B |
|
After Width: | Height: | Size: 101 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |