Merge branch 'master' into licenses-unstable

This commit is contained in:
Luke Sneeringer
2015-02-18 13:21:45 -06:00
27 changed files with 617 additions and 294 deletions

View File

@@ -17,6 +17,7 @@ recursive-exclude awx/main/tests *
recursive-exclude awx/ui/static/lib/ansible * recursive-exclude awx/ui/static/lib/ansible *
recursive-exclude awx/settings local_settings.py* recursive-exclude awx/settings local_settings.py*
include awx/ui/static/dist/tower.concat.js include awx/ui/static/dist/tower.concat.js
include awx/ui/static/dist/tower.concat.map
include awx/ui/static/dist/tower.concat.js.gz include awx/ui/static/dist/tower.concat.js.gz
include awx/ui/static/js/config.js include awx/ui/static/js/config.js
include tools/scripts/request_tower_configuration.sh include tools/scripts/request_tower_configuration.sh

View File

@@ -202,8 +202,6 @@ server_noattach:
tmux select-pane -U tmux select-pane -U
tmux split-window -v 'exec make receiver' tmux split-window -v 'exec make receiver'
tmux split-window -h 'exec make taskmanager' tmux split-window -h 'exec make taskmanager'
tmux select-pane -U
tmux split-window -h 'exec make sync_ui'
server: server_noattach server: server_noattach
tmux -2 attach-session -t tower tmux -2 attach-session -t tower

View File

@@ -44,7 +44,6 @@ import qsstats
from awx.main.task_engine import TaskSerializer, TASK_FILE from awx.main.task_engine import TaskSerializer, TASK_FILE
from awx.main.access import get_user_queryset from awx.main.access import get_user_queryset
from awx.main.ha import is_ha_environment from awx.main.ha import is_ha_environment
from awx.main.redact import UriCleaner
from awx.api.authentication import JobTaskAuthentication from awx.api.authentication import JobTaskAuthentication
from awx.api.utils.decorators import paginated from awx.api.utils.decorators import paginated
from awx.api.generics import get_view_name from awx.api.generics import get_view_name
@@ -2214,7 +2213,6 @@ class UnifiedJobStdout(RetrieveAPIView):
conv = Ansi2HTMLConverter(scheme=scheme, dark_bg=dark_bg, conv = Ansi2HTMLConverter(scheme=scheme, dark_bg=dark_bg,
title=get_view_name(self.__class__)) title=get_view_name(self.__class__))
content, start, end, absolute_end = unified_job.result_stdout_raw_limited(start_line, end_line) content, start, end, absolute_end = unified_job.result_stdout_raw_limited(start_line, end_line)
content = UriCleaner.remove_sensitive(content)
if content_only: if content_only:
headers = conv.produce_headers() headers = conv.produce_headers()
body = conv.convert(content, full=False) # Escapes any HTML that may be in content. body = conv.convert(content, full=False) # Escapes any HTML that may be in content.
@@ -2231,7 +2229,7 @@ class UnifiedJobStdout(RetrieveAPIView):
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body}) return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body})
return Response(data) return Response(data)
elif request.accepted_renderer.format == 'ansi': elif request.accepted_renderer.format == 'ansi':
return Response(UriCleaner.remove_sensitive(unified_job.result_stdout_raw)) return Response(unified_job.result_stdout_raw)
else: else:
return super(UnifiedJobStdout, self).retrieve(request, *args, **kwargs) return super(UnifiedJobStdout, self).retrieve(request, *args, **kwargs)

View File

@@ -15,7 +15,6 @@ from django.conf import settings
from django.core.management.base import NoArgsCommand from django.core.management.base import NoArgsCommand
from django.db import transaction, DatabaseError from django.db import transaction, DatabaseError
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from django.utils.timezone import now
from django.utils.tzinfo import FixedOffset from django.utils.tzinfo import FixedOffset
from django.db import connection from django.db import connection
@@ -28,7 +27,6 @@ logger = logging.getLogger('awx.main.commands.run_callback_receiver')
MAX_REQUESTS = 10000 MAX_REQUESTS = 10000
WORKERS = 4 WORKERS = 4
class CallbackReceiver(object): class CallbackReceiver(object):
def __init__(self): def __init__(self):
self.parent_mappings = {} self.parent_mappings = {}

View File

