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
  ...
This commit is contained in:
Matthew Jones 2015-03-03 14:45:44 -05:00
commit f6faff5b14
881 changed files with 95714 additions and 16929 deletions

2
.gitignore vendored
View File

@ -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__

View File

@ -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/_*

View File

@ -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

View File

@ -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

View 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 %}

View File

@ -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.

View File

@ -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)

View File

@ -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)

View File

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

View File

@ -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([
('&', '&amp;'),
('<', '&lt;'),
('>', '&gt;'),
])
for pattern, special in specials.items():
ansi = ansi.replace(pattern, special)
state = _State()
inside_span = False
last_end = 0 # the index of the last end of a code we've seen
for match in self.ansi_codes_prog.finditer(ansi):
yield ansi[last_end:match.start()]
last_end = match.end()
params, command = match.groups()
if command not in 'mMA':
continue
# Special cursor-moving code. The only supported one.
if command == 'A':
yield CursorMoveUp
continue
try:
params = list(map(int, params.split(';')))
except ValueError:
params = [ANSI_FULL_RESET]
# Find latest reset marker
last_null_index = None
skip_after_index = -1
for i, v in enumerate(params):
if i <= skip_after_index:
continue
if v == ANSI_FULL_RESET:
last_null_index = i
elif v in (ANSI_FOREGROUND_256, ANSI_BACKGROUND_256):
skip_after_index = i + 2
# Process reset marker, drop everything before
if last_null_index is not None:
params = params[last_null_index + 1:]
if inside_span:
inside_span = False
yield '</span>'
state.reset()
if not params:
continue
# Turn codes into CSS classes
skip_after_index = -1
for i, v in enumerate(params):
if i <= skip_after_index:
continue
if v in (ANSI_FOREGROUND_256, ANSI_BACKGROUND_256):
try:
parameter = params[i + 2]
except IndexError:
continue
skip_after_index = i + 2
else:
parameter = None
state.adjust(v, parameter=parameter)
if inside_span:
yield '</span>'
inside_span = False
css_classes = state.to_css_classes()
if not css_classes:
continue
if self.inline:
style = [self.styles[klass].kw for klass in css_classes if
klass in self.styles]
yield '<span style="%s">' % "; ".join(style)
else:
yield '<span class="%s">' % " ".join(css_classes)
inside_span = True
yield ansi[last_end:]
if inside_span:
yield '</span>'
inside_span = False
def _collapse_cursor(self, parts):
""" Act on any CursorMoveUp commands by deleting preceding tokens """
final_parts = []
for part in parts:
# Throw out empty string tokens ("")
if not part:
continue
# Go back, deleting every token in the last 'line'
if part == CursorMoveUp:
final_parts.pop()
while '\n' not in final_parts[-1]:
final_parts.pop()
continue
# Otherwise, just pass this token forward
final_parts.append(part)
return final_parts
def prepare(self, ansi='', ensure_trailing_newline=False):
""" Load the contents of 'ansi' into this object """
body = self.apply_regex(ansi)
if ensure_trailing_newline and _needs_extra_newline(body):
body += '\n'
self._attrs = {
'dark_bg': self.dark_bg,
'font_size': self.font_size,
'body': body,
}
return self._attrs
def attrs(self):
""" Prepare attributes for the template """
if not self._attrs:
raise Exception("Method .prepare not yet called.")
return self._attrs
def convert(self, ansi, full=True, ensure_trailing_newline=False):
attrs = self.prepare(ansi, ensure_trailing_newline=ensure_trailing_newline)
if not full:
return attrs["body"]
else:
return _template % {
'style' : "\n".join(map(str, get_styles(self.dark_bg, self.scheme))),
'title' : self.title,
'font_size' : self.font_size,
'content' : attrs["body"],
'output_encoding' : self.output_encoding,
}
def produce_headers(self):
return '<style type="text/css">\n%(style)s\n</style>\n' % {
'style' : "\n".join(map(str, get_styles(self.dark_bg, self.scheme)))
}
def main():
"""
$ ls --color=always | ansi2html > directories.html
$ sudo tail /var/log/messages | ccze -A | ansi2html > logs.html
$ task burndown | ansi2html > burndown.html
"""
scheme_names = sorted(six.iterkeys(SCHEME))
version_str = pkg_resources.get_distribution('ansi2html').version
parser = optparse.OptionParser(
usage=main.__doc__,
version="%%prog %s" % version_str)
parser.add_option(
"-p", "--partial", dest="partial",
default=False, action="store_true",
help="Process lines as them come in. No headers are produced.")
parser.add_option(
"-i", "--inline", dest="inline",
default=False, action="store_true",
help="Inline style without headers or template.")
parser.add_option(
"-H", "--headers", dest="headers",
default=False, action="store_true",
help="Just produce the <style> tag.")
parser.add_option(
"-f", '--font-size', dest='font_size', metavar='SIZE',
default="normal",
help="Set the global font size in the output.")
parser.add_option(
"-l", '--light-background', dest='light_background',
default=False, action="store_true",
help="Set output to 'light background' mode.")
parser.add_option(
"-a", '--linkify', dest='linkify',
default=False, action="store_true",
help="Transform URLs into <a> links.")
parser.add_option(
"-u", '--unescape', dest='escaped',
default=True, action="store_false",
help="Do not escape XML tags found in the input.")
parser.add_option(
"-m", '--markup-lines', dest="markup_lines",
default=False, action="store_true",
help="Surround lines with <span id='line-n'>..</span>.")
parser.add_option(
'--input-encoding', dest='input_encoding', metavar='ENCODING',
default='utf-8',
help="Specify input encoding")
parser.add_option(
'--output-encoding', dest='output_encoding', metavar='ENCODING',
default='utf-8',
help="Specify output encoding")
parser.add_option(
'-s', '--scheme', dest='scheme', metavar='SCHEME',
default='ansi2html', choices=scheme_names,
help=("Specify color palette scheme. Default: %%default. Choices: %s"
% scheme_names))
parser.add_option(
'-t', '--title', dest='output_title',
default='',
help="Specify output title")
opts, args = parser.parse_args()
conv = Ansi2HTMLConverter(
inline=opts.inline,
dark_bg=not opts.light_background,
font_size=opts.font_size,
linkify=opts.linkify,
escaped=opts.escaped,
markup_lines=opts.markup_lines,
output_encoding=opts.output_encoding,
scheme=opts.scheme,
title=opts.output_title,
)
def _read(input_bytes):
if six.PY3:
# This is actually already unicode. How to we explicitly decode in
# python3? I don't know the answer yet.
return input_bytes
else:
return input_bytes.decode(opts.input_encoding)
def _print(output_unicode, end='\n'):
if hasattr(sys.stdout, 'buffer'):
output_bytes = (output_unicode + end).encode(opts.output_encoding)
sys.stdout.buffer.write(output_bytes)
elif not six.PY3:
sys.stdout.write((output_unicode + end).encode(opts.output_encoding))
else:
sys.stdout.write(output_unicode + end)
# Produce only the headers and quit
if opts.headers:
_print(conv.produce_headers(), end='')
return
full = not bool(opts.partial or opts.inline)
if six.PY3:
output = conv.convert("".join(sys.stdin.readlines()), full=full, ensure_trailing_newline=True)
_print(output, end='')
else:
output = conv.convert(six.u("").join(
map(_read, sys.stdin.readlines())
), full=full, ensure_trailing_newline=True)
_print(output, end='')

