mirror of
https://github.com/ansible/awx.git
synced 2026-02-14 01:34:45 -03:30
Merge branch 'master' into licenses-unstable
This commit is contained in:
@@ -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
|
||||||
|
|||||||
2
Makefile
2
Makefile
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -15,9 +15,8 @@ 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
|
||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx.main.models import * # noqa
|
from awx.main.models import * # noqa
|
||||||
@@ -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 = {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 *
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
3
awx/main/tests/jobs/__init__.py
Normal file
3
awx/main/tests/jobs/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
from awx.main.tests.jobs.jobs import *
|
||||||
|
from awx.main.tests.jobs.survey_password import *
|
||||||
@@ -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)
|
||||||
203
awx/main/tests/jobs/survey_password.py
Normal file
203
awx/main/tests/jobs/survey_password.py
Normal 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)
|
||||||
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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')
|
||||||
@@ -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',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ export default
|
|||||||
'user_id<br>host_name<br><div class="popover-footer"><span class="key">esc</span> or click to close</div>" '+
|
'user_id<br>host_name<br><div class="popover-footer"><span class="key">esc</span> or click to close</div>" '+
|
||||||
'data-placement="right" data-container="body" data-title="Answer Variable Name" class="help-link" data-original-title="" title="" tabindex="-1"><i class="fa fa-question-circle"></i></a> </label>'+
|
'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 && 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 && 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>',
|
||||||
|
|||||||
@@ -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] = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user