@@ -11,7 +11,6 @@ from threading import Thread
# Django # Django
from django.conf import settings from django.conf import settings
from django.core.management.base import NoArgsCommand from django.core.management.base import NoArgsCommand
from django.utils.timezone import now
# AWX # AWX
import awx import awx
@@ -49,23 +48,21 @@ class TowerBaseNamespace(BaseNamespace):
return set(['recv_connect']) return set(['recv_connect'])
def valid_user(self): def valid_user(self):
if 'HTTP_COOKIE' not in self.environ: if 'QUERY_STRING' not in self.environ:
return False return False
else: else:
try: try:
all_keys = [e.strip() for e in self.environ['HTTP_COOKIE'].split(";")] k, v = self.environ['QUERY_STRING'].split("=")
for each_key in all_keys: if k == "Token":
k, v = each_key.split("=") token_actual = urllib.unquote_plus(v).decode().replace("\"","")
if k == "token": auth_token = AuthToken.objects.filter(key=token_actual)
token_actual = urllib.unquote_plus(v).decode().replace("\"","") if not auth_token.exists():
auth_token = AuthToken.objects.filter(key=token_actual) return False
if not auth_token.exists(): auth_token = auth_token[0]
return False if not auth_token.expired:
auth_token = auth_token[0] return auth_token.user
if not auth_token.expired: else:
return auth_token.user return False
else:
return False
except Exception, e: except Exception, e:
logger.error("Exception validating user: " + str(e)) logger.error("Exception validating user: " + str(e))
return False return False

View File

@@ -11,7 +11,6 @@ import time
# Django # Django
from django.conf import settings from django.conf import settings
from django.core.management.base import NoArgsCommand from django.core.management.base import NoArgsCommand
from django.utils.timezone import now
# AWX # AWX
from awx.main.models import * # noqa from awx.main.models import * # noqa

View File

@@ -5,6 +5,7 @@
import hmac import hmac
import json import json
import logging import logging
import re
# Django # Django
from django.conf import settings from django.conf import settings
@@ -23,6 +24,7 @@ from awx.main.models.base import * # noqa
from awx.main.models.unified_jobs 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 decrypt_field, ignore_inventory_computed_fields
from awx.main.utils import emit_websocket_notification from awx.main.utils import emit_websocket_notification
from awx.main.redact import PlainTextCleaner
logger = logging.getLogger('awx.main.models.jobs') logger = logging.getLogger('awx.main.models.jobs')
@@ -220,7 +222,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions):
if survey_element['variable'] not in data and \ if survey_element['variable'] not in data and \
survey_element['required']: survey_element['required']:
errors.append("'%s' value missing" % survey_element['variable']) 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 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']: 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)" % errors.append("'%s' value %s is too small (must be at least %s)" %
@@ -452,6 +454,31 @@ class Job(UnifiedJob, JobOptions):
evars.update(extra_vars) evars.update(extra_vars)
self.update_fields(extra_vars=json.dumps(evars)) 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:
vars = []
# Get variables that are type password
for survey_element in jt.survey_spec['spec']:
if survey_element['type'] == 'password':
vars.append(survey_element['variable'])
# Use password vars to find in extra_vars
for key in vars:
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): def copy(self):
presets = {} presets = {}
for kw in self.job_template._get_unified_job_field_names(): for kw in self.job_template._get_unified_job_field_names():

View File