View File

@ -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

View File

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

View 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

View File

@ -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.

View File

@ -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):

View 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

View File

@ -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

View File

@ -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

View File

@ -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():

View File

@ -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")

View File

@ -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)

View File

@ -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):

View File

@ -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'

View File

@ -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

View File

@ -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):

View 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
View 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()

View File

@ -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)

View 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)

View File

@ -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()

View File

@ -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)

View File

@ -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,

View File

@ -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')

View File

@ -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

View File

@ -0,0 +1,4 @@
- hosts: all
tasks:
- scan_packages:

View 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()

View 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()

View File

@ -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.

View File

@ -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',
)

View File

@ -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', [

View File

@ -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';

View File

@ -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");
}

View File

@ -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'
];
];

View File

@ -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',

View File

@ -63,8 +63,8 @@ export default
'user_id<br>host_name<br><div class=&quot;popover-footer&quot;><span class=&quot;key&quot;>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 &amp;&amp; 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 &amp;&amp; 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>',

View File

@ -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";

View File

@ -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",

View File

@ -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+'" '+

View File

@ -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 = "" ;

View File

@ -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'])
}
};
}
]);
]);

View File

@ -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
}
};
}
]);
]);

View File

@ -14,7 +14,7 @@
*/
export default
angular.module('ModalDialog', ['Utilities', 'ParseHelper'])
/**

View File

@ -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'])
}
};
}
]);
]);

View File

@ -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 {

View File

@ -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'])
}
};
}
]);
]);

View File

@ -14,6 +14,7 @@
export default
angular.module('Utilities', ['RestServices', 'Utilities'])
/**

View File

@ -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];
};
}
]);
]);

View File

@ -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() {

View File

@ -11,6 +11,7 @@
export default
angular.module('AWFilters', [])
//
@ -91,4 +92,4 @@ angular.module('AWFilters', [])
}
return input;
};
}]);
}]);

View File

@ -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',

View File

@ -12,7 +12,7 @@
*/
export default
angular.module('GeneratorHelpers', [])
.factory('Attr', function () {

View File

@ -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',

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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
};

File diff suppressed because it is too large Load Diff

View 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"
}

View File

@ -0,0 +1,4 @@
components
composer.lock
vendor
.DS_Store

View 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>

View 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.

View 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.

View File

@ -0,0 +1,12 @@
{
"name": "jquery-ui",
"version": "1.11.3",
"main": [
"jquery-ui.js"
],
"ignore": [
],
"dependencies": {
"jquery": ">=1.6"
}
}

View 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": "*"
}
}

View 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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View 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": []
}

View 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;
}

View 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";

View 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;
}

View 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");

View 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;
}

View 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%;
}

View 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;
}

View 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;
}

View 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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Some files were not shown because too many files have changed in this diff Show More