diff --git a/MANIFEST.in b/MANIFEST.in index dfa5d45237..895910f06e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -17,6 +17,7 @@ recursive-exclude awx/main/tests * recursive-exclude awx/ui/static/lib/ansible * recursive-exclude awx/settings local_settings.py* include awx/ui/static/dist/tower.concat.js +include awx/ui/static/dist/tower.concat.map include awx/ui/static/dist/tower.concat.js.gz include awx/ui/static/js/config.js include tools/scripts/request_tower_configuration.sh diff --git a/Makefile b/Makefile index 12bd816eaf..51b14223ce 100644 --- a/Makefile +++ b/Makefile @@ -202,8 +202,6 @@ server_noattach: tmux select-pane -U tmux split-window -v 'exec make receiver' tmux split-window -h 'exec make taskmanager' - tmux select-pane -U - tmux split-window -h 'exec make sync_ui' server: server_noattach tmux -2 attach-session -t tower diff --git a/awx/api/views.py b/awx/api/views.py index e5a1ac270a..7a212976cd 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -44,7 +44,6 @@ import qsstats from awx.main.task_engine import TaskSerializer, TASK_FILE from awx.main.access import get_user_queryset from awx.main.ha import is_ha_environment -from awx.main.redact import UriCleaner from awx.api.authentication import JobTaskAuthentication from awx.api.utils.decorators import paginated from awx.api.generics import get_view_name @@ -2214,7 +2213,6 @@ class UnifiedJobStdout(RetrieveAPIView): conv = Ansi2HTMLConverter(scheme=scheme, dark_bg=dark_bg, title=get_view_name(self.__class__)) content, start, end, absolute_end = unified_job.result_stdout_raw_limited(start_line, end_line) - content = UriCleaner.remove_sensitive(content) if content_only: headers = conv.produce_headers() body = conv.convert(content, full=False) # Escapes any HTML that may be in content. @@ -2231,7 +2229,7 @@ class UnifiedJobStdout(RetrieveAPIView): return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body}) return Response(data) elif request.accepted_renderer.format == 'ansi': - return Response(UriCleaner.remove_sensitive(unified_job.result_stdout_raw)) + return Response(unified_job.result_stdout_raw) else: return super(UnifiedJobStdout, self).retrieve(request, *args, **kwargs) diff --git a/awx/main/management/commands/run_callback_receiver.py b/awx/main/management/commands/run_callback_receiver.py index e883a1963c..38fa01ad00 100644 --- a/awx/main/management/commands/run_callback_receiver.py +++ b/awx/main/management/commands/run_callback_receiver.py @@ -15,9 +15,8 @@ from django.conf import settings from django.core.management.base import NoArgsCommand from django.db import transaction, DatabaseError from django.utils.dateparse import parse_datetime -from django.utils.timezone import now from django.utils.tzinfo import FixedOffset -from django.db import connection +from django.db import connection # AWX from awx.main.models import * # noqa @@ -28,7 +27,6 @@ logger = logging.getLogger('awx.main.commands.run_callback_receiver') MAX_REQUESTS = 10000 WORKERS = 4 - class CallbackReceiver(object): def __init__(self): self.parent_mappings = {} diff --git a/awx/main/management/commands/run_socketio_service.py b/awx/main/management/commands/run_socketio_service.py index 2a47818893..d50f3aee2b 100644 --- a/awx/main/management/commands/run_socketio_service.py +++ b/awx/main/management/commands/run_socketio_service.py @@ -11,7 +11,6 @@ from threading import Thread # Django from django.conf import settings from django.core.management.base import NoArgsCommand -from django.utils.timezone import now # AWX import awx @@ -49,23 +48,21 @@ class TowerBaseNamespace(BaseNamespace): return set(['recv_connect']) def valid_user(self): - if 'HTTP_COOKIE' not in self.environ: + if 'QUERY_STRING' not in self.environ: return False else: try: - all_keys = [e.strip() for e in self.environ['HTTP_COOKIE'].split(";")] - for each_key in all_keys: - k, v = each_key.split("=") - if k == "token": - token_actual = urllib.unquote_plus(v).decode().replace("\"","") - auth_token = AuthToken.objects.filter(key=token_actual) - if not auth_token.exists(): - return False - auth_token = auth_token[0] - if not auth_token.expired: - return auth_token.user - else: - return False + k, v = self.environ['QUERY_STRING'].split("=") + if k == "Token": + token_actual = urllib.unquote_plus(v).decode().replace("\"","") + auth_token = AuthToken.objects.filter(key=token_actual) + if not auth_token.exists(): + return False + auth_token = auth_token[0] + if not auth_token.expired: + return auth_token.user + else: + return False except Exception, e: logger.error("Exception validating user: " + str(e)) return False diff --git a/awx/main/management/commands/run_task_system.py b/awx/main/management/commands/run_task_system.py index 31acbb6af4..d7918e58f5 100644 --- a/awx/main/management/commands/run_task_system.py +++ b/awx/main/management/commands/run_task_system.py @@ -11,7 +11,6 @@ import time # Django from django.conf import settings from django.core.management.base import NoArgsCommand -from django.utils.timezone import now # AWX from awx.main.models import * # noqa diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 0a33b19e34..3e4a474c3d 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -5,6 +5,7 @@ import hmac import json import logging +import re # Django 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.utils import decrypt_field, ignore_inventory_computed_fields from awx.main.utils import emit_websocket_notification +from awx.main.redact import PlainTextCleaner logger = logging.getLogger('awx.main.models.jobs') @@ -220,7 +222,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions): if survey_element['variable'] not in data and \ survey_element['required']: errors.append("'%s' value missing" % survey_element['variable']) - elif survey_element['type'] in ["textarea", "text"]: + elif survey_element['type'] in ["textarea", "text", "password"]: if survey_element['variable'] in data: if 'min' in survey_element and survey_element['min'] not in ["", None] and len(data[survey_element['variable']]) < survey_element['min']: errors.append("'%s' value %s is too small (must be at least %s)" % @@ -452,6 +454,31 @@ class Job(UnifiedJob, JobOptions): evars.update(extra_vars) self.update_fields(extra_vars=json.dumps(evars)) + def _survey_search_and_replace(self, content): + # Use job template survey spec to identify password fields. + # Then lookup password fields in extra_vars and save the values + jt = self.job_template + if jt and jt.survey_enabled and 'spec' in jt.survey_spec: + 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): presets = {} for kw in self.job_template._get_unified_job_field_names(): diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 7e4978a7f3..00df267fec 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -625,16 +625,27 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique else: return StringIO("stdout capture is missing") + def _escape_ascii(self, content): + ansi_escape = re.compile(r'\x1b[^m]*m') + return ansi_escape.sub('', content) + + def _result_stdout_raw(self, redact_sensitive=True, escape_ascii=False): + content = self.result_stdout_raw_handle().read() + if redact_sensitive: + content = UriCleaner.remove_sensitive(content) + if escape_ascii: + content = self._escape_ascii(content) + return content + @property def result_stdout_raw(self): - return self.result_stdout_raw_handle().read() + return self._result_stdout_raw() @property def result_stdout(self): - ansi_escape = re.compile(r'\x1b[^m]*m') - return ansi_escape.sub('', UriCleaner.remove_sensitive(self.result_stdout_raw)) + return self._result_stdout_raw(escape_ascii=True) - def result_stdout_raw_limited(self, start_line=0, end_line=None): + def _result_stdout_raw_limited(self, start_line=0, end_line=None, redact_sensitive=True, escape_ascii=False): return_buffer = u"" if end_line is not None: end_line = int(end_line) @@ -651,12 +662,19 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique end_actual = min(int(end_line), len(stdout_lines)) else: end_actual = len(stdout_lines) + + if redact_sensitive: + return_buffer = UriCleaner.remove_sensitive(return_buffer) + if escape_ascii: + return_buffer = self._escape_ascii(return_buffer) + return return_buffer, start_actual, end_actual, absolute_end + def result_stdout_raw_limited(self, start_line=0, end_line=None): + return self._result_stdout_raw_limited(start_line, end_line) + def result_stdout_limited(self, start_line=0, end_line=None): - ansi_escape = re.compile(r'\x1b[^m]*m') - content, start, end, absolute_end = UriCleaner.remove_sensitive(self.result_stdout_raw_limited(start_line, end_line)) - return ansi_escape.sub('', content), start, end, absolute_end + return self._result_stdout_raw_limited(start_line, end_line, escape_ascii=True) @property def celery_task(self): @@ -729,9 +747,6 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique def signal_start(self, **kwargs): """Notify the task runner system to begin work on this task.""" - # Sanity check: If we are running unit tests, then run synchronously. - if getattr(settings, 'CELERY_UNIT_TEST', False): - return self.start(None, **kwargs) # Sanity check: Are we able to start the job? If not, do not attempt # to do so. @@ -747,6 +762,10 @@ class UnifiedJob(PolymorphicModel, PasswordFieldsModel, CommonModelNameNotUnique if 'extra_vars' in kwargs: self.handle_extra_data(kwargs['extra_vars']) + # Sanity check: If we are running unit tests, then run synchronously. + if getattr(settings, 'CELERY_UNIT_TEST', False): + return self.start(None, **kwargs) + # Save the pending status, and inform the SocketIO listener. self.update_fields(start_args=json.dumps(kwargs), status='pending') self.socketio_emit_status("pending") diff --git a/awx/main/redact.py b/awx/main/redact.py index 1c196aead7..10f7877faa 100644 --- a/awx/main/redact.py +++ b/awx/main/redact.py @@ -1,8 +1,10 @@ import re import urlparse +REPLACE_STR = '$encrypted$' + class UriCleaner(object): - REPLACE_STR = '$encrypted$' + REPLACE_STR = REPLACE_STR # https://regex101.com/r/sV2dO2/2 SENSITIVE_URI_PATTERN = re.compile(ur'(?i)\b((?:https?://|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?\xab\xbb\u201c\u201d\u2018\u2019]))', re.MULTILINE) @@ -51,4 +53,9 @@ class UriCleaner(object): return redactedtext +class PlainTextCleaner(object): + REPLACE_STR = REPLACE_STR + @staticmethod + def remove_sensitive(cleartext, sensitive): + return re.sub(r'%s' % re.escape(sensitive), '$encrypted$', cleartext) diff --git a/awx/main/tests/__init__.py b/awx/main/tests/__init__.py index 7489dbd7b3..d5b6667e43 100644 --- a/awx/main/tests/__init__.py +++ b/awx/main/tests/__init__.py @@ -14,3 +14,4 @@ from awx.main.tests.activity_stream import * # noqa from awx.main.tests.schedules import * # noqa from awx.main.tests.redact import * # noqa from awx.main.tests.views import * # noqa +from awx.main.tests.jobs import * diff --git a/awx/main/tests/base.py b/awx/main/tests/base.py index 9bc25b91f8..4da109718c 100644 --- a/awx/main/tests/base.py +++ b/awx/main/tests/base.py @@ -13,15 +13,17 @@ import tempfile import time from multiprocessing import Process from subprocess import Popen +import re # PyYAML import yaml # Django +import django.test from django.conf import settings, UserSettingsHolder from django.contrib.auth.models import User -import django.test from django.test.client import Client +from django.test.utils import override_settings # AWX from awx.main.models import * # noqa @@ -211,17 +213,20 @@ class BaseTestMixin(QueueTestMixin): def make_organizations(self, created_by, count=1): results = [] for x in range(0, count): - self.object_ctr = self.object_ctr + 1 - results.append(Organization.objects.create( - name="org%s-%s" % (x, self.object_ctr), description="org%s" % x, created_by=created_by - )) + results.append(self.make_organization(created_by=created_by, count=x)) return results - def make_organization(self, created_by): - return self.make_organizations(created_by, 1)[0] + def make_organization(self, created_by, count=1): + self.object_ctr = self.object_ctr + 1 + return Organization.objects.create( + name="org%s-%s" % (count, self.object_ctr), description="org%s" % count, created_by=created_by + ) - def make_project(self, name, description='', created_by=None, + def make_project(self, name=None, description='', created_by=None, playbook_content='', role_playbooks=None, unicode_prefix=True): + if not name: + name = self.unique_name('Project') + if not os.path.exists(settings.PROJECTS_ROOT): os.makedirs(settings.PROJECTS_ROOT) # Create temp project directory. @@ -283,7 +288,7 @@ class BaseTestMixin(QueueTestMixin): return Inventory.objects.create(name=name or self.unique_name('Inventory'), organization=organization, created_by=created_by) - def make_job_template(self, name=None, created_by=None, organization=None, inventory=None, project=None, playbook=None): + def make_job_template(self, name=None, created_by=None, organization=None, inventory=None, project=None, playbook=None, **kwargs): created_by = self.decide_created_by(created_by) if not inventory: inventory = self.make_inventory(organization=organization, created_by=created_by) @@ -300,24 +305,47 @@ class BaseTestMixin(QueueTestMixin): if project not in organization.projects.all(): organization.projects.add(project) - return JobTemplate.objects.create( - name=name or self.unique_name('JobTemplate'), - job_type='check', - inventory=inventory, - project=project, - playbook=project.playbooks[0], - host_config_key=settings.SYSTEM_UUID, - created_by=created_by, - ) + opts = { + 'name' : name or self.unique_name('JobTemplate'), + 'job_type': 'check', + 'inventory': inventory, + 'project': project, + 'host_config_key': settings.SYSTEM_UUID, + 'created_by': created_by, + 'playbook': playbook, + } + opts.update(kwargs) + return JobTemplate.objects.create(**opts) - def make_job(self, job_template=None, created_by=None, inital_state='new'): + def make_job(self, job_template=None, created_by=None, inital_state='new', **kwargs): created_by = self.decide_created_by(created_by) if not job_template: job_template = self.make_job_template(created_by=created_by) - job = job_template.create_job(created_by=created_by) - job.status = inital_state - return job + opts = { + 'created_by': created_by, + 'status': inital_state, + } + opts.update(kwargs) + return job_template.create_job(**opts) + + def make_credential(self, **kwargs): + opts = { + 'name': self.unique_name('Credential'), + 'kind': 'ssh', + 'user': self.super_django_user, + 'username': '', + 'ssh_key_data': '', + 'ssh_key_unlock': '', + 'password': '', + 'sudo_username': '', + 'sudo_password': '', + 'su_username': '', + 'su_password': '', + 'vault_password': '', + } + opts.update(kwargs) + return Credential.objects.create(**opts) def setup_instances(self): instance = Instance(uuid=settings.SYSTEM_UUID, primary=True, hostname='127.0.0.1') @@ -419,6 +447,10 @@ class BaseTestMixin(QueueTestMixin): obj = json.loads(response.content) elif response['Content-Type'].startswith('application/yaml'): obj = yaml.safe_load(response.content) + elif response['Content-Type'].startswith('text/plain'): + obj = { 'content': response.content } + elif response['Content-Type'].startswith('text/html'): + obj = { 'content': response.content } else: self.fail('Unsupport response content type %s' % response['Content-Type']) else: @@ -556,12 +588,58 @@ class BaseTestMixin(QueueTestMixin): msg += 'fields %s not returned ' % ', '.join(not_returned) self.assertTrue(set(obj.keys()) <= set(fields), msg) - def check_not_found(self, string, substr): - self.assertEqual(string.find(substr), -1, "'%s' found in:\n%s" % (substr, string)) + def check_not_found(self, string, substr, description=None, word_boundary=False): + if word_boundary: + count = len(re.findall(r'\b%s\b' % re.escape(substr), string)) + else: + count = string.find(substr) + if count == -1: + count = 0 + + msg = '' + if description: + msg = 'Test "%s".\n' % description + msg += '"%s" found in: "%s"' % (substr, string) + self.assertEqual(count, 0, msg) + + def check_found(self, string, substr, count, description=None, word_boundary=False): + if word_boundary: + count_actual = len(re.findall(r'\b%s\b' % re.escape(substr), string)) + else: + count_actual = string.count(substr) + + msg = '' + if description: + msg = 'Test "%s".\n' % description + msg += 'Found %d occurances of "%s" instead of %d in: "%s"' % (count_actual, substr, count, string) + self.assertEqual(count_actual, count, msg) + + def check_job_result(self, job, expected='successful', expect_stdout=True, + expect_traceback=False): + msg = u'job status is %s, expected %s' % (job.status, expected) + msg = u'%s\nargs:\n%s' % (msg, job.job_args) + msg = u'%s\nenv:\n%s' % (msg, job.job_env) + if job.result_traceback: + msg = u'%s\ngot traceback:\n%s' % (msg, job.result_traceback) + if job.result_stdout: + msg = u'%s\ngot stdout:\n%s' % (msg, job.result_stdout) + if isinstance(expected, (list, tuple)): + self.assertTrue(job.status in expected) + else: + self.assertEqual(job.status, expected, msg) + if expect_stdout: + self.assertTrue(job.result_stdout) + else: + self.assertTrue(job.result_stdout in ('', 'stdout capture is missing'), + u'expected no stdout, got:\n%s' % + job.result_stdout) + if expect_traceback: + self.assertTrue(job.result_traceback) + else: + self.assertFalse(job.result_traceback, + u'expected no traceback, got:\n%s' % + job.result_traceback) - def check_found(self, string, substr, count=1): - count_actual = string.count(substr) - self.assertEqual(count_actual, count, "Found %d occurances of '%s' instead of %d in:\n%s" % (count_actual, substr, count, string)) def start_taskmanager(self, command_port): self.start_redis() @@ -589,6 +667,17 @@ class BaseLiveServerTest(BaseTestMixin, django.test.LiveServerTestCase): ''' Base class for tests requiring a live test server. ''' + def setUp(self): + super(BaseLiveServerTest, self).setUp() + settings.INTERNAL_API_URL = self.live_server_url + +@override_settings(CELERY_ALWAYS_EAGER=True, + CELERY_EAGER_PROPAGATES_EXCEPTIONS=True, + ANSIBLE_TRANSPORT='local') +class BaseJobExecutionTest(QueueStartStopTestMixin, BaseLiveServerTest): + ''' + Base class for celery task tests. + ''' # Helps with test cases. # Save all components of a uri (i.e. scheme, username, password, etc.) so that diff --git a/awx/main/tests/commands.py b/awx/main/tests/commands.py index 3141e541a2..2d52f4b574 100644 --- a/awx/main/tests/commands.py +++ b/awx/main/tests/commands.py @@ -310,7 +310,6 @@ class CleanupJobsTest(BaseCommandMixin, BaseLiveServerTest): self.group.hosts.add(self.host) self.project = None self.credential = None - settings.INTERNAL_API_URL = self.live_server_url self.start_queue() def tearDown(self): @@ -320,18 +319,7 @@ class CleanupJobsTest(BaseCommandMixin, BaseLiveServerTest): shutil.rmtree(self.test_project_path, True) def create_test_credential(self, **kwargs): - opts = { - 'name': 'test-creds', - 'user': self.super_django_user, - 'ssh_username': '', - 'ssh_key_data': '', - 'ssh_key_unlock': '', - 'ssh_password': '', - 'sudo_username': '', - 'sudo_password': '', - } - opts.update(kwargs) - self.credential = Credential.objects.create(**opts) + self.credential = self.make_credential(kwargs) return self.credential def create_test_project(self, playbook_content): diff --git a/awx/main/tests/jobs/__init__.py b/awx/main/tests/jobs/__init__.py new file mode 100644 index 0000000000..0087044beb --- /dev/null +++ b/awx/main/tests/jobs/__init__.py @@ -0,0 +1,3 @@ + +from awx.main.tests.jobs.jobs import * +from awx.main.tests.jobs.survey_password import * \ No newline at end of file diff --git a/awx/main/tests/jobs.py b/awx/main/tests/jobs/jobs.py similarity index 99% rename from awx/main/tests/jobs.py rename to awx/main/tests/jobs/jobs.py index 3bab9a4603..89826e782c 100644 --- a/awx/main/tests/jobs.py +++ b/awx/main/tests/jobs/jobs.py @@ -27,7 +27,7 @@ from awx.main.models import * # noqa from awx.main.tests.base import BaseTestMixin __all__ = ['JobTemplateTest', 'JobTest', 'JobStartCancelTest', - 'JobTemplateCallbackTest', 'JobTransactionTest'] + 'JobTemplateCallbackTest', 'JobTransactionTest', 'JobTemplateSurveyTest'] TEST_PLAYBOOK = '''- hosts: all gather_facts: false @@ -933,112 +933,6 @@ class JobTemplateTest(BaseJobTestMixin, django.test.TestCase): # FIXME: Check other credentials and optional fields. - def test_post_job_template_survey(self): - url = reverse('api:job_template_list') - data = dict( - name = 'launched job template', - job_type = PERM_INVENTORY_DEPLOY, - inventory = self.inv_eng.pk, - project = self.proj_dev.pk, - playbook = self.proj_dev.playbooks[0], - credential = self.cred_sue.pk, - survey_enabled = True, - ) - with self.current_user(self.user_sue): - response = self.post(url, data, expect=201) - new_jt_id = response['id'] - detail_url = reverse('api:job_template_detail', - args=(new_jt_id,)) - self.assertEquals(response['url'], detail_url) - url = reverse('api:job_template_survey_spec', args=(new_jt_id,)) - with self.current_user(self.user_sue): - response = self.post(url, json.loads(TEST_SIMPLE_REQUIRED_SURVEY), expect=200) - launch_url = reverse('api:job_template_launch', args=(new_jt_id,)) - response = self.get(launch_url) - self.assertTrue('favorite_color' in response['variables_needed_to_start']) - response = self.post(launch_url, dict(extra_vars=dict(favorite_color="green")), expect=202) - job = Job.objects.get(pk=response["job"]) - job_extra = json.loads(job.extra_vars) - self.assertTrue("favorite_color" in job_extra) - - with self.current_user(self.user_sue): - response = self.post(url, json.loads(TEST_SIMPLE_NONREQUIRED_SURVEY), expect=200) - launch_url = reverse('api:job_template_launch', args=(new_jt_id,)) - response = self.get(launch_url) - self.assertTrue(len(response['variables_needed_to_start']) == 0) - - with self.current_user(self.user_sue): - response = self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200) - launch_url = reverse('api:job_template_launch', args=(new_jt_id,)) - # Just the required answer should work - self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo")), expect=202) - # Short answer but requires a long answer - self.post(launch_url, dict(extra_vars=dict(long_answer='a', reqd_answer="foo")), expect=400) - # Long answer but requires a short answer - self.post(launch_url, dict(extra_vars=dict(short_answer='thisissomelongtext', reqd_answer="foo")), expect=400) - # Long answer but missing required answer - self.post(launch_url, dict(extra_vars=dict(long_answer='thisissomelongtext')), expect=400) - # Integer that's not big enough - self.post(launch_url, dict(extra_vars=dict(int_answer=0, reqd_answer="foo")), expect=400) - # Integer that's too big - self.post(launch_url, dict(extra_vars=dict(int_answer=10, reqd_answer="foo")), expect=400) - # Integer that's just riiiiight - self.post(launch_url, dict(extra_vars=dict(int_answer=3, reqd_answer="foo")), expect=202) - # Integer bigger than min with no max defined - self.post(launch_url, dict(extra_vars=dict(int_answer_no_max=3, reqd_answer="foo")), expect=202) - # Integer answer that's the wrong type - self.post(launch_url, dict(extra_vars=dict(int_answer="test", reqd_answer="foo")), expect=400) - # Float that's too big - self.post(launch_url, dict(extra_vars=dict(float_answer=10.5, reqd_answer="foo")), expect=400) - # Float that's too small - self.post(launch_url, dict(extra_vars=dict(float_answer=1.995, reqd_answer="foo")), expect=400) - # float that's just riiiiight - self.post(launch_url, dict(extra_vars=dict(float_answer=2.01, reqd_answer="foo")), expect=202) - # float answer that's the wrong type - self.post(launch_url, dict(extra_vars=dict(float_answer="test", reqd_answer="foo")), expect=400) - # Wrong choice in single choice - self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", single_choice="three")), expect=400) - # Wrong choice in multi choice - self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice=["four"])), expect=400) - # Wrong type for multi choicen - self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice="two")), expect=400) - # Right choice in single choice - self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", single_choice="two")), expect=202) - # Right choices in multi choice - self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice=["one", "two"])), expect=202) - # Nested json - self.post(launch_url, dict(extra_vars=dict(json_answer=dict(test="val", num=1), reqd_answer="foo")), expect=202) - - # Bob can access and update the survey because he's an org-admin - with self.current_user(self.user_bob): - self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200) - - # Chuck is the lead engineer and has the right permissions to edit it also - with self.current_user(self.user_chuck): - self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200) - - # Doug shouldn't be able to access this playbook - with self.current_user(self.user_doug): - self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=403) - - # Neither can juan because he doesn't have the job template create permission - with self.current_user(self.user_juan): - self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=403) - - # Bob and chuck can read the template - with self.current_user(self.user_bob): - self.get(url, expect=200) - - with self.current_user(self.user_chuck): - self.get(url, expect=200) - - # Doug and Juan can't - with self.current_user(self.user_doug): - self.get(url, expect=403) - - with self.current_user(self.user_juan): - self.get(url, expect=403) - def test_launch_job_template(self): url = reverse('api:job_template_list') data = dict( @@ -1945,3 +1839,115 @@ class JobTransactionTest(BaseJobTestMixin, django.test.LiveServerTestCase): self.assertEqual(job.status, 'successful', job.result_stdout) self.assertFalse(errors) +class JobTemplateSurveyTest(BaseJobTestMixin, django.test.TestCase): + def setUp(self): + super(JobTemplateSurveyTest, self).setUp() + + def tearDown(self): + super(JobTemplateSurveyTest, self).tearDown() + + def test_post_job_template_survey(self): + url = reverse('api:job_template_list') + data = dict( + name = 'launched job template', + job_type = PERM_INVENTORY_DEPLOY, + inventory = self.inv_eng.pk, + project = self.proj_dev.pk, + playbook = self.proj_dev.playbooks[0], + credential = self.cred_sue.pk, + survey_enabled = True, + ) + with self.current_user(self.user_sue): + response = self.post(url, data, expect=201) + new_jt_id = response['id'] + detail_url = reverse('api:job_template_detail', + args=(new_jt_id,)) + self.assertEquals(response['url'], detail_url) + url = reverse('api:job_template_survey_spec', args=(new_jt_id,)) + with self.current_user(self.user_sue): + response = self.post(url, json.loads(TEST_SIMPLE_REQUIRED_SURVEY), expect=200) + launch_url = reverse('api:job_template_launch', args=(new_jt_id,)) + response = self.get(launch_url) + self.assertTrue('favorite_color' in response['variables_needed_to_start']) + response = self.post(launch_url, dict(extra_vars=dict(favorite_color="green")), expect=202) + job = Job.objects.get(pk=response["job"]) + job_extra = json.loads(job.extra_vars) + self.assertTrue("favorite_color" in job_extra) + + with self.current_user(self.user_sue): + response = self.post(url, json.loads(TEST_SIMPLE_NONREQUIRED_SURVEY), expect=200) + launch_url = reverse('api:job_template_launch', args=(new_jt_id,)) + response = self.get(launch_url) + self.assertTrue(len(response['variables_needed_to_start']) == 0) + + with self.current_user(self.user_sue): + response = self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200) + launch_url = reverse('api:job_template_launch', args=(new_jt_id,)) + # Just the required answer should work + self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo")), expect=202) + # Short answer but requires a long answer + self.post(launch_url, dict(extra_vars=dict(long_answer='a', reqd_answer="foo")), expect=400) + # Long answer but requires a short answer + self.post(launch_url, dict(extra_vars=dict(short_answer='thisissomelongtext', reqd_answer="foo")), expect=400) + # Long answer but missing required answer + self.post(launch_url, dict(extra_vars=dict(long_answer='thisissomelongtext')), expect=400) + # Integer that's not big enough + self.post(launch_url, dict(extra_vars=dict(int_answer=0, reqd_answer="foo")), expect=400) + # Integer that's too big + self.post(launch_url, dict(extra_vars=dict(int_answer=10, reqd_answer="foo")), expect=400) + # Integer that's just riiiiight + self.post(launch_url, dict(extra_vars=dict(int_answer=3, reqd_answer="foo")), expect=202) + # Integer bigger than min with no max defined + self.post(launch_url, dict(extra_vars=dict(int_answer_no_max=3, reqd_answer="foo")), expect=202) + # Integer answer that's the wrong type + self.post(launch_url, dict(extra_vars=dict(int_answer="test", reqd_answer="foo")), expect=400) + # Float that's too big + self.post(launch_url, dict(extra_vars=dict(float_answer=10.5, reqd_answer="foo")), expect=400) + # Float that's too small + self.post(launch_url, dict(extra_vars=dict(float_answer=1.995, reqd_answer="foo")), expect=400) + # float that's just riiiiight + self.post(launch_url, dict(extra_vars=dict(float_answer=2.01, reqd_answer="foo")), expect=202) + # float answer that's the wrong type + self.post(launch_url, dict(extra_vars=dict(float_answer="test", reqd_answer="foo")), expect=400) + # Wrong choice in single choice + self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", single_choice="three")), expect=400) + # Wrong choice in multi choice + self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice=["four"])), expect=400) + # Wrong type for multi choicen + self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice="two")), expect=400) + # Right choice in single choice + self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", single_choice="two")), expect=202) + # Right choices in multi choice + self.post(launch_url, dict(extra_vars=dict(reqd_answer="foo", multi_choice=["one", "two"])), expect=202) + # Nested json + self.post(launch_url, dict(extra_vars=dict(json_answer=dict(test="val", num=1), reqd_answer="foo")), expect=202) + + # Bob can access and update the survey because he's an org-admin + with self.current_user(self.user_bob): + self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200) + + # Chuck is the lead engineer and has the right permissions to edit it also + with self.current_user(self.user_chuck): + self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=200) + + # Doug shouldn't be able to access this playbook + with self.current_user(self.user_doug): + self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=403) + + # Neither can juan because he doesn't have the job template create permission + with self.current_user(self.user_juan): + self.post(url, json.loads(TEST_SURVEY_REQUIREMENTS), expect=403) + + # Bob and chuck can read the template + with self.current_user(self.user_bob): + self.get(url, expect=200) + + with self.current_user(self.user_chuck): + self.get(url, expect=200) + + # Doug and Juan can't + with self.current_user(self.user_doug): + self.get(url, expect=403) + + with self.current_user(self.user_juan): + self.get(url, expect=403) diff --git a/awx/main/tests/jobs/survey_password.py b/awx/main/tests/jobs/survey_password.py new file mode 100644 index 0000000000..4d652675f2 --- /dev/null +++ b/awx/main/tests/jobs/survey_password.py @@ -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) + + \ No newline at end of file diff --git a/awx/main/tests/redact.py b/awx/main/tests/redact.py index 96ac2a03b0..ea61aebd77 100644 --- a/awx/main/tests/redact.py +++ b/awx/main/tests/redact.py @@ -87,9 +87,9 @@ class UriCleanTests(BaseTest): for uri in TEST_URIS: redacted_str = UriCleaner.remove_sensitive(str(uri)) if uri.username: - self.check_not_found(redacted_str, uri.username) + self.check_not_found(redacted_str, uri.username, uri.description) if uri.password: - self.check_not_found(redacted_str, uri.password) + self.check_not_found(redacted_str, uri.password, uri.description) # should replace secret data with safe string, UriCleaner.REPLACE_STR def test_uri_scm_simple_replaced(self): @@ -107,9 +107,9 @@ class UriCleanTests(BaseTest): redacted_str = UriCleaner.remove_sensitive(str(uri)) if uri.username: - self.check_not_found(redacted_str, uri.username) + self.check_not_found(redacted_str, uri.username, uri.description) if uri.password: - self.check_not_found(redacted_str, uri.password) + self.check_not_found(redacted_str, uri.password, uri.description) # should replace multiple secret data with safe string def test_uri_scm_multiple_replaced(self): @@ -131,8 +131,8 @@ class UriCleanTests(BaseTest): for test_data in TEST_CLEARTEXT: uri = test_data['uri'] redacted_str = UriCleaner.remove_sensitive(test_data['text']) - self.check_not_found(redacted_str, uri.username) - self.check_not_found(redacted_str, uri.password) + self.check_not_found(redacted_str, uri.username, uri.description) + self.check_not_found(redacted_str, uri.password, uri.description) # Ensure the host didn't get redacted - self.check_found(redacted_str, uri.host, count=test_data['host_occurrences']) + self.check_found(redacted_str, uri.host, test_data['host_occurrences'], uri.description) diff --git a/awx/main/tests/tasks.py b/awx/main/tests/tasks.py index 831a98d18a..c0785597ba 100644 --- a/awx/main/tests/tasks.py +++ b/awx/main/tests/tasks.py @@ -21,7 +21,7 @@ from crum import impersonate # AWX from awx.main.models import * # noqa -from awx.main.tests.base import BaseLiveServerTest +from awx.main.tests.base import BaseJobExecutionTest TEST_PLAYBOOK = u''' - name: test success @@ -341,15 +341,7 @@ L5Hj+B02+FAiz8zVGumbVykvPtzgTb0E+0rJKNO0/EgGqWsk/oC0 TEST_SSH_KEY_DATA_UNLOCK = 'unlockme' -@override_settings(CELERY_ALWAYS_EAGER=True, - CELERY_EAGER_PROPAGATES_EXCEPTIONS=True) -class BaseCeleryTest(BaseLiveServerTest): - ''' - Base class for celery task tests. - ''' - -@override_settings(ANSIBLE_TRANSPORT='local') -class RunJobTest(BaseCeleryTest): +class RunJobTest(BaseJobExecutionTest): ''' Test cases for RunJob celery task. ''' @@ -371,31 +363,14 @@ class RunJobTest(BaseCeleryTest): self.credential = None self.cloud_credential = None settings.INTERNAL_API_URL = self.live_server_url - self.start_queue() def tearDown(self): super(RunJobTest, self).tearDown() if self.test_project_path: shutil.rmtree(self.test_project_path, True) - self.terminate_queue() def create_test_credential(self, **kwargs): - opts = { - 'name': 'test-creds', - 'kind': 'ssh', - 'user': self.super_django_user, - 'username': '', - 'ssh_key_data': '', - 'ssh_key_unlock': '', - 'password': '', - 'sudo_username': '', - 'sudo_password': '', - 'su_username': '', - 'su_password': '', - 'vault_password': '', - } - opts.update(kwargs) - self.credential = Credential.objects.create(**opts) + self.credential = self.make_credential(**kwargs) return self.credential def create_test_cloud_credential(self, **kwargs): @@ -429,7 +404,7 @@ class RunJobTest(BaseCeleryTest): except (AttributeError, IndexError): pass opts.update(kwargs) - self.job_template = JobTemplate.objects.create(**opts) + self.job_template = self.make_job_template(**opts) return self.job_template def create_test_job(self, **kwargs): @@ -453,32 +428,6 @@ class RunJobTest(BaseCeleryTest): self.job = Job.objects.create(**opts) return self.job - def check_job_result(self, job, expected='successful', expect_stdout=True, - expect_traceback=False): - msg = u'job status is %s, expected %s' % (job.status, expected) - msg = u'%s\nargs:\n%s' % (msg, job.job_args) - msg = u'%s\nenv:\n%s' % (msg, job.job_env) - if job.result_traceback: - msg = u'%s\ngot traceback:\n%s' % (msg, job.result_traceback) - if job.result_stdout: - msg = u'%s\ngot stdout:\n%s' % (msg, job.result_stdout) - if isinstance(expected, (list, tuple)): - self.assertTrue(job.status in expected) - else: - self.assertEqual(job.status, expected, msg) - if expect_stdout: - self.assertTrue(job.result_stdout) - else: - self.assertTrue(job.result_stdout in ('', 'stdout capture is missing'), - u'expected no stdout, got:\n%s' % - job.result_stdout) - if expect_traceback: - self.assertTrue(job.result_traceback) - else: - self.assertFalse(job.result_traceback, - u'expected no traceback, got:\n%s' % - job.result_traceback) - def check_job_events(self, job, runner_status='ok', plays=1, tasks=1, async=False, async_timeout=False, async_nowait=False, check_ignore_errors=False, async_tasks=0, diff --git a/awx/main/tests/views.py b/awx/main/tests/views.py index 56c454ca86..34924b1da8 100644 --- a/awx/main/tests/views.py +++ b/awx/main/tests/views.py @@ -5,29 +5,30 @@ from django.core.urlresolvers import reverse from awx.main.tests.base import BaseLiveServerTest, QueueStartStopTestMixin from awx.main.tests.base import URI -__all__ = ['UnifiedJobStdoutTests'] +__all__ = ['UnifiedJobStdoutRedactedTests'] TEST_STDOUTS = [] uri = URI(scheme="https", username="Dhh3U47nmC26xk9PKscV", password="PXPfWW8YzYrgS@E5NbQ2H@", host="github.ginger.com/theirrepo.git/info/refs") TEST_STDOUTS.append({ + 'description': 'uri in a plain text document', 'uri' : uri, 'text' : 'hello world %s goodbye world' % uri, - 'host_occurrences' : 1 + 'occurrences' : 1 }) uri = URI(scheme="https", username="applepie@@@", password="thatyouknow@@@@", host="github.ginger.com/theirrepo.git/info/refs") TEST_STDOUTS.append({ + 'description': 'uri appears twice in a multiline plain text document', 'uri' : uri, 'text' : 'hello world %s \n\nyoyo\n\nhello\n%s' % (uri, uri), - 'host_occurrences' : 2 + 'occurrences' : 2 }) - -class UnifiedJobStdoutTests(BaseLiveServerTest, QueueStartStopTestMixin): +class UnifiedJobStdoutRedactedTests(BaseLiveServerTest, QueueStartStopTestMixin): def setUp(self): - super(UnifiedJobStdoutTests, self).setUp() + super(UnifiedJobStdoutRedactedTests, self).setUp() self.setup_instances() self.setup_users() self.test_cases = [] @@ -40,15 +41,38 @@ class UnifiedJobStdoutTests(BaseLiveServerTest, QueueStartStopTestMixin): # This is more of a functional test than a unit test. # should filter out username and password - def test_redaction_enabled(self): + def check_sensitive_redacted(self, test_data, response): + uri = test_data['uri'] + self.assertIsNotNone(response['content']) + self.check_not_found(response['content'], uri.username, test_data['description']) + self.check_not_found(response['content'], uri.password, test_data['description']) + # Ensure the host didn't get redacted + self.check_found(response['content'], uri.host, test_data['occurrences'], test_data['description']) + + def _get_url_job_stdout(self, job, format='json'): + formats = { + 'json': 'application/json', + 'ansi': 'text/plain', + 'txt': 'text/plain', + 'html': 'text/html', + } + content_type = formats[format] + job_stdout_url = reverse('api:job_stdout', args=(job.pk,)) + "?format=" + format + return self.get(job_stdout_url, expect=200, auth=self.get_super_credentials(), accept=content_type) + + def _test_redaction_enabled(self, format): for test_data in self.test_cases: - uri = test_data['uri'] - job_stdout_url = reverse('api:job_stdout', args=(test_data['job'].pk,)) + response = self._get_url_job_stdout(test_data['job'], format=format) + self.check_sensitive_redacted(test_data, response) - response = self.get(job_stdout_url, expect=200, auth=self.get_super_credentials(), accept='application/json') + def test_redaction_enabled_json(self): + self._test_redaction_enabled('json') - self.assertIsNotNone(response['content']) - self.check_not_found(response['content'], uri.username) - self.check_not_found(response['content'], uri.password) - # Ensure the host didn't get redacted - self.check_found(response['content'], uri.host, count=test_data['host_occurrences']) + def test_redaction_enabled_ansi(self): + self._test_redaction_enabled('ansi') + + def test_redaction_enabled_html(self): + self._test_redaction_enabled('html') + + def test_redaction_enabled_txt(self): + self._test_redaction_enabled('txt') \ No newline at end of file diff --git a/awx/settings/development.py b/awx/settings/development.py index d305adb719..fa9e067744 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -37,9 +37,10 @@ except ImportError: if 'django_jenkins' in INSTALLED_APPS: JENKINS_TASKS = ( 'django_jenkins.tasks.run_pylint', - 'django_jenkins.tasks.run_pep8', - 'django_jenkins.tasks.run_pyflakes', 'django_jenkins.tasks.run_flake8', + # The following are not needed when including run_flake8 + # 'django_jenkins.tasks.run_pep8', + # 'django_jenkins.tasks.run_pyflakes', 'django_jenkins.tasks.run_jshint', 'django_jenkins.tasks.run_csslint', ) diff --git a/awx/ui/static/js/controllers/JobDetail.js b/awx/ui/static/js/controllers/JobDetail.js index b8e0a9a194..d4c3942a4f 100644 --- a/awx/ui/static/js/controllers/JobDetail.js +++ b/awx/ui/static/js/controllers/JobDetail.js @@ -200,7 +200,7 @@ export function JobDetailController ($location, $rootScope, $scope, $compile, $r scope.removeLoadHostSummaries(); } scope.removeHostSummaries = scope.$on('LoadHostSummaries', function() { - if(scope.job.related){ + if(scope.job){ var url = scope.job.related.job_host_summaries + '?'; url += '&page_size=' + scope.hostSummariesMaxRows + '&order=host_name'; @@ -247,9 +247,11 @@ export function JobDetailController ($location, $rootScope, $scope, $compile, $r if (scope.activeTask) { var play = scope.jobData.plays[scope.activePlay], - task = play.tasks[scope.activeTask], + task, // = play.tasks[scope.activeTask], url; - + if(play){ + task = play.tasks[scope.activeTask]; + } if (play && task) { url = scope.job.related.job_events + '?parent=' + task.id + '&'; url += 'event__startswith=runner&page_size=' + scope.hostResultsMaxRows + '&order=host_name,counter'; diff --git a/awx/ui/static/js/forms/SurveyQuestion.js b/awx/ui/static/js/forms/SurveyQuestion.js index 40f692d474..fe50a0a236 100644 --- a/awx/ui/static/js/forms/SurveyQuestion.js +++ b/awx/ui/static/js/forms/SurveyQuestion.js @@ -63,8 +63,8 @@ export default 'user_id
host_name
esc or click to close
" '+ 'data-placement="right" data-container="body" data-title="Answer Variable Name" class="help-link" data-original-title="" title="" tabindex="-1"> '+ '
'+ - '
Please enter an answer variable name.
'+ - '
Please remove the illegal character from the survey question variable name.
'+ + '
Please enter an answer variable name.
'+ + '
Please remove the illegal character from the survey question variable name.
'+ '
This question variable is already in use. Please enter a different variable name.
' + '
'+ '
', @@ -106,13 +106,13 @@ export default control:'
'+ '
'+ ''+ - '
The minimum length you entered is not a number. Please enter a number.
'+ + '
The minimum length you entered is not a number. Please enter a number.
'+ '
The minimium length is too high. Please enter a lower number.
'+ '
The minimum length is too low. Please enter a positive number.
'+ '
'+ '
'+ ''+ - '
The maximum length you entered is not a number. Please enter a number.
'+ + '
The maximum length you entered is not a number. Please enter a number.
'+ '
The maximum length is too low. Please enter a number larger than the minimum length you set.
'+ '
'+ '
', @@ -127,13 +127,13 @@ export default control:'
'+ '
'+ ''+ - '
The minimum length you entered is not a number. Please enter a number.
'+ + '
The minimum length you entered is not a number. Please enter a number.
'+ '
The minimium length is too high. Please enter a lower number.
'+ '
The minimum length is too low. Please enter a positive number.
'+ '
'+ '
'+ ''+ - '
The maximum length you entered is not a number. Please enter a number.
'+ + '
The maximum length you entered is not a number. Please enter a number.
'+ '
The maximum length is too low. Please enter a number larger than the minimum length you set.
'+ '
'+ '
', @@ -148,13 +148,13 @@ export default control:'
'+ '
'+ ''+ - '
The minimum length you entered is not a number. Please enter a number.
'+ + '
The minimum length you entered is not a number. Please enter a number.
'+ '
The minimium length is too high. Please enter a lower number.
'+ '
The minimum length is too low. Please enter a positive number.
'+ '
'+ '
'+ ''+ - '
The maximum length you entered is not a number. Please enter a number.
'+ + '
The maximum length you entered is not a number. Please enter a number.
'+ '
The maximum length is too low. Please enter a number larger than the minimum length you set.
'+ '
'+ '
', @@ -211,7 +211,7 @@ export default ''+ '
'+ ''+ - '
Please enter an answer for the choices listed.
' + + '
Please enter an answer from the choices listed.
' + '
The answer is shorter than the minimium length. Please make the answer longer.
' + '
The answer is longer than the maximum length. Please make the answer shorter.
' + '
'+ @@ -227,7 +227,7 @@ export default ''+ '
'+ ''+ - '
Please enter an answer/answers for the choices listed.
' + + '
Please enter an answer/answers from the choices listed.
' + '
'+ '
'+ '
', diff --git a/awx/ui/static/js/helpers/JobSubmission.js b/awx/ui/static/js/helpers/JobSubmission.js index d352f6c4ce..cb0d3b8be6 100644 --- a/awx/ui/static/js/helpers/JobSubmission.js +++ b/awx/ui/static/js/helpers/JobSubmission.js @@ -81,7 +81,7 @@ angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'Credential job_launch_data.extra_vars[fld] = scope[fld]; } // for optional text and text-areas, submit a blank string if min length is 0 - if(scope.survey_questions[i].required === false && (scope.survey_questions[i].type === "text" || scope.survey_questions[i].type === "textarea") && scope.survey_questions[i].min === 0 && scope[fld] ===""){ + if(scope.survey_questions[i].required === false && (scope.survey_questions[i].type === "text" || scope.survey_questions[i].type === "textarea") && scope.survey_questions[i].min === 0 && (scope[fld] === "" || scope[fld] === undefined)){ job_launch_data.extra_vars[fld] = ""; } } diff --git a/awx/ui/static/js/helpers/Survey.js b/awx/ui/static/js/helpers/Survey.js index e5f6289522..cc1834accf 100644 --- a/awx/ui/static/js/helpers/Survey.js +++ b/awx/ui/static/js/helpers/Survey.js @@ -577,11 +577,14 @@ angular.module('SurveyHelper', [ 'Utilities', 'RestServices', 'SchedulesHelper', scope.int_max = null; scope.float_min = null; scope.float_max = null; + scope.duplicate = false; + scope.invalidChoice = false; + scope.minTextError = false; + scope.maxTextError = false; }; scope.addNewQuestion = function(){ // $('#add_question_btn').on("click" , function(){ - scope.duplicate = false; scope.addQuestion(); $('#survey_question_question_name').focus(); $('#add_question_btn').attr('disabled', 'disabled'); diff --git a/awx/ui/static/js/widgets/PortalJobs.js b/awx/ui/static/js/widgets/PortalJobs.js index 4a4c66d7f7..ffd8005aba 100644 --- a/awx/ui/static/js/widgets/PortalJobs.js +++ b/awx/ui/static/js/widgets/PortalJobs.js @@ -45,7 +45,7 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) PortalJobsList.fields.type.searchOptions = scope.type_choices; } 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({ parent_scope: scope, scope: jobs_scope, @@ -77,7 +77,7 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) $("#active-jobs").empty(); $("#active-jobs-search-container").empty(); 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({ parent_scope: scope, scope: jobs_scope, @@ -96,8 +96,8 @@ angular.module('PortalJobsWidget', ['RestServices', 'Utilities']) html += "
\n"; html += "
" ; html += "
" ; - html += " " ; - html += "" ; + html += "" ; + html += "" ; html += "
" ; html += "
" ; html += "\n"; //row diff --git a/awx/ui/static/lib/ansible/Socket.js b/awx/ui/static/lib/ansible/Socket.js index d51438cc8b..486ec5a7d8 100644 --- a/awx/ui/static/lib/ansible/Socket.js +++ b/awx/ui/static/lib/ansible/Socket.js @@ -71,6 +71,7 @@ angular.module('SocketIO', ['AuthService', 'Utilities']) $log.debug('Socket connecting to: ' + url); self.scope.socket_url = url; self.socket = io.connect(url, { + query: "Token="+token, headers: { 'Authorization': 'Token ' + token, // i don't think these are actually inserted into the header--jt @@ -78,7 +79,7 @@ angular.module('SocketIO', ['AuthService', 'Utilities']) }, 'connect timeout': 3000, 'try multiple transports': false, - 'max reconneciton attemps': 3, + 'max reconnection attempts': 3, 'reconnection limit': 3000 }); diff --git a/awx/ui/static/lib/ansible/directives.js b/awx/ui/static/lib/ansible/directives.js index 1ac2599250..cf97f9d8e8 100644 --- a/awx/ui/static/lib/ansible/directives.js +++ b/awx/ui/static/lib/ansible/directives.js @@ -70,7 +70,7 @@ angular.module('AWDirectives', ['RestServices', 'Utilities', 'AuthService', 'Job require: 'ngModel', scope: { ngModel: '=ngModel' }, template: '
' + - '
- My Jobs + Jobs