@@ -625,16 +625,27 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
else: else:
return StringIO("stdout capture is missing") 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 @property
def result_stdout_raw(self): def result_stdout_raw(self):
return self.result_stdout_raw_handle().read() return self._result_stdout_raw()
@property @property
def result_stdout(self): def result_stdout(self):
ansi_escape = re.compile(r'\x1b[^m]*m') return self._result_stdout_raw(escape_ascii=True)
return ansi_escape.sub('', UriCleaner.remove_sensitive(self.result_stdout_raw))
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"" return_buffer = u""
if end_line is not None: if end_line is not None:
end_line = int(end_line) end_line = int(end_line)
@@ -651,12 +662,19 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
end_actual = min(int(end_line), len(stdout_lines)) end_actual = min(int(end_line), len(stdout_lines))
else: else:
end_actual = len(stdout_lines) 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 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): def result_stdout_limited(self, start_line=0, end_line=None):
ansi_escape = re.compile(r'\x1b[^m]*m') return self._result_stdout_raw_limited(start_line, end_line, escape_ascii=True)
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
@property @property
def celery_task(self): def celery_task(self):
@@ -729,9 +747,6 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
def signal_start(self, **kwargs): def signal_start(self, **kwargs):
"""Notify the task runner system to begin work on this task.""" """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 # Sanity check: Are we able to start the job? If not, do not attempt
# to do so. # to do so.
@@ -747,6 +762,10 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique
if 'extra_vars' in kwargs: if 'extra_vars' in kwargs:
self.handle_extra_data(kwargs['extra_vars']) 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. # Save the pending status, and inform the SocketIO listener.
self.update_fields(start_args=json.dumps(kwargs), status='pending') self.update_fields(start_args=json.dumps(kwargs), status='pending')
self.socketio_emit_status("pending") self.socketio_emit_status("pending")

View File

@@ -1,8 +1,10 @@
import re import re
import urlparse import urlparse
REPLACE_STR = '$encrypted$'
class UriCleaner(object): class UriCleaner(object):
REPLACE_STR = '$encrypted$' REPLACE_STR = REPLACE_STR
# https://regex101.com/r/sV2dO2/2 # 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) 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 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

@@ -14,3 +14,4 @@ from awx.main.tests.activity_stream import * # noqa
from awx.main.tests.schedules import * # noqa from awx.main.tests.schedules import * # noqa
from awx.main.tests.redact import * # noqa from awx.main.tests.redact import * # noqa
from awx.main.tests.views import * # noqa from awx.main.tests.views import * # noqa
from awx.main.tests.jobs import *

View File

@@ -13,15 +13,17 @@ import tempfile
import time import time
from multiprocessing import Process from multiprocessing import Process
from subprocess import Popen from subprocess import Popen
import re
# PyYAML # PyYAML
import yaml import yaml
# Django # Django
import django.test
from django.conf import settings, UserSettingsHolder from django.conf import settings, UserSettingsHolder
from django.contrib.auth.models import User from django.contrib.auth.models import User
import django.test
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings
# AWX # AWX
from awx.main.models import * # noqa from awx.main.models import * # noqa
@@ -211,17 +213,20 @@ class BaseTestMixin(QueueTestMixin):
def make_organizations(self, created_by, count=1): def make_organizations(self, created_by, count=1):
results = [] results = []
for x in range(0, count): for x in range(0, count):
self.object_ctr = self.object_ctr + 1 results.append(self.make_organization(created_by=created_by, count=x))
results.append(Organization.objects.create(
name="org%s-%s" % (x, self.object_ctr), description="org%s" % x, created_by=created_by
))
return results return results
def make_organization(self, created_by): def make_organization(self, created_by, count=1):
return self.make_organizations(created_by, 1)[0] 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): playbook_content='', role_playbooks=None, unicode_prefix=True):
if not name:
name = self.unique_name('Project')
if not os.path.exists(settings.PROJECTS_ROOT): if not os.path.exists(settings.PROJECTS_ROOT):
os.makedirs(settings.PROJECTS_ROOT) os.makedirs(settings.PROJECTS_ROOT)
# Create temp project directory. # 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) 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) created_by = self.decide_created_by(created_by)
if not inventory: if not inventory:
inventory = self.make_inventory(organization=organization, created_by=created_by) inventory = self.make_inventory(organization=organization, created_by=created_by)
@@ -300,24 +305,47 @@ class BaseTestMixin(QueueTestMixin):
if project not in organization.projects.all(): if project not in organization.projects.all():
organization.projects.add(project) organization.projects.add(project)
return JobTemplate.objects.create( opts = {
name=name or self.unique_name('JobTemplate'), 'name' : name or self.unique_name('JobTemplate'),
job_type='check', 'job_type': 'check',
inventory=inventory, 'inventory': inventory,
project=project, 'project': project,
playbook=project.playbooks[0], 'host_config_key': settings.SYSTEM_UUID,
host_config_key=settings.SYSTEM_UUID, 'created_by': created_by,
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) created_by = self.decide_created_by(created_by)
if not job_template: if not job_template:
job_template = self.make_job_template(created_by=created_by) job_template = self.make_job_template(created_by=created_by)
job = job_template.create_job(created_by=created_by) opts = {
job.status = inital_state 'created_by': created_by,
return job '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): def setup_instances(self):
instance = Instance(uuid=settings.SYSTEM_UUID, primary=True, hostname='127.0.0.1') instance = Instance(uuid=settings.SYSTEM_UUID, primary=True, hostname='127.0.0.1')
@@ -419,6 +447,10 @@ class BaseTestMixin(QueueTestMixin):
obj = json.loads(response.content) obj = json.loads(response.content)
elif response['Content-Type'].startswith('application/yaml'): elif response['Content-Type'].startswith('application/yaml'):
obj = yaml.safe_load(response.content) 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: else:
self.fail('Unsupport response content type %s' % response['Content-Type']) self.fail('Unsupport response content type %s' % response['Content-Type'])
else: else:
@@ -556,12 +588,58 @@ class BaseTestMixin(QueueTestMixin):
msg += 'fields %s not returned ' % ', '.join(not_returned) msg += 'fields %s not returned ' % ', '.join(not_returned)
self.assertTrue(set(obj.keys()) <= set(fields), msg) self.assertTrue(set(obj.keys()) <= set(fields), msg)
def check_not_found(self, string, substr): def check_not_found(self, string, substr, description=None, word_boundary=False):
self.assertEqual(string.find(substr), -1, "'%s' found in:\n%s" % (substr, string)) 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): def start_taskmanager(self, command_port):
self.start_redis() self.start_redis()
@@ -589,6 +667,17 @@ class BaseLiveServerTest(BaseTestMixin, django.test.LiveServerTestCase):
''' '''
Base class for tests requiring a live test server. 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. # Helps with test cases.
# Save all components of a uri (i.e. scheme, username, password, etc.) so that # 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.group.hosts.add(self.host)
self.project = None self.project = None
self.credential = None self.credential = None
settings.INTERNAL_API_URL = self.live_server_url
self.start_queue() self.start_queue()
def tearDown(self): def tearDown(self):
@@ -320,18 +319,7 @@ class CleanupJobsTest(BaseCommandMixin, BaseLiveServerTest):
shutil.rmtree(self.test_project_path, True) shutil.rmtree(self.test_project_path, True)
def create_test_credential(self, **kwargs): def create_test_credential(self, **kwargs):
opts = { self.credential = self.make_credential(kwargs)
'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)
return self.credential return self.credential
def create_test_project(self, playbook_content): def create_test_project(self, playbook_content):

View File

@@ -0,0 +1,3 @@
from awx.main.tests.jobs.jobs import *
from awx.main.tests.jobs.survey_password import *

View File

@@ -27,7 +27,7 @@ from awx.main.models import * # noqa
from awx.main.tests.base import BaseTestMixin from awx.main.tests.base import BaseTestMixin
__all__ = ['JobTemplateTest', 'JobTest', 'JobStartCancelTest', __all__ = ['JobTemplateTest', 'JobTest', 'JobStartCancelTest',
'JobTemplateCallbackTest', 'JobTransactionTest'] 'JobTemplateCallbackTest', 'JobTransactionTest', 'JobTemplateSurveyTest']
TEST_PLAYBOOK = '''- hosts: all TEST_PLAYBOOK = '''- hosts: all
gather_facts: false gather_facts: false
@@ -933,112 +933,6 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase):
# FIXME: Check other credentials and optional fields. # 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): def test_launch_job_template(self):
url = reverse('api:job_template_list') url = reverse('api:job_template_list')
data = dict( data = dict(
@@ -1945,3 +1839,115 @@ class JobTransactionTest(BaseJobTestMixin, django.test.LiveServerTestCase):
self.assertEqual(job.status, 'successful', job.result_stdout) self.assertEqual(job.status, 'successful', job.result_stdout)
self.assertFalse(errors) 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,203 @@
# Python
import json
# Django
import django.test
from django.core.urlresolvers import reverse
# AWX
from awx.main.models import * # noqa
from awx.main.tests.base import BaseTest
__all__ = ['SurveyPasswordTest']
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):
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'])
def _get_url_job_stdout(self, job):
job_stdout_url = reverse('api:job_stdout', args=(job.pk,))
return self.get(job_stdout_url, expect=200, auth=self.get_super_credentials(), accept='application/json')
class SurveyPasswordTest(SurveyPasswordBaseTest):
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
self.tests[test_name].append(test)
def setUp(self):
super(SurveyPasswordTest, self).setUp()
self.tests = {}
self.setup_test('simple')
self.setup_test('complex')
# should redact single variable survey
def test_survey_password_redact_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_survey_password_redact_complex_survey(self):
for test in self.tests['complex']:
response = self._get_url_job_stdout(test['job'])
self.check_passwords_redacted(test, response)

View File

@@ -87,9 +87,9 @@ class UriCleanTests(BaseTest):
for uri in TEST_URIS: for uri in TEST_URIS:
redacted_str = UriCleaner.remove_sensitive(str(uri)) redacted_str = UriCleaner.remove_sensitive(str(uri))
if uri.username: if uri.username:
self.check_not_found(redacted_str, uri.username) self.check_not_found(redacted_str, uri.username, uri.description)
if uri.password: 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 # should replace secret data with safe string, UriCleaner.REPLACE_STR
def test_uri_scm_simple_replaced(self): def test_uri_scm_simple_replaced(self):
@@ -107,9 +107,9 @@ class UriCleanTests(BaseTest):
redacted_str = UriCleaner.remove_sensitive(str(uri)) redacted_str = UriCleaner.remove_sensitive(str(uri))
if uri.username: if uri.username:
self.check_not_found(redacted_str, uri.username) self.check_not_found(redacted_str, uri.username, uri.description)
if uri.password: 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 # should replace multiple secret data with safe string
def test_uri_scm_multiple_replaced(self): def test_uri_scm_multiple_replaced(self):
@@ -131,8 +131,8 @@ class UriCleanTests(BaseTest):
for test_data in TEST_CLEARTEXT: for test_data in TEST_CLEARTEXT:
uri = test_data['uri'] uri = test_data['uri']
redacted_str = UriCleaner.remove_sensitive(test_data['text']) redacted_str = UriCleaner.remove_sensitive(test_data['text'])
self.check_not_found(redacted_str, uri.username) self.check_not_found(redacted_str, uri.username, uri.description)
self.check_not_found(redacted_str, uri.password) self.check_not_found(redacted_str, uri.password, uri.description)
# Ensure the host didn't get redacted # 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

@@ -21,7 +21,7 @@ from crum import impersonate
# AWX # AWX
from awx.main.models import * # noqa from awx.main.models import * # noqa
from awx.main.tests.base import BaseLiveServerTest from awx.main.tests.base import BaseJobExecutionTest
TEST_PLAYBOOK = u''' TEST_PLAYBOOK = u'''
- name: test success - name: test success
@@ -341,15 +341,7 @@ L5Hj+B02+FAiz8zVGumbVykvPtzgTb0E+0rJKNO0/EgGqWsk/oC0
TEST_SSH_KEY_DATA_UNLOCK = 'unlockme' TEST_SSH_KEY_DATA_UNLOCK = 'unlockme'
@override_settings(CELERY_ALWAYS_EAGER=True, class RunJobTest(BaseJobExecutionTest):
CELERY_EAGER_PROPAGATES_EXCEPTIONS=True)
class BaseCeleryTest(BaseLiveServerTest):
'''
Base class for celery task tests.
'''
@override_settings(ANSIBLE_TRANSPORT='local')
class RunJobTest(BaseCeleryTest):
''' '''
Test cases for RunJob celery task. Test cases for RunJob celery task.
''' '''
@@ -371,31 +363,14 @@ class RunJobTest(BaseCeleryTest):
self.credential = None self.credential = None
self.cloud_credential = None self.cloud_credential = None
settings.INTERNAL_API_URL = self.live_server_url settings.INTERNAL_API_URL = self.live_server_url
self.start_queue()
def tearDown(self): def tearDown(self):
super(RunJobTest, self).tearDown() super(RunJobTest, self).tearDown()
if self.test_project_path: if self.test_project_path:
shutil.rmtree(self.test_project_path, True) shutil.rmtree(self.test_project_path, True)
self.terminate_queue()
def create_test_credential(self, **kwargs): def create_test_credential(self, **kwargs):
opts = { self.credential = self.make_credential(**kwargs)
'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)
return self.credential return self.credential
def create_test_cloud_credential(self, **kwargs): def create_test_cloud_credential(self, **kwargs):
@@ -429,7 +404,7 @@ class RunJobTest(BaseCeleryTest):
except (AttributeError, IndexError): except (AttributeError, IndexError):
pass pass
opts.update(kwargs) opts.update(kwargs)
self.job_template = JobTemplate.objects.create(**opts) self.job_template = self.make_job_template(**opts)
return self.job_template return self.job_template
def create_test_job(self, **kwargs): def create_test_job(self, **kwargs):
@@ -453,32 +428,6 @@ class RunJobTest(BaseCeleryTest):
self.job = Job.objects.create(**opts) self.job = Job.objects.create(**opts)
return self.job 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, def check_job_events(self, job, runner_status='ok', plays=1, tasks=1,
async=False, async_timeout=False, async_nowait=False, async=False, async_timeout=False, async_nowait=False,
check_ignore_errors=False, async_tasks=0, 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 BaseLiveServerTest, QueueStartStopTestMixin
from awx.main.tests.base import URI from awx.main.tests.base import URI
__all__ = ['UnifiedJobStdoutTests'] __all__ = ['UnifiedJobStdoutRedactedTests']
TEST_STDOUTS = [] TEST_STDOUTS = []
uri = URI(scheme="https", username="Dhh3U47nmC26xk9PKscV", password="PXPfWW8YzYrgS@E5NbQ2H@", host="github.ginger.com/theirrepo.git/info/refs") uri = URI(scheme="https", username="Dhh3U47nmC26xk9PKscV", password="PXPfWW8YzYrgS@E5NbQ2H@", host="github.ginger.com/theirrepo.git/info/refs")
TEST_STDOUTS.append({ TEST_STDOUTS.append({
'description': 'uri in a plain text document',
'uri' : uri, 'uri' : uri,
'text' : 'hello world %s goodbye world' % 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") uri = URI(scheme="https", username="applepie@@@", password="thatyouknow@@@@", host="github.ginger.com/theirrepo.git/info/refs")
TEST_STDOUTS.append({ TEST_STDOUTS.append({
'description': 'uri appears twice in a multiline plain text document',
'uri' : uri, 'uri' : uri,
'text' : 'hello world %s \n\nyoyo\n\nhello\n%s' % (uri, uri), 'text' : 'hello world %s \n\nyoyo\n\nhello\n%s' % (uri, uri),
'host_occurrences' : 2 'occurrences' : 2
}) })
class UnifiedJobStdoutRedactedTests(BaseLiveServerTest, QueueStartStopTestMixin):
class UnifiedJobStdoutTests(BaseLiveServerTest, QueueStartStopTestMixin):
def setUp(self): def setUp(self):
super(UnifiedJobStdoutTests, self).setUp() super(UnifiedJobStdoutRedactedTests, self).setUp()
self.setup_instances() self.setup_instances()
self.setup_users() self.setup_users()
self.test_cases = [] self.test_cases = []
@@ -40,15 +41,38 @@ class UnifiedJobStdoutTests(BaseLiveServerTest, QueueStartStopTestMixin):
# This is more of a functional test than a unit test. # This is more of a functional test than a unit test.
# should filter out username and password # 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: for test_data in self.test_cases:
uri = test_data['uri'] response = self._get_url_job_stdout(test_data['job'], format=format)
job_stdout_url = reverse('api:job_stdout', args=(test_data['job'].pk,)) 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']) def test_redaction_enabled_ansi(self):
self.check_not_found(response['content'], uri.username) self._test_redaction_enabled('ansi')
self.check_not_found(response['content'], uri.password)
# Ensure the host didn't get redacted def test_redaction_enabled_html(self):
self.check_found(response['content'], uri.host, count=test_data['host_occurrences']) self._test_redaction_enabled('html')
def test_redaction_enabled_txt(self):
self._test_redaction_enabled('txt')

View File

@@ -37,9 +37,10 @@ except ImportError:
if 'django_jenkins' in INSTALLED_APPS: if 'django_jenkins' in INSTALLED_APPS:
JENKINS_TASKS = ( JENKINS_TASKS = (
'django_jenkins.tasks.run_pylint', 'django_jenkins.tasks.run_pylint',
'django_jenkins.tasks.run_pep8',
'django_jenkins.tasks.run_pyflakes',
'django_jenkins.tasks.run_flake8', '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_jshint',
'django_jenkins.tasks.run_csslint', 'django_jenkins.tasks.run_csslint',
) )

View File

@@ -200,7 +200,7 @@ export function JobDetailController ($location, $rootScope, $scope, $compile, $r
scope.removeLoadHostSummaries(); scope.removeLoadHostSummaries();
} }
scope.removeHostSummaries = scope.$on('LoadHostSummaries', function() { scope.removeHostSummaries = scope.$on('LoadHostSummaries', function() {
if(scope.job.related){ if(scope.job){
var url = scope.job.related.job_host_summaries + '?'; var url = scope.job.related.job_host_summaries + '?';
url += '&page_size=' + scope.hostSummariesMaxRows + '&order=host_name'; url += '&page_size=' + scope.hostSummariesMaxRows + '&order=host_name';
@@ -247,9 +247,11 @@ export function JobDetailController ($location, $rootScope, $scope, $compile, $r
if (scope.activeTask) { if (scope.activeTask) {
var play = scope.jobData.plays[scope.activePlay], var play = scope.jobData.plays[scope.activePlay],
task = play.tasks[scope.activeTask], task, // = play.tasks[scope.activeTask],
url; url;
if(play){
task = play.tasks[scope.activeTask];
}
if (play && task) { if (play && task) {
url = scope.job.related.job_events + '?parent=' + task.id + '&'; url = scope.job.related.job_events + '?parent=' + task.id + '&';
url += 'event__startswith=runner&page_size=' + scope.hostResultsMaxRows + '&order=host_name,counter'; url += 'event__startswith=runner&page_size=' + scope.hostResultsMaxRows + '&order=host_name,counter';

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>" '+ '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>'+ '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><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-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 &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-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 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 class="error api-error ng-binding" id="survey_question-variable-api-error" ng-bind="variable_api_error"></div>'+
'</div>', '</div>',
@@ -106,13 +106,13 @@ export default
control:'<div class="row">'+ control:'<div class="row">'+
'<div class="col-xs-6">'+ '<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 />'+ '<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 number. Please enter a 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.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 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>'+
'<div class="col-xs-6">'+ '<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 >'+ '<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 number. Please enter a number.</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 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>'+
'</div>', '</div>',
@@ -127,13 +127,13 @@ export default
control:'<div class="row">'+ control:'<div class="row">'+
'<div class="col-xs-6">'+ '<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 />'+ '<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 number. Please enter a 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.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 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>'+
'<div class="col-xs-6">'+ '<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 >'+ '<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 number. Please enter a 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 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>'+
'</div>', '</div>',
@@ -148,13 +148,13 @@ export default
control:'<div class="row">'+ control:'<div class="row">'+
'<div class="col-xs-6">'+ '<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 />'+ '<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 number. Please enter a 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.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 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>'+
'<div class="col-xs-6">'+ '<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 >'+ '<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 number. Please enter a 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 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>'+
'</div>', '</div>',
@@ -211,7 +211,7 @@ export default
'<label for="default"><span class="label-text">Default Answer</span></label>'+ '<label for="default"><span class="label-text">Default Answer</span></label>'+
'<div>'+ '<div>'+
'<input type="text" ng-model="default" name="default" id="default" class="form-control">'+ '<input type="text" ng-model="default" name="default" id="default" class="form-control">'+
'<div class="error ng-hide" id=survey_question-default-duplicate-error" ng-show="invalidChoice">Please enter an answer for the choices listed.</div>' + '<div class="error ng-hide" id=survey_question-default-duplicate-error" ng-show="invalidChoice">Please enter an answer from the choices listed.</div>' +
'<div class="error ng-hide" id=survey_question-default-duplicate-error" ng-show="minTextError">The answer is shorter than the minimium length. Please make the answer longer. </div>' + '<div class="error ng-hide" id=survey_question-default-duplicate-error" ng-show="minTextError">The answer is shorter than the minimium length. Please make the answer longer. </div>' +
'<div class="error ng-hide" id=survey_question-default-duplicate-error" ng-show="maxTextError">The answer is longer than the maximum length. Please make the answer shorter. </div>' + '<div class="error ng-hide" id=survey_question-default-duplicate-error" ng-show="maxTextError">The answer is longer than the maximum length. Please make the answer shorter. </div>' +
'<div class="error api-error ng-binding" id="survey_question-default-api-error" ng-bind="default_api_error"></div>'+ '<div class="error api-error ng-binding" id="survey_question-default-api-error" ng-bind="default_api_error"></div>'+
@@ -227,7 +227,7 @@ export default
'<label for="default_multiselect"><span class="label-text">Default Answer</span></label>'+ '<label for="default_multiselect"><span class="label-text">Default Answer</span></label>'+
'<div>'+ '<div>'+
'<textarea rows="3" ng-model="default_multiselect" name="default_multiselect" class="form-control ng-pristine ng-valid" id="default_multiselect" aw-watch=""></textarea>'+ '<textarea rows="3" ng-model="default_multiselect" name="default_multiselect" class="form-control ng-pristine ng-valid" id="default_multiselect" aw-watch=""></textarea>'+
'<div class="error ng-hide" id=survey_question-default_multiselect-duplicate-error" ng-show="invalidChoice">Please enter an answer/answers for the choices listed.</div>' + '<div class="error ng-hide" id=survey_question-default_multiselect-duplicate-error" ng-show="invalidChoice">Please enter an answer/answers from the choices listed.</div>' +
'<div class="error api-error ng-binding" id="survey_question-default_multiselect-api-error" ng-bind="default_multiselect_api_error"></div>'+ '<div class="error api-error ng-binding" id="survey_question-default_multiselect-api-error" ng-bind="default_multiselect_api_error"></div>'+
'</div>'+ '</div>'+
'</div>', '</div>',

View File

@@ -81,7 +81,7 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential
job_launch_data.extra_vars[fld] = scope[fld]; job_launch_data.extra_vars[fld] = scope[fld];
} }
// for optional text and text-areas, submit a blank string if min length is 0 // 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] = ""; job_launch_data.extra_vars[fld] = "";
} }
} }

View File

@@ -577,11 +577,14 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper',
scope.int_max = null; scope.int_max = null;
scope.float_min = null; scope.float_min = null;
scope.float_max = null; scope.float_max = null;
scope.duplicate = false;
scope.invalidChoice = false;
scope.minTextError = false;
scope.maxTextError = false;
}; };
scope.addNewQuestion = function(){ scope.addNewQuestion = function(){
// $('#add_question_btn').on("click" , function(){ // $('#add_question_btn').on("click" , function(){
scope.duplicate = false;
scope.addQuestion(); scope.addQuestion();
$('#survey_question_question_name').focus(); $('#survey_question_question_name').focus();
$('#add_question_btn').attr('disabled', 'disabled'); $('#add_question_btn').attr('disabled', 'disabled');

View File

@@ -45,7 +45,7 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities'])
PortalJobsList.fields.type.searchOptions = scope.type_choices; PortalJobsList.fields.type.searchOptions = scope.type_choices;
} }
user = scope.$parent.current_user.id; user = scope.$parent.current_user.id;
url = (filter === "Team" ) ? GetBasePath('jobs') : GetBasePath('jobs')+'?created_by='+user ; url = (filter === "All Jobs" ) ? GetBasePath('jobs') : GetBasePath('jobs')+'?created_by='+user ;
LoadJobsScope({ LoadJobsScope({
parent_scope: scope, parent_scope: scope,
scope: jobs_scope, scope: jobs_scope,
@@ -77,7 +77,7 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities'])
$("#active-jobs").empty(); $("#active-jobs").empty();
$("#active-jobs-search-container").empty(); $("#active-jobs-search-container").empty();
user = scope.$parent.current_user.id; user = scope.$parent.current_user.id;
url = (filter === "Team" ) ? GetBasePath('jobs') : GetBasePath('jobs')+'?created_by='+user ; url = (filter === "All Jobs" ) ? GetBasePath('jobs') : GetBasePath('jobs')+'?created_by='+user ;
LoadJobsScope({ LoadJobsScope({
parent_scope: scope, parent_scope: scope,
scope: jobs_scope, scope: jobs_scope,
@@ -96,8 +96,8 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities'])
html += "<div class=\"col-lg-6 col-md-6\" id=\"active-jobs-search-container\"></div>\n"; html += "<div class=\"col-lg-6 col-md-6\" id=\"active-jobs-search-container\"></div>\n";
html += "<div class=\"form-group\">" ; html += "<div class=\"form-group\">" ;
html += "<div class=\"btn-group\" aw-toggle-button data-after-toggle=\"filterPortalJobs\">" ; html += "<div class=\"btn-group\" aw-toggle-button data-after-toggle=\"filterPortalJobs\">" ;
html += " <button class=\"btn btn-xs btn-primary active\">User</button>" ; html += "<button id='portal-toggle-user' class=\"btn btn-xs btn-primary active\">My Jobs</button>" ;
html += "<button class=\"btn btn-xs btn-default\">Team</button>" ; html += "<button id='portal-toggle-all' class=\"btn btn-xs btn-default\">All Jobs</button>" ;
html += "</div>" ; html += "</div>" ;
html += "</div>" ; html += "</div>" ;
html += "</div>\n"; //row html += "</div>\n"; //row

View File

@@ -71,6 +71,7 @@ angular.module('SocketIO', ['AuthService', 'Utilities'])
$log.debug('Socket connecting to: ' + url); $log.debug('Socket connecting to: ' + url);
self.scope.socket_url = url; self.scope.socket_url = url;
self.socket = io.connect(url, { self.socket = io.connect(url, {
query: "Token="+token,
headers: headers:
{ {
'Authorization': 'Token ' + token, // i don't think these are actually inserted into the header--jt 'Authorization': 'Token ' + token, // i don't think these are actually inserted into the header--jt
@@ -78,7 +79,7 @@ angular.module('SocketIO', ['AuthService', 'Utilities'])
}, },
'connect timeout': 3000, 'connect timeout': 3000,
'try multiple transports': false, 'try multiple transports': false,
'max reconneciton attemps': 3, 'max reconnection attempts': 3,
'reconnection limit': 3000 'reconnection limit': 3000
}); });

View File

@@ -70,7 +70,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
require: 'ngModel', require: 'ngModel',
scope: { ngModel: '=ngModel' }, scope: { ngModel: '=ngModel' },
template: '<div class="survey_taker_input" ng-repeat="option in ngModel.options">' + 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)" />' + 'value="{{option.value}}" class="mc" ng-change="update(this.value)" />' +
'<span>'+ '<span>'+
'{{option.value}}'+ '{{option.value}}'+
@@ -150,7 +150,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
var min = (attr.awMin) ? scope.$eval(attr.awMin) : -Infinity; var min = (attr.awMin) ? scope.$eval(attr.awMin) : -Infinity;
if (!Empty(min) && !Empty(viewValue) && Number(viewValue) < min) { if (!Empty(min) && !Empty(viewValue) && Number(viewValue) < min) {
ctrl.$setValidity('awMin', false); ctrl.$setValidity('awMin', false);
return undefined; return viewValue;
} else { } else {
ctrl.$setValidity('awMin', true); ctrl.$setValidity('awMin', true);
return viewValue; return viewValue;
@@ -217,17 +217,17 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
if ( elm.attr('min') && if ( elm.attr('min') &&
( viewValue === '' || viewValue === null || parseInt(viewValue,10) < parseInt(elm.attr('min'),10) ) ) { ( viewValue === '' || viewValue === null || parseInt(viewValue,10) < parseInt(elm.attr('min'),10) ) ) {
ctrl.$setValidity('min', false); ctrl.$setValidity('min', false);
return undefined; return viewValue;
} }
if ( elm.attr('max') && ( parseInt(viewValue,10) > parseInt(elm.attr('max'),10) ) ) { if ( elm.attr('max') && ( parseInt(viewValue,10) > parseInt(elm.attr('max'),10) ) ) {
ctrl.$setValidity('max', false); ctrl.$setValidity('max', false);
return undefined; return viewValue;
} }
return viewValue; return viewValue;
} }
// Invalid, return undefined (no model update) // Invalid, return undefined (no model update)
ctrl.$setValidity('integer', false); ctrl.$setValidity('integer', false);
return undefined; return viewValue;
}); });
} }
}; };
@@ -238,16 +238,25 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job
.directive('awSurveyVariableName', function() { .directive('awSurveyVariableName', function() {
var FLOAT_REGEXP = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/; var FLOAT_REGEXP = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
return { return {
restrict: 'A',
require: 'ngModel', require: 'ngModel',
link: function(scope, elm, attrs, ctrl) { 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) { ctrl.$parsers.unshift(function(viewValue) {
if (FLOAT_REGEXP.test(viewValue) && viewValue.indexOf(' ') === -1) { //check for a spaces if(viewValue.length !== 0){
ctrl.$setValidity('variable', true); 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; return viewValue;
} else { }
ctrl.$setValidity('variable', false); // spaces found, therefore throw error
return undefined;
}
}); });
} }
}; };

View File

@@ -16,7 +16,7 @@
</div> </div>
<div class="right-side col-sm-6 col-xs-12"> <div class="right-side col-sm-6 col-xs-12">
<div id="portal-container-jobs" class="portal-container"> <div id="portal-container-jobs" class="portal-container">
<span class="portal-header">My Jobs</span> <span class="portal-header">Jobs</span>
<div id="portal-jobs" > <div id="portal-jobs" >
</div> </div>
</div> </div>