From b5a34e45d26a55bc7993d79177c8c854e3b24184 Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 15 Aug 2017 15:12:09 -0400 Subject: [PATCH 001/138] Fixed credential permission user/teams lists. Fixed user permission credentials list --- .../credentials/legacy.credentials.js | 23 ++++++--- .../src/shared/stateDefinitions.factory.js | 49 +++++++++++++++++-- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/awx/ui/client/features/credentials/legacy.credentials.js b/awx/ui/client/features/credentials/legacy.credentials.js index 3025be90ac..28a11e8cb9 100644 --- a/awx/ui/client/features/credentials/legacy.credentials.js +++ b/awx/ui/client/features/credentials/legacy.credentials.js @@ -149,10 +149,10 @@ function LegacyCredentialsService (pathService) { 'QuerySet', '$stateParams', 'GetBasePath', - (list, qs, $stateParams, GetBasePath) => { - let path = GetBasePath(list.basePath) || GetBasePath(list.name); + 'resourceData', + (list, qs, $stateParams, GetBasePath, resourceData) => { + let path = resourceData.data.organization ? GetBasePath('organizations') + `${resourceData.data.organization}/users` : ((list.basePath) || GetBasePath(list.name)); return qs.search(path, $stateParams.user_search); - } ], teamsDataset: [ @@ -160,9 +160,19 @@ function LegacyCredentialsService (pathService) { 'QuerySet', '$stateParams', 'GetBasePath', - (list, qs, $stateParams, GetBasePath) => { + 'resourceData', + (list, qs, $stateParams, GetBasePath, resourceData) => { let path = GetBasePath(list.basePath) || GetBasePath(list.name); - return qs.search(path, $stateParams.team_search); + + if(!resourceData.data.organization) { + return null; + } + else { + $stateParams[`${list.iterator}_search`].organization = resourceData.data.organization; + return qs.search(path, $stateParams.team_search); + } + + } ], resourceData: ['CredentialModel', '$stateParams', (Credential, $stateParams) => { @@ -197,7 +207,8 @@ function LegacyCredentialsService (pathService) { teams-dataset='$resolve.teamsDataset' selected='allSelected' resource-data='$resolve.resourceData' - title='Add Users / Teams'> + without-team-permissions='{{$resolve.resourceData.data.organization ? null : true}}' + title='{{$resolve.resourceData.data.organization ? "Add Users / Teams" : "Add Users"}}'> ` } }, diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index b473e17426..d417ee867c 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -313,15 +313,58 @@ function($injector, $stateExtender, $log, i18n) { return qs.search(path, $stateParams[`${list.iterator}_search`]); } ], - credentialsDataset: ['CredentialList', 'QuerySet', '$stateParams', 'GetBasePath', 'resourceData', - function(list, qs, $stateParams, GetBasePath, resourceData) { + credentialsDataset: ['CredentialList', 'QuerySet', '$stateParams', 'GetBasePath', 'resourceData', 'Rest', '$q', + function(list, qs, $stateParams, GetBasePath, resourceData, Rest, $q) { let path = GetBasePath(list.basePath) || GetBasePath(list.name); if(resourceData.data.type === "team") { $stateParams[`${list.iterator}_search`].organization = resourceData.data.organization; } - return qs.search(path, $stateParams[`${list.iterator}_search`]); + if(resourceData.data.type === "user") { + + let resolve = $q.defer(); + + let getMoreOrgs = function(data, arr) { + Rest.setUrl(data.next); + Rest.get() + .then(function (resData) { + if (data.next) { + getMoreOrgs(resData.data, arr.concat(resData.data.results)); + } else { + resolve.resolve(arr.concat(resData.data.results)); + } + }); + }; + + Rest.setUrl(GetBasePath('users') + `${resourceData.data.id}/organizations?page_size=200`); + Rest.get() + .then(function(resData) { + if (resData.data.next) { + getMoreOrgs(resData.data, resData.data.results); + } else { + resolve.resolve(resData.data.results); + } + }); + + return resolve.promise.then(function (organizations) { + if(organizations && organizations.length > 0) { + let orgIds = _.map(organizations, function(organization){ + return organization.id; + }); + + $stateParams[`${list.iterator}_search`].or__organization = 'null'; + $stateParams[`${list.iterator}_search`].or__organization__in = orgIds.join(); + + } + + return qs.search(path, $stateParams[`${list.iterator}_search`]); + }); + + } + else { + return qs.search(path, $stateParams[`${list.iterator}_search`]); + } } ], organizationsDataset: ['OrganizationList', 'QuerySet', '$stateParams', 'GetBasePath', From 4bc26310acc8de3aac12eac14d1253b75ad9380c Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Tue, 15 Aug 2017 16:16:34 -0400 Subject: [PATCH 002/138] Add time unit to all unified job timeout conf help texts --- awx/main/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index c4f4bcf9b0..b8d47e591e 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -311,7 +311,7 @@ register( min_value=0, default=0, label=_('Default Inventory Update Timeout'), - help_text=_('Maximum time to allow inventory updates to run. Use value of 0 to indicate that no ' + help_text=_('Maximum time in seconds to allow inventory updates to run. Use value of 0 to indicate that no ' 'timeout should be imposed. A timeout set on an individual inventory source will override this.'), category=_('Jobs'), category_slug='jobs', @@ -323,7 +323,7 @@ register( min_value=0, default=0, label=_('Default Project Update Timeout'), - help_text=_('Maximum time to allow project updates to run. Use value of 0 to indicate that no ' + help_text=_('Maximum time in seconds to allow project updates to run. Use value of 0 to indicate that no ' 'timeout should be imposed. A timeout set on an individual project will override this.'), category=_('Jobs'), category_slug='jobs', From 01c3f62ed7f2ac2079acee41c9b3a7c3e9734b82 Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 15 Aug 2017 16:55:00 -0400 Subject: [PATCH 003/138] Added show/hide to explanation to limit long strings --- .../src/job-results/job-results.block.less | 10 +++++ .../src/job-results/job-results.controller.js | 2 + .../src/job-results/job-results.partial.html | 43 +++++++------------ .../standard-out-inventory-sync.partial.html | 13 ++++++ .../src/standard-out/standard-out.block.less | 10 +++++ .../standard-out/standard-out.controller.js | 39 ++++++++++++++++- 6 files changed, 88 insertions(+), 29 deletions(-) diff --git a/awx/ui/client/src/job-results/job-results.block.less b/awx/ui/client/src/job-results/job-results.block.less index e5ed7beddc..c05d872b74 100644 --- a/awx/ui/client/src/job-results/job-results.block.less +++ b/awx/ui/client/src/job-results/job-results.block.less @@ -232,3 +232,13 @@ job-results-standard-out { margin-left: 10px; color: @default-icon; } + +.JobResults-seeMoreLess { + color: #337AB7; + margin: 4px 0px; + text-transform: uppercase; + padding: 2px 0px; + cursor: pointer; + border-radius: 5px; + font-size: 11px; +} diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index c787c891bb..6ee0ec9d02 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -22,6 +22,8 @@ function(jobData, jobDataOptions, jobLabels, jobFinished, count, $scope, ParseTy var currentContext = 1; $scope.firstCounterFromSocket = -1; + $scope.explanationLimit = 150; + // if the user enters the page mid-run, reset the search to include a param // to only grab events less than the first counter from the websocket events toDestroy.push($scope.$watch('firstCounterFromSocket', function(counter) { diff --git a/awx/ui/client/src/job-results/job-results.partial.html b/awx/ui/client/src/job-results/job-results.partial.html index ded3071ed5..7f5d653bca 100644 --- a/awx/ui/client/src/job-results/job-results.partial.html +++ b/awx/ui/client/src/job-results/job-results.partial.html @@ -75,6 +75,22 @@ + +
+ +
+ {{task_detail | limitTo:explanationLimit}} + + ... + Show More + + Show Less +
+
+
@@ -98,33 +114,6 @@
- -
- -
- {{job.job_explanation}} -
-
{{ 'Previous Task Failed' | translate }} - - - - -
-
-
diff --git a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html index 9907c3df64..4cd0ec3d82 100644 --- a/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html +++ b/awx/ui/client/src/standard-out/inventory-sync/standard-out-inventory-sync.partial.html @@ -41,6 +41,19 @@
+ +
+
EXPLANATION
+
+ {{task_detail | limitTo:explanationLimit}} + + ... + Show More + + Show Less +
+
+
LICENSE ERROR
diff --git a/awx/ui/client/src/standard-out/standard-out.block.less b/awx/ui/client/src/standard-out/standard-out.block.less index 3ceb1f695e..bf0ca8727b 100644 --- a/awx/ui/client/src/standard-out/standard-out.block.less +++ b/awx/ui/client/src/standard-out/standard-out.block.less @@ -158,3 +158,13 @@ standard-out-log { .StandardOut-actionButton + a { margin-left: 15px; } + +.StandardOut-seeMoreLess { + color: #337AB7; + margin: 4px 0px; + text-transform: uppercase; + padding: 2px 0px; + cursor: pointer; + border-radius: 5px; + font-size: 11px; +} diff --git a/awx/ui/client/src/standard-out/standard-out.controller.js b/awx/ui/client/src/standard-out/standard-out.controller.js index 1b2743f4f8..97b85f68b2 100644 --- a/awx/ui/client/src/standard-out/standard-out.controller.js +++ b/awx/ui/client/src/standard-out/standard-out.controller.js @@ -12,7 +12,8 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, GetBasePath, Rest, ProcessErrors, Empty, GetChoices, LookUpName, - ParseTypeChange, ParseVariableString, RelaunchJob, DeleteJob, Wait, i18n) { + ParseTypeChange, ParseVariableString, RelaunchJob, DeleteJob, Wait, i18n, + fieldChoices, fieldLabels) { var job_id = $stateParams.id, jobType = $state.current.data.jobType; @@ -22,6 +23,8 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, $scope.stdoutFullScreen = false; $scope.toggleStdoutFullscreenTooltip = i18n._("Expand Output"); + $scope.explanationLimit = 150; + // Listen for job status updates that may come across via sockets. We need to check the payload // to see whethere the updated job is the one that we're currently looking at. $scope.$on(`ws-jobs`, function(e, data) { @@ -35,6 +38,37 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, } }); + $scope.previousTaskFailed = false; + + $scope.$watch('job.job_explanation', function(explanation) { + if (explanation && explanation.split(":")[0] === "Previous Task Failed") { + $scope.previousTaskFailed = true; + + var taskObj = JSON.parse(explanation.substring(explanation.split(":")[0].length + 1)); + // return a promise from the options request with the permission type choices (including adhoc) as a param + var fieldChoice = fieldChoices({ + $scope: $scope, + url: GetBasePath('unified_jobs'), + field: 'type' + }); + + // manipulate the choices from the options request to be set on + // scope and be usable by the list form + fieldChoice.then(function (choices) { + choices = + fieldLabels({ + choices: choices + }); + $scope.explanation_fail_type = choices[taskObj.job_type]; + $scope.explanation_fail_name = taskObj.job_name; + $scope.explanation_fail_id = taskObj.job_id; + $scope.task_detail = $scope.explanation_fail_type + " failed for " + $scope.explanation_fail_name + " with ID " + $scope.explanation_fail_id + "."; + }); + } else { + $scope.previousTaskFailed = false; + } + }); + // Set the parse type so that CodeMirror knows how to display extra params YAML/JSON $scope.parseType = 'yaml'; @@ -242,4 +276,5 @@ export function JobStdoutController ($rootScope, $scope, $state, $stateParams, JobStdoutController.$inject = [ '$rootScope', '$scope', '$state', '$stateParams', 'GetBasePath', 'Rest', 'ProcessErrors', 'Empty', 'GetChoices', 'LookUpName', 'ParseTypeChange', - 'ParseVariableString', 'RelaunchJob', 'DeleteJob', 'Wait', 'i18n']; + 'ParseVariableString', 'RelaunchJob', 'DeleteJob', 'Wait', 'i18n', + 'fieldChoices', 'fieldLabels']; From d615e2e9ff4ae63803b02c3a338271cfdf6585f9 Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 16 Aug 2017 08:30:32 -0400 Subject: [PATCH 004/138] do not include workflow jobs for reaping * Workflow jobs are virtual jobs that don't actually run. Thus they won't have a celery id and aren't candidates for the generic reaping. * Better error logging when Instance not found in reaping code. --- awx/main/scheduler/__init__.py | 10 +++++++--- .../tests/functional/task_management/test_scheduler.py | 9 +++++++-- awx/main/tests/unit/test_task_manager.py | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/awx/main/scheduler/__init__.py b/awx/main/scheduler/__init__.py index cdd9a55f3b..abc7c167ee 100644 --- a/awx/main/scheduler/__init__.py +++ b/awx/main/scheduler/__init__.py @@ -14,6 +14,7 @@ from django.db import transaction, connection, DatabaseError from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now as tz_now, utc from django.db.models import Q +from django.contrib.contenttypes.models import ContentType # AWX from awx.main.models import * # noqa @@ -78,8 +79,10 @@ class TaskManager(): def get_running_tasks(self): execution_nodes = {} now = tz_now() - jobs = UnifiedJob.objects.filter(Q(status='running') | - Q(status='waiting', modified__lte=now - timedelta(seconds=60))) + workflow_ctype_id = ContentType.objects.get_for_model(WorkflowJob).id + jobs = UnifiedJob.objects.filter((Q(status='running') | + Q(status='waiting', modified__lte=now - timedelta(seconds=60))) & + ~Q(polymorphic_ctype_id=workflow_ctype_id)) [execution_nodes.setdefault(j.execution_node, [j]).append(j) for j in jobs] return execution_nodes @@ -445,7 +448,8 @@ class TaskManager(): continue except Instance.DoesNotExist: logger.error("Execution node Instance {} not found in database. " - "The node is currently executing jobs {}".format(node, [str(j) for j in node_jobs])) + "The node is currently executing jobs {}".format(node, + [j.log_format for j in node_jobs])) active_tasks = [] for task in node_jobs: if (task.celery_task_id not in active_tasks and not hasattr(settings, 'IGNORE_CELERY_INSPECTOR')): diff --git a/awx/main/tests/functional/task_management/test_scheduler.py b/awx/main/tests/functional/task_management/test_scheduler.py index 335505a65e..01bb88d0a0 100644 --- a/awx/main/tests/functional/task_management/test_scheduler.py +++ b/awx/main/tests/functional/task_management/test_scheduler.py @@ -9,6 +9,7 @@ from awx.main.scheduler import TaskManager from awx.main.models import ( Job, Instance, + WorkflowJob, ) @@ -250,7 +251,9 @@ class TestReaper(): j11 = Job.objects.create(status='running', celery_task_id='host4_j11', execution_node='host4_offline') - js = [j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11] + j12 = WorkflowJob.objects.create(status='running', celery_task_id='workflow_job', execution_node='host1') + + js = [j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12] for j in js: j.save = mocker.Mock(wraps=j.save) j.websocket_emit_status = mocker.Mock() @@ -263,7 +266,7 @@ class TestReaper(): @pytest.fixture def running_tasks(self, all_jobs): return { - 'host1': all_jobs[2:5], + 'host1': all_jobs[2:5] + [all_jobs[11]], 'host2': all_jobs[5:8], 'host3_split': all_jobs[8:10], 'host4_offline': [all_jobs[10]], @@ -331,3 +334,5 @@ class TestReaper(): assert all_jobs[9] in execution_nodes_jobs['host3_split'] assert all_jobs[10] in execution_nodes_jobs['host4_offline'] + + assert all_jobs[11] not in execution_nodes_jobs['host1'] diff --git a/awx/main/tests/unit/test_task_manager.py b/awx/main/tests/unit/test_task_manager.py index fc0be720c8..b479952e53 100644 --- a/awx/main/tests/unit/test_task_manager.py +++ b/awx/main/tests/unit/test_task_manager.py @@ -31,8 +31,8 @@ class TestCleanupInconsistentCeleryTasks(): assert "mocked" in str(excinfo.value) logger_mock.error.assert_called_once_with("Execution node Instance host1 not found in database. " - "The node is currently executing jobs ['None-2-new', " - "'None-3-new']") + "The node is currently executing jobs ['job 2 (new)', " + "'job 3 (new)']") @mock.patch.object(cache, 'get', return_value=None) @mock.patch.object(TaskManager, 'get_active_tasks', return_value=([], {'host1': []})) From 2908a0c2bd6883fc471085651c670019987720be Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 16 Aug 2017 10:45:23 -0400 Subject: [PATCH 005/138] Changed smart host filter help text from RHEL to RedHat. Removed rogue console.log --- .../inventories/smart-inventory/smart-inventory.form.js | 2 +- .../job_templates/factories/callback-help-init.factory.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js index 4e10b6b6ab..2eb4f3b10a 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.form.js @@ -65,7 +65,7 @@ export default ['i18n', 'InventoryCompletedJobsList', function(i18n, InventoryCo label: i18n._('Smart Host Filter'), type: 'custom', control: '', - awPopOver: "

" + i18n._("Populate the hosts for this inventory by using a search filter.") + "

" + i18n._("Example: ansible_facts.ansible_distribution:\"RHEL\"") + "

" + i18n._("Refer to the Ansible Tower documentation for further syntax and examples.") + "

", + awPopOver: "

" + i18n._("Populate the hosts for this inventory by using a search filter.") + "

" + i18n._("Example: ansible_facts.ansible_distribution:\"RedHat\"") + "

" + i18n._("Refer to the Ansible Tower documentation for further syntax and examples.") + "

", dataTitle: i18n._('Smart Host Filter'), dataPlacement: 'right', dataContainer: 'body', diff --git a/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js b/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js index 5be4d4fc55..d0eada857c 100644 --- a/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js +++ b/awx/ui/client/src/templates/job_templates/factories/callback-help-init.factory.js @@ -160,7 +160,7 @@ export default scope.selectedCredentials = selectedCredentials; scope.credential_types = credTypes; scope.credentialTypeOptions = credTypeOptions; - scope.credentialsToPost = credTags;console.log(credTags); + scope.credentialsToPost = credTags; scope.$emit('jobTemplateLoaded', master); }); } From 1b6e4af9a7e00501ba0ea370da5de244b416cdce Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Wed, 16 Aug 2017 12:32:41 -0400 Subject: [PATCH 006/138] Default value of preview multiple select dropdown should be read-only --- .../templates/survey-maker/render/multiselect.directive.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/awx/ui/client/src/templates/survey-maker/render/multiselect.directive.js b/awx/ui/client/src/templates/survey-maker/render/multiselect.directive.js index 5c88e610ac..82a39bbd54 100644 --- a/awx/ui/client/src/templates/survey-maker/render/multiselect.directive.js +++ b/awx/ui/client/src/templates/survey-maker/render/multiselect.directive.js @@ -15,6 +15,13 @@ var directive = { require: 'ngModel', + controller: function($scope) { + $('select').on('select2:unselecting', (event) => { + if (_.has($scope.$parent, 'preview')) { + event.preventDefault(); + } + }); + }, compile: function() { return { pre: function(scope, element, attrs, ngModel) { From 1df47a2ddde607dcc7e7a9c6938b49f25067513d Mon Sep 17 00:00:00 2001 From: Chris Meyers Date: Wed, 16 Aug 2017 13:18:25 -0400 Subject: [PATCH 007/138] account for waiting tasks not having execution_nodes yet * Reap running tasks on non-netsplit nodes * Reap running tasks on known to be offline nodes * Reap waiting tasks with no celery id anywhere if waiting >= 60 seconds --- awx/main/scheduler/__init__.py | 59 ++++++++++++------- .../task_management/test_scheduler.py | 39 ++++++------ awx/main/tests/unit/test_task_manager.py | 4 +- 3 files changed, 61 insertions(+), 41 deletions(-) diff --git a/awx/main/scheduler/__init__.py b/awx/main/scheduler/__init__.py index abc7c167ee..2337170091 100644 --- a/awx/main/scheduler/__init__.py +++ b/awx/main/scheduler/__init__.py @@ -78,13 +78,18 @@ class TaskManager(): ''' def get_running_tasks(self): execution_nodes = {} + waiting_jobs = [] now = tz_now() workflow_ctype_id = ContentType.objects.get_for_model(WorkflowJob).id jobs = UnifiedJob.objects.filter((Q(status='running') | Q(status='waiting', modified__lte=now - timedelta(seconds=60))) & ~Q(polymorphic_ctype_id=workflow_ctype_id)) - [execution_nodes.setdefault(j.execution_node, [j]).append(j) for j in jobs] - return execution_nodes + for j in jobs: + if j.execution_node: + execution_nodes.setdefault(j.execution_node, [j]).append(j) + else: + waiting_jobs.append(j) + return (execution_nodes, waiting_jobs) ''' Tasks that are currently running in celery @@ -410,6 +415,27 @@ class TaskManager(): if not found_acceptable_queue: logger.debug("%s couldn't be scheduled on graph, waiting for next cycle", task.log_format) + def fail_jobs_if_not_in_celery(self, node_jobs, active_tasks, celery_task_start_time): + for task in node_jobs: + if (task.celery_task_id not in active_tasks and not hasattr(settings, 'IGNORE_CELERY_INSPECTOR')): + if isinstance(task, WorkflowJob): + continue + if task.modified > celery_task_start_time: + continue + task.status = 'failed' + task.job_explanation += ' '.join(( + 'Task was marked as running in Tower but was not present in', + 'Celery, so it has been marked as failed.', + )) + try: + task.save(update_fields=['status', 'job_explanation']) + except DatabaseError: + logger.error("Task {} DB error in marking failed. Job possibly deleted.".format(task.log_format)) + continue + awx_tasks._send_notification_templates(task, 'failed') + task.websocket_emit_status('failed') + logger.error("Task {} has no record in celery. Marking as failed".format(task.log_format)) + def cleanup_inconsistent_celery_tasks(self): ''' Rectify tower db <-> celery inconsistent view of jobs state @@ -431,7 +457,13 @@ class TaskManager(): Only consider failing tasks on instances for which we obtained a task list from celery for. ''' - running_tasks = self.get_running_tasks() + running_tasks, waiting_tasks = self.get_running_tasks() + all_celery_task_ids = [] + for node, node_jobs in active_queues.iteritems(): + all_celery_task_ids.extend(node_jobs) + + self.fail_jobs_if_not_in_celery(waiting_tasks, all_celery_task_ids, celery_task_start_time) + for node, node_jobs in running_tasks.iteritems(): if node in active_queues: active_tasks = active_queues[node] @@ -451,25 +483,8 @@ class TaskManager(): "The node is currently executing jobs {}".format(node, [j.log_format for j in node_jobs])) active_tasks = [] - for task in node_jobs: - if (task.celery_task_id not in active_tasks and not hasattr(settings, 'IGNORE_CELERY_INSPECTOR')): - if isinstance(task, WorkflowJob): - continue - if task.modified > celery_task_start_time: - continue - task.status = 'failed' - task.job_explanation += ' '.join(( - 'Task was marked as running in Tower but was not present in', - 'Celery, so it has been marked as failed.', - )) - try: - task.save(update_fields=['status', 'job_explanation']) - except DatabaseError: - logger.error("Task {} DB error in marking failed. Job possibly deleted.".format(task.log_format)) - continue - awx_tasks._send_notification_templates(task, 'failed') - task.websocket_emit_status('failed') - logger.error("Task {} has no record in celery. Marking as failed".format(task.log_format)) + + self.fail_jobs_if_not_in_celery(node_jobs, active_tasks, celery_task_start_time) def calculate_capacity_used(self, tasks): for rampart_group in self.graph: diff --git a/awx/main/tests/functional/task_management/test_scheduler.py b/awx/main/tests/functional/task_management/test_scheduler.py index 01bb88d0a0..05de8b2a81 100644 --- a/awx/main/tests/functional/task_management/test_scheduler.py +++ b/awx/main/tests/functional/task_management/test_scheduler.py @@ -231,23 +231,23 @@ class TestReaper(): Instance.objects.create(hostname='host4_offline', capacity=0) j1 = Job.objects.create(status='pending', execution_node='host1') - j2 = Job.objects.create(status='waiting', celery_task_id='considered_j2', execution_node='host1') - j3 = Job.objects.create(status='waiting', celery_task_id='considered_j3', execution_node='host1') + j2 = Job.objects.create(status='waiting', celery_task_id='considered_j2') + j3 = Job.objects.create(status='waiting', celery_task_id='considered_j3') j3.modified = now - timedelta(seconds=60) j3.save(update_fields=['modified']) j4 = Job.objects.create(status='running', celery_task_id='considered_j4', execution_node='host1') - j5 = Job.objects.create(status='waiting', celery_task_id='reapable_j5', execution_node='host1') + j5 = Job.objects.create(status='waiting', celery_task_id='reapable_j5') j5.modified = now - timedelta(seconds=60) j5.save(update_fields=['modified']) - j6 = Job.objects.create(status='waiting', celery_task_id='considered_j6', execution_node='host2') + j6 = Job.objects.create(status='waiting', celery_task_id='considered_j6') j6.modified = now - timedelta(seconds=60) j6.save(update_fields=['modified']) j7 = Job.objects.create(status='running', celery_task_id='considered_j7', execution_node='host2') j8 = Job.objects.create(status='running', celery_task_id='reapable_j7', execution_node='host2') - j9 = Job.objects.create(status='waiting', celery_task_id='host3_j8', execution_node='host3_split') + j9 = Job.objects.create(status='waiting', celery_task_id='reapable_j8') j9.modified = now - timedelta(seconds=60) j9.save(update_fields=['modified']) - j10 = Job.objects.create(status='running', execution_node='host3_split') + j10 = Job.objects.create(status='running', celery_task_id='host3_j10', execution_node='host3_split') j11 = Job.objects.create(status='running', celery_task_id='host4_j11', execution_node='host4_offline') @@ -266,12 +266,16 @@ class TestReaper(): @pytest.fixture def running_tasks(self, all_jobs): return { - 'host1': all_jobs[2:5] + [all_jobs[11]], - 'host2': all_jobs[5:8], - 'host3_split': all_jobs[8:10], + 'host1': [all_jobs[3]], + 'host2': [all_jobs[7], all_jobs[8]], + 'host3_split': [all_jobs[9]], 'host4_offline': [all_jobs[10]], } + @pytest.fixture + def waiting_tasks(self, all_jobs): + return [all_jobs[2], all_jobs[4], all_jobs[5], all_jobs[8]] + @pytest.fixture def reapable_jobs(self, all_jobs): return [all_jobs[4], all_jobs[7], all_jobs[10]] @@ -290,10 +294,10 @@ class TestReaper(): @pytest.mark.django_db @mock.patch('awx.main.tasks._send_notification_templates') @mock.patch.object(TaskManager, 'get_active_tasks', lambda self: ([], [])) - def test_cleanup_inconsistent_task(self, notify, active_tasks, considered_jobs, reapable_jobs, running_tasks, mocker): + def test_cleanup_inconsistent_task(self, notify, active_tasks, considered_jobs, reapable_jobs, running_tasks, waiting_tasks, mocker): tm = TaskManager() - tm.get_running_tasks = mocker.Mock(return_value=running_tasks) + tm.get_running_tasks = mocker.Mock(return_value=(running_tasks, waiting_tasks)) tm.get_active_tasks = mocker.Mock(return_value=active_tasks) tm.cleanup_inconsistent_celery_tasks() @@ -302,7 +306,7 @@ class TestReaper(): if j not in reapable_jobs: j.save.assert_not_called() - assert notify.call_count == 3 + assert notify.call_count == 4 notify.assert_has_calls([mock.call(j, 'failed') for j in reapable_jobs], any_order=True) for j in reapable_jobs: @@ -317,22 +321,23 @@ class TestReaper(): tm = TaskManager() # Ensure the query grabs the expected jobs - execution_nodes_jobs = tm.get_running_tasks() + execution_nodes_jobs, waiting_jobs = tm.get_running_tasks() assert 'host1' in execution_nodes_jobs assert 'host2' in execution_nodes_jobs assert 'host3_split' in execution_nodes_jobs - assert all_jobs[2] in execution_nodes_jobs['host1'] assert all_jobs[3] in execution_nodes_jobs['host1'] - assert all_jobs[4] in execution_nodes_jobs['host1'] - assert all_jobs[5] in execution_nodes_jobs['host2'] assert all_jobs[6] in execution_nodes_jobs['host2'] assert all_jobs[7] in execution_nodes_jobs['host2'] - assert all_jobs[8] in execution_nodes_jobs['host3_split'] assert all_jobs[9] in execution_nodes_jobs['host3_split'] assert all_jobs[10] in execution_nodes_jobs['host4_offline'] assert all_jobs[11] not in execution_nodes_jobs['host1'] + + assert all_jobs[2] in waiting_jobs + assert all_jobs[4] in waiting_jobs + assert all_jobs[5] in waiting_jobs + assert all_jobs[8] in waiting_jobs diff --git a/awx/main/tests/unit/test_task_manager.py b/awx/main/tests/unit/test_task_manager.py index b479952e53..f76e77862b 100644 --- a/awx/main/tests/unit/test_task_manager.py +++ b/awx/main/tests/unit/test_task_manager.py @@ -19,7 +19,7 @@ from django.core.cache import cache class TestCleanupInconsistentCeleryTasks(): @mock.patch.object(cache, 'get', return_value=None) @mock.patch.object(TaskManager, 'get_active_tasks', return_value=([], {})) - @mock.patch.object(TaskManager, 'get_running_tasks', return_value={'host1': [Job(id=2), Job(id=3),]}) + @mock.patch.object(TaskManager, 'get_running_tasks', return_value=({'host1': [Job(id=2), Job(id=3),]}, [])) @mock.patch.object(InstanceGroup.objects, 'all', return_value=[]) @mock.patch.object(Instance.objects, 'get', side_effect=Instance.DoesNotExist) @mock.patch('awx.main.scheduler.logger') @@ -43,7 +43,7 @@ class TestCleanupInconsistentCeleryTasks(): logger_mock.error = mock.MagicMock() job = Job(id=2, modified=tz_now(), status='running', celery_task_id='blah', execution_node='host1') job.websocket_emit_status = mock.MagicMock() - get_running_tasks.return_value = {'host1': [job]} + get_running_tasks.return_value = ({'host1': [job]}, []) tm = TaskManager() with mock.patch.object(job, 'save', side_effect=DatabaseError): From ad4b1650e3c041857ea4f30855a25649e2344a4c Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Wed, 16 Aug 2017 14:19:07 -0400 Subject: [PATCH 008/138] Generalize empty SurveyMaker prompt text --- awx/ui/client/src/partials/survey-maker-modal.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/partials/survey-maker-modal.html b/awx/ui/client/src/partials/survey-maker-modal.html index d88931fce4..41b9777138 100644 --- a/awx/ui/client/src/partials/survey-maker-modal.html +++ b/awx/ui/client/src/partials/survey-maker-modal.html @@ -42,7 +42,7 @@
PREVIEW
-
PLEASE ADD A SURVEY PROMPT ON THE LEFT.
+
PLEASE ADD A SURVEY PROMPT.
  • From dfc4070dba362b7a7ea6a5b7547f8836a4fca43f Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 16 Aug 2017 13:59:18 -0400 Subject: [PATCH 009/138] Fixed some delete workflow node bugs --- .../workflow-maker.controller.js | 85 +++++++++++++++---- .../templates/workflows/workflow.service.js | 2 - 2 files changed, 70 insertions(+), 17 deletions(-) diff --git a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js index 428ada820a..f511c7a297 100644 --- a/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js +++ b/awx/ui/client/src/templates/workflows/workflow-maker/workflow-maker.controller.js @@ -183,11 +183,13 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }) .then(function(data) { - $scope.associateRequests.push({ - parentId: params.parentId, - nodeId: data.data.id, - edge: params.node.edgeType - }); + if(!params.node.isRoot) { + $scope.associateRequests.push({ + parentId: params.parentId, + nodeId: data.data.id, + edge: params.node.edgeType + }); + } params.node.isNew = false; continueRecursing(data.data.id); @@ -370,7 +372,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', edgeType = "always"; $scope.edgeFlags.showTypeOptions = false; } else { - if ((_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && _.includes(siblingConnectionTypes, "always")) { + if ($scope.placeholderNode.edgeConflict) { // This is a conflicted scenario but we'll just let the user keep building - they will have to remediate before saving $scope.edgeFlags.typeRestriction = null; } else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) { @@ -644,12 +646,12 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', // a type as this node will always be executed $scope.edgeFlags.showTypeOptions = false; } else { - if ((_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) && _.includes(siblingConnectionTypes, "always")) { + if (nodeToEdit.edgeConflict) { // This is a conflicted scenario but we'll just let the user keep building - they will have to remediate before saving $scope.edgeFlags.typeRestriction = null; - } else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure") && (nodeToEdit.edgeType === "success" || nodeToEdit.edgeType === "failure")) { + } else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) { $scope.edgeFlags.typeRestriction = "successFailure"; - } else if (_.includes(siblingConnectionTypes, "always") && nodeToEdit.edgeType === "always") { + } else if (_.includes(siblingConnectionTypes, "always")) { $scope.edgeFlags.typeRestriction = "always"; } else { $scope.edgeFlags.typeRestriction = null; @@ -759,12 +761,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', nodeToBeDeleted: $scope.nodeToBeDeleted }); - if($scope.workflowMakerFormConfig.nodeMode === "add") { - if($scope.placeholderNode.isRoot) { - $scope.edgeFlags.showTypeOptions = false; - } - } - if ($scope.nodeToBeDeleted.isNew !== true) { $scope.treeData.data.deletedNodes.push($scope.nodeToBeDeleted.nodeId); } @@ -780,6 +776,65 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.$broadcast("refreshWorkflowChart"); + if($scope.placeholderNode) { + let edgeType = "success"; + if($scope.placeholderNode.isRoot) { + $scope.edgeFlags.showTypeOptions = false; + edgeType = "always"; + } + else { + // we need to update the possible edges based on any new siblings + let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({ + tree: $scope.treeData.data, + parentId: $scope.placeholderNode.parent.id, + childId: $scope.placeholderNode.id + }); + + if ($scope.placeholderNode.edgeConflict) { + // This is a conflicted scenario but we'll just let the user keep building - they will have to remediate before saving + $scope.edgeFlags.typeRestriction = null; + } else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) { + $scope.edgeFlags.typeRestriction = "successFailure"; + } else if (_.includes(siblingConnectionTypes, "always")) { + $scope.edgeFlags.typeRestriction = "always"; + edgeType = "always"; + } else { + $scope.edgeFlags.typeRestriction = null; + } + + $scope.edgeFlags.showTypeOptions = true; + + } + $scope.$broadcast("setEdgeType", edgeType); + } + else if($scope.nodeBeingEdited) { + if($scope.nodeBeingEdited.isRoot) { + $scope.edgeFlags.showTypeOptions = false; + } + else { + let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({ + tree: $scope.treeData.data, + parentId: $scope.nodeBeingEdited.parent.id, + childId: $scope.nodeBeingEdited.id + }); + + if ($scope.nodeBeingEdited.edgeConflict) { + // This is a conflicted scenario but we'll just let the user keep building - they will have to remediate before saving + $scope.edgeFlags.typeRestriction = null; + } else if (_.includes(siblingConnectionTypes, "success") || _.includes(siblingConnectionTypes, "failure")) { + $scope.edgeFlags.typeRestriction = "successFailure"; + } else if (_.includes(siblingConnectionTypes, "always") && $scope.nodeBeingEdited.edgeType === "always") { + $scope.edgeFlags.typeRestriction = "always"; + } else { + $scope.edgeFlags.typeRestriction = null; + } + + $scope.edgeFlags.showTypeOptions = true; + + } + $scope.$broadcast("setEdgeType", $scope.nodeBeingEdited.edgeType); + } + $scope.treeData.data.totalNodes--; } diff --git a/awx/ui/client/src/templates/workflows/workflow.service.js b/awx/ui/client/src/templates/workflows/workflow.service.js index 293672045e..5ac2998a84 100644 --- a/awx/ui/client/src/templates/workflows/workflow.service.js +++ b/awx/ui/client/src/templates/workflows/workflow.service.js @@ -45,9 +45,7 @@ export default ['$q', function($q){ child.isRoot = true; child.edgeType = "always"; } - child.parent = parentNode; - parentNode.children.push(child); }); } From 116a47fc6a1207702bdd3fa2090b5314928c4bd3 Mon Sep 17 00:00:00 2001 From: mabashian Date: Wed, 16 Aug 2017 15:53:48 -0400 Subject: [PATCH 010/138] Filter credentials to only credentials not associated with an organization when adding permissions for a user not associated with an organization. --- awx/ui/client/src/shared/stateDefinitions.factory.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/awx/ui/client/src/shared/stateDefinitions.factory.js b/awx/ui/client/src/shared/stateDefinitions.factory.js index 78b9c95805..975dfb8416 100644 --- a/awx/ui/client/src/shared/stateDefinitions.factory.js +++ b/awx/ui/client/src/shared/stateDefinitions.factory.js @@ -357,6 +357,9 @@ function($injector, $stateExtender, $log, i18n) { $stateParams[`${list.iterator}_search`].or__organization__in = orgIds.join(); } + else { + $stateParams[`${list.iterator}_search`].organization = 'null'; + } return qs.search(path, $stateParams[`${list.iterator}_search`]); }); From 67c25570f38f7fa454cff1e85bcda377bfe50c34 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Wed, 16 Aug 2017 17:04:17 -0400 Subject: [PATCH 011/138] Ensure Smart Inventory hosts are assigned job history --- awx/main/models/jobs.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 60663a8087..18202e18a9 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -1185,30 +1185,48 @@ class JobEvent(CreatedModifiedModel): parent = parent[0] parent._update_hosts(qs.values_list('id', flat=True)) - def _update_host_summary_from_stats(self): - from awx.main.models.inventory import Host + def _hostnames(self): hostnames = set() try: for stat in ('changed', 'dark', 'failures', 'ok', 'processed', 'skipped'): hostnames.update(self.event_data.get(stat, {}).keys()) - except AttributeError: # In case event_data or v isn't a dict. + except AttributeError: # In case event_data or v isn't a dict. pass + return hostnames + + def _update_smart_inventory_hosts(self, hostnames): + '''If the job the job_event is for was run using a Smart Inventory + update the hosts fields related to job history and summary. + ''' with ignore_inventory_computed_fields(): - qs = Host.objects.filter(inventory__jobs__id=self.job_id, - name__in=hostnames) + if hasattr(self.job, 'inventory') and self.job.inventory.kind == 'smart': + logger.debug(self.job.inventory) + smart_hosts = self.job.inventory.hosts.filter(name__in=hostnames) + for smart_host in smart_hosts: + host_summary = self.job.job_host_summaries.get(host_name=smart_host.name) + smart_host.inventory.jobs.add(self.job) + smart_host.last_job_id = self.job_id + smart_host.last_job_host_summary_id = host_summary.pk + smart_host.save() + + def _update_host_summary_from_stats(self, hostnames): + with ignore_inventory_computed_fields(): + from awx.main.models.inventory import Host + qs = Host.objects.filter(inventory__jobs__id=self.job_id, name__in=hostnames) job = self.job for host in hostnames: host_stats = {} for stat in ('changed', 'dark', 'failures', 'ok', 'processed', 'skipped'): try: host_stats[stat] = self.event_data.get(stat, {}).get(host, 0) - except AttributeError: # in case event_data[stat] isn't a dict. + except AttributeError: # in case event_data[stat] isn't a dict. pass if qs.filter(name=host).exists(): host_actual = qs.get(name=host) host_summary, created = job.job_host_summaries.get_or_create(host=host_actual, host_name=host_actual.name, defaults=host_stats) else: host_summary, created = job.job_host_summaries.get_or_create(host_name=host, defaults=host_stats) + if not created: update_fields = [] for stat, value in host_stats.items(): @@ -1217,8 +1235,6 @@ class JobEvent(CreatedModifiedModel): update_fields.append(stat) if update_fields: host_summary.save(update_fields=update_fields) - job.inventory.update_computed_fields() - emit_channel_notification('jobs-summary', dict(group_name='jobs', unified_job_id=job.id)) def save(self, *args, **kwargs): from awx.main.models.inventory import Host @@ -1249,7 +1265,13 @@ class JobEvent(CreatedModifiedModel): self._update_hosts() if self.event == 'playbook_on_stats': self._update_parents_failed_and_changed() - self._update_host_summary_from_stats() + + hostnames = self._hostnames() + self._update_host_summary_from_stats(hostnames) + self._update_smart_inventory_hosts(hostnames) + self.job.inventory.update_computed_fields() + + emit_channel_notification('jobs-summary', dict(group_name='jobs', unified_job_id=self.job.id)) @classmethod def create_from_data(self, **kwargs): From 1edc688acbdc491c08db7bdbb0397f70d1415d30 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Wed, 16 Aug 2017 16:48:35 -0700 Subject: [PATCH 012/138] adding translation directive in places --- .../client/lib/components/input/label.partial.html | 6 +++--- .../src/shared/column-sort/column-sort.partial.html | 4 ++-- awx/ui/client/src/shared/directives.js | 2 +- awx/ui/client/src/shared/form-generator.js | 12 ++++++------ awx/ui/client/src/shared/generator-helpers.js | 2 +- .../shared/list-generator/list-actions.partial.html | 8 ++++---- .../shared/list-generator/list-generator.factory.js | 12 ++++++------ 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/awx/ui/client/lib/components/input/label.partial.html b/awx/ui/client/lib/components/input/label.partial.html index e5c79544d5..537fd89fb5 100644 --- a/awx/ui/client/lib/components/input/label.partial.html +++ b/awx/ui/client/lib/components/input/label.partial.html @@ -1,11 +1,11 @@
`; } for (itm in this.form.relatedButtons) { diff --git a/awx/ui/client/src/shared/generator-helpers.js b/awx/ui/client/src/shared/generator-helpers.js index e2fffd2581..f25caa0689 100644 --- a/awx/ui/client/src/shared/generator-helpers.js +++ b/awx/ui/client/src/shared/generator-helpers.js @@ -744,7 +744,7 @@ angular.module('GeneratorHelpers', [systemStatus.name]) html += (options.ngHide) ? "ng-hide=\"" + options.ngHide + "\" " : ""; html += (options.awFeature) ? "aw-feature=\"" + options.awFeature + "\" " : ""; html += '>'; - html += ''; + html += ''; html += (options.buttonContent) ? options.buttonContent : ""; html += ''; html += ''; diff --git a/awx/ui/client/src/shared/list-generator/list-actions.partial.html b/awx/ui/client/src/shared/list-generator/list-actions.partial.html index a837b52ef0..bf3265f32f 100644 --- a/awx/ui/client/src/shared/list-generator/list-actions.partial.html +++ b/awx/ui/client/src/shared/list-generator/list-actions.partial.html @@ -4,7 +4,7 @@
- +
@@ -57,7 +57,7 @@ ng-show="{{options.ngShow}}" toolbar="true" aw-feature="{{options.awFeature}}"> - + diff --git a/awx/ui/client/src/shared/list-generator/list-generator.factory.js b/awx/ui/client/src/shared/list-generator/list-generator.factory.js index 64bd243d72..68a3760823 100644 --- a/awx/ui/client/src/shared/list-generator/list-generator.factory.js +++ b/awx/ui/client/src/shared/list-generator/list-generator.factory.js @@ -135,9 +135,9 @@ export default ['$compile', 'Attr', 'Icon', // Don't display an empty
if there is no listTitle if ((options.title !== false && list.title !== false) && list.listTitle !== undefined) { html += "
"; - html += "
"; + html += "
"; if (list.listTitle && options.listTitle !== false) { - html += "
" + list.listTitle + "
"; + html += "
" + list.listTitle + "
"; // We want to show the list title badge by default and only hide it when the list config specifically passes a false flag list.listTitleBadge = (typeof list.listTitleBadge === 'boolean' && list.listTitleBadge === false) ? false : true; if (list.listTitleBadge) { @@ -506,13 +506,13 @@ export default ['$compile', 'Attr', 'Icon', html = "\n"; html += "\n"; if (list.index) { - html += "#\n"; + html += "#\n"; } if (list.multiSelect) { html += buildSelectAll().prop('outerHTML'); } else if (options.mode === 'lookup') { - html += ""; + html += ""; } if (options.mode !== 'lookup'){ @@ -565,11 +565,11 @@ export default ['$compile', 'Attr', 'Icon', } } if (options.mode === 'select') { - html += "Select"; + html += "Select"; } else if (options.mode === 'edit' && list.fieldActions) { html += ""; + html += "\" translate>"; html += (list.fieldActions.label === undefined || list.fieldActions.label) ? i18n._("Actions") : ""; html += "\n"; } From 7ac2376a015d069609c875f3efee0acdbca47169 Mon Sep 17 00:00:00 2001 From: Shane McDonald Date: Wed, 16 Aug 2017 22:31:15 -0400 Subject: [PATCH 013/138] Make parent image tag for unit tests a build arg --- tools/docker-compose/unit-tests/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/docker-compose/unit-tests/Dockerfile b/tools/docker-compose/unit-tests/Dockerfile index 9398c89398..15d8d25c22 100644 --- a/tools/docker-compose/unit-tests/Dockerfile +++ b/tools/docker-compose/unit-tests/Dockerfile @@ -1,4 +1,5 @@ -FROM gcr.io/ansible-tower-engineering/awx_devel:latest +ARG TAG=latest +FROM gcr.io/ansible-tower-engineering/awx_devel:$TAG # For UI tests RUN yum install -y bzip2 gcc-c++ From 66a8170bbcafb970ac6fa644dee039b18ee82826 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 17 Aug 2017 10:33:10 -0400 Subject: [PATCH 014/138] basic API optimization of labels list --- awx/main/access.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index bf722c0e2c..8842336ffb 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -2170,10 +2170,13 @@ class LabelAccess(BaseAccess): def get_queryset(self): if self.user.is_superuser or self.user.is_system_auditor: - return self.model.objects.all() - return self.model.objects.all().filter( - organization__in=Organization.accessible_objects(self.user, 'read_role') - ) + qs = self.model.objects.all() + else: + qs = self.model.objects.all().filter( + organization__in=Organization.accessible_pk_qs(self.user, 'read_role') + ) + qs = qs.prefetch_related('modified_by', 'created_by', 'organization') + return qs @check_superuser def can_read(self, obj): From 395779781584bc121506bf815a36d02f665fc3e3 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Thu, 17 Aug 2017 12:16:21 -0400 Subject: [PATCH 015/138] fix scheduler inner panel border --- awx/ui/client/src/scheduler/schedulerFormDetail.block.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/scheduler/schedulerFormDetail.block.less b/awx/ui/client/src/scheduler/schedulerFormDetail.block.less index 0b6b63e2ad..e05bbb89d3 100644 --- a/awx/ui/client/src/scheduler/schedulerFormDetail.block.less +++ b/awx/ui/client/src/scheduler/schedulerFormDetail.block.less @@ -2,7 +2,7 @@ .SchedulerFormDetail-container { padding: 15px; - border: 1px solid @default-border; + border: 1px solid @b7grey; border-radius: 5px; margin-bottom: 20px; } From 1944c7fbd74c086ff06d90645933a5abc438858b Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Thu, 17 Aug 2017 13:04:25 -0700 Subject: [PATCH 016/138] Fixing host-event's stdout/stdout ng-if logic and fixing small issue with resizing the host event modal --- .../host-event/host-event-stderr.partial.html | 4 -- .../host-event/host-event-stdout.partial.html | 6 +-- .../host-event/host-event.controller.js | 39 ++++++++++++++----- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/awx/ui/client/src/job-results/host-event/host-event-stderr.partial.html b/awx/ui/client/src/job-results/host-event/host-event-stderr.partial.html index 0a9e84a137..e775d17681 100644 --- a/awx/ui/client/src/job-results/host-event/host-event-stderr.partial.html +++ b/awx/ui/client/src/job-results/host-event/host-event-stderr.partial.html @@ -3,10 +3,6 @@
1
-
- 2 -
-
diff --git a/awx/ui/client/src/job-results/host-event/host-event-stdout.partial.html b/awx/ui/client/src/job-results/host-event/host-event-stdout.partial.html index 1f67a22cc1..96df87b163 100644 --- a/awx/ui/client/src/job-results/host-event/host-event-stdout.partial.html +++ b/awx/ui/client/src/job-results/host-event/host-event-stdout.partial.html @@ -1,12 +1,8 @@ -
+
1
-
- 2 -
-
diff --git a/awx/ui/client/src/job-results/host-event/host-event.controller.js b/awx/ui/client/src/job-results/host-event/host-event.controller.js index bec557e108..562db441e8 100644 --- a/awx/ui/client/src/job-results/host-event/host-event.controller.js +++ b/awx/ui/client/src/job-results/host-event/host-event.controller.js @@ -24,10 +24,6 @@ }); editor.setSize("100%", 200); editor.getDoc().setValue(data); - $('.modal-dialog').on('resize', function(){ - let height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120; - editor.setSize("100%", height); - }); }; /*ignore jslint end*/ $scope.isActiveState = function(name){ @@ -53,11 +49,15 @@ $scope.event = _.cloneDeep(hostEvent); // grab standard out & standard error if present from the host - // event's "res" object, for things like Ansible modules + // event's "res" object, for things like Ansible modules. Small + // wrinkle in this implementation is that the stdout/stderr tabs + // should be shown if the `res` object has stdout/stderr keys, even + // if they're a blank string. The presence of these keys is + // potentially significant to a user. try{ $scope.module_name = hostEvent.event_data.task_action || "No result found"; - $scope.stdout = (hostEvent.event_data.res.stdout === "" || " ") ? undefined : hostEvent.event_data.res.stdout; - $scope.stderr = (hostEvent.event_data.res.stderr === "" || " ") ? undefined : hostEvent.event_data.res.stderr; + $scope.stdout = hostEvent.event_data.res.stdout ? hostEvent.event_data.res.stdout : hostEvent.event_data.res.stdout === "" ? " " : undefined; + $scope.stderr = hostEvent.event_data.res.stderr ? hostEvent.event_data.res.stderr : hostEvent.event_data.res.stderr === "" ? " " : undefined; $scope.json = hostEvent.event_data.res; } catch(err){ @@ -78,6 +78,7 @@ try{ if(_.has(hostEvent.event_data, "res")){ initCodeMirror('HostEvent-codemirror', JSON.stringify($scope.json, null, 4), {name: "javascript", json: true}); + resize(); } else{ $scope.no_json = true; @@ -90,7 +91,7 @@ } else if ($state.current.name === 'jobResult.host-event.stdout'){ try{ - initCodeMirror('HostEvent-codemirror', $scope.stdout, 'shell'); + resize(); } catch(err){ // element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController @@ -98,7 +99,7 @@ } else if ($state.current.name === 'jobResult.host-event.stderr'){ try{ - initCodeMirror('HostEvent-codemirror', $scope.stderr, 'shell'); + resize(); } catch(err){ // element with id HostEvent-codemirror is not the view controlled by this instance of HostEventController @@ -113,6 +114,26 @@ cancel: '.CodeMirror' }); + function resize(){ + if ($state.current.name === 'jobResult.host-event.json'){ + let editor = $('.CodeMirror')[0].CodeMirror; + let height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120; + editor.setSize("100%", height); + } + else if($state.current.name === 'jobResult.host-event.stdout' || $state.current.name === 'jobResult.host-event.stderr'){ + let height = $('.modal-dialog').height() - $('.HostEvent-header').height() - $('.HostEvent-details').height() - $('.HostEvent-nav').height() - $('.HostEvent-controls').height() - 120; + $(".HostEvent-stdout").width("100%"); + $(".HostEvent-stdout").height(height); + $(".HostEvent-stdoutContainer").height(height); + $(".HostEvent-numberColumnPreload").height(height); + } + + } + + $('.modal-dialog').on('resize', function(){ + resize(); + }); + $('#HostEvent').on('hidden.bs.modal', function () { $scope.closeHostEvent(); }); From 9254bcaf169a126b7ff1e7d76b0fa18b34c861fe Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 17 Aug 2017 17:18:41 -0400 Subject: [PATCH 017/138] Make cache compatible with encrypted settings This saves the id value of the setting into the cache if the setting is encrypted. That can then be combined with the secret_key in order to decrypt the setting, without having to make an additional query to the database. --- awx/conf/models.py | 4 ++++ awx/conf/settings.py | 15 ++++++++++++++- awx/conf/tests/unit/test_settings.py | 20 +++++++++++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/awx/conf/models.py b/awx/conf/models.py index afdacb1757..7dae8bc77e 100644 --- a/awx/conf/models.py +++ b/awx/conf/models.py @@ -74,6 +74,10 @@ class Setting(CreatedModifiedModel): def get_cache_key(self, key): return key + @classmethod + def get_cache_id_key(self, key): + return '{}_ID'.format(key) + import awx.conf.signals # noqa diff --git a/awx/conf/settings.py b/awx/conf/settings.py index 89bfa0660a..d288ba3a16 100644 --- a/awx/conf/settings.py +++ b/awx/conf/settings.py @@ -124,9 +124,12 @@ class EncryptedCacheProxy(object): if value is not empty and self.registry.is_setting_encrypted(key): # If the setting exists in the database, we'll use its primary key # as part of the AES key when encrypting/decrypting + obj_id = self.cache.get(Setting.get_cache_id_key(key), default=empty) + if obj_id is empty: + obj_id = getattr(self._get_setting_from_db(key), 'pk', None) return method( TransientSetting( - pk=getattr(self._get_setting_from_db(key), 'pk', None), + pk=obj_id, value=value ), 'value' @@ -241,11 +244,13 @@ class SettingsWrapper(UserSettingsHolder): # to indicate from the cache that the setting is not configured without # a database lookup. settings_to_cache = get_settings_to_cache(self.registry) + setting_ids = {} # Load all settings defined in the database. for setting in Setting.objects.filter(key__in=settings_to_cache.keys(), user__isnull=True).order_by('pk'): if settings_to_cache[setting.key] != SETTING_CACHE_NOTSET: continue if self.registry.is_setting_encrypted(setting.key): + setting_ids[setting.key] = setting.id try: value = decrypt_field(setting, 'value') except ValueError, e: @@ -268,6 +273,9 @@ class SettingsWrapper(UserSettingsHolder): pass # Generate a cache key for each setting and store them all at once. settings_to_cache = dict([(Setting.get_cache_key(k), v) for k, v in settings_to_cache.items()]) + for k, id_val in setting_ids.items(): + logger.debug('Saving id in cache for encrypted setting %s', k) + self.cache.set(Setting.get_cache_id_key(k), id_val) settings_to_cache['_awx_conf_preload_expires'] = self._awx_conf_preload_expires logger.debug('cache set_many(%r, %r)', settings_to_cache, SETTING_CACHE_TIMEOUT) self.cache.set_many(settings_to_cache, timeout=SETTING_CACHE_TIMEOUT) @@ -293,6 +301,7 @@ class SettingsWrapper(UserSettingsHolder): field = self.registry.get_setting_field(name) if value is empty: setting = None + setting_id = None if not field.read_only or name in ( # these two values are read-only - however - we *do* want # to fetch their value from the database @@ -303,6 +312,7 @@ class SettingsWrapper(UserSettingsHolder): if setting: if getattr(field, 'encrypted', False): value = decrypt_field(setting, 'value') + setting_id = setting.id else: value = setting.value else: @@ -319,6 +329,9 @@ class SettingsWrapper(UserSettingsHolder): logger.debug('cache set(%r, %r, %r)', cache_key, get_cache_value(value), SETTING_CACHE_TIMEOUT) + if setting_id: + logger.debug('Saving id in cache for encrypted setting %s', cache_key) + self.cache.set(Setting.get_cache_id_key(cache_key), setting_id) self.cache.set(cache_key, get_cache_value(value), timeout=SETTING_CACHE_TIMEOUT) if value == SETTING_CACHE_NOTSET and not SETTING_CACHE_DEFAULTS: try: diff --git a/awx/conf/tests/unit/test_settings.py b/awx/conf/tests/unit/test_settings.py index f7f1540108..2f3c197478 100644 --- a/awx/conf/tests/unit/test_settings.py +++ b/awx/conf/tests/unit/test_settings.py @@ -391,7 +391,20 @@ def test_charfield_properly_sets_none(settings, mocker): ) -def test_settings_use_an_encrypted_cache(settings): +def test_settings_use_cache(settings, mocker): + settings.registry.register( + 'AWX_VAR', + field_class=fields.CharField, + category=_('System'), + category_slug='system' + ) + settings.cache.set('AWX_VAR', 'foobar') + settings.cache.set('_awx_conf_preload_expires', 100) + # Will fail test if database is used + getattr(settings, 'AWX_VAR') + + +def test_settings_use_an_encrypted_cache(settings, mocker): settings.registry.register( 'AWX_ENCRYPTED', field_class=fields.CharField, @@ -402,6 +415,11 @@ def test_settings_use_an_encrypted_cache(settings): assert isinstance(settings.cache, EncryptedCacheProxy) assert settings.cache.__dict__['encrypter'] == encrypt_field assert settings.cache.__dict__['decrypter'] == decrypt_field + settings.cache.set('AWX_ENCRYPTED_ID', 402) + settings.cache.set('AWX_ENCRYPTED', 'foobar') + settings.cache.set('_awx_conf_preload_expires', 100) + # Will fail test if database is used + getattr(settings, 'AWX_ENCRYPTED') def test_sensitive_cache_data_is_encrypted(settings, mocker): From 4e077e2b9508203b3b1fcc5a86536629ea955372 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 18 Aug 2017 11:28:42 -0400 Subject: [PATCH 018/138] fixing unit test for column-sort after adding the translate directive --- awx/ui/tests/spec/column-sort/column-sort.directive-test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/awx/ui/tests/spec/column-sort/column-sort.directive-test.js b/awx/ui/tests/spec/column-sort/column-sort.directive-test.js index 494f1d41d1..eff9ddb378 100644 --- a/awx/ui/tests/spec/column-sort/column-sort.directive-test.js +++ b/awx/ui/tests/spec/column-sort/column-sort.directive-test.js @@ -40,6 +40,10 @@ describe('Directive: column-sort', () =>{ this.$stateParams = {}; + var mockFilter = function (value) { + return value; + }; + angular.mock.module('ColumnSortModule', ($provide) =>{ QuerySet = jasmine.createSpyObj('qs', ['search']); @@ -49,6 +53,7 @@ describe('Directive: column-sort', () =>{ $provide.value('GetBasePath', GetBasePath); $provide.value('$state', this.$state); $provide.value('$stateParams', this.$stateParams); + $provide.value("translateFilter", mockFilter); }); }); From 043523b6d24434fa6d1ac6ee7a74db71b1c491f3 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 18 Aug 2017 11:45:32 -0400 Subject: [PATCH 019/138] adding translate directive to parts of components --- awx/ui/client/lib/components/input/label.partial.html | 2 +- awx/ui/client/lib/components/popover/popover.partial.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/lib/components/input/label.partial.html b/awx/ui/client/lib/components/input/label.partial.html index 537fd89fb5..c8d4802ffd 100644 --- a/awx/ui/client/lib/components/input/label.partial.html +++ b/awx/ui/client/lib/components/input/label.partial.html @@ -1,6 +1,6 @@
From 6bd5679429b60d40c4c833833a43707aae2de13c Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Fri, 18 Aug 2017 11:47:41 -0400 Subject: [PATCH 020/138] Update the instance filters popover text and add translations --- .../sources/add/sources-add.controller.js | 65 +++++---- .../sources/edit/sources-edit.controller.js | 59 ++++---- .../related/sources/sources.form.js | 127 +++++++++--------- 3 files changed, 123 insertions(+), 128 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js index 3989160a1c..4b0fc3369f 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/add/sources-add.controller.js @@ -9,12 +9,12 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'rbacUiControlService', 'ToJSON', 'SourcesService', 'Empty', 'Wait', 'Rest', 'Alert', 'ProcessErrors', 'inventorySourcesOptions', - '$rootScope', + '$rootScope', 'i18n', function($state, $stateParams, $scope, SourcesFormDefinition, ParseTypeChange, GenerateForm, inventoryData, GroupsService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, rbacUiControlService, ToJSON, SourcesService, Empty, Wait, Rest, Alert, ProcessErrors, - inventorySourcesOptions,$rootScope) { + inventorySourcesOptions,$rootScope, i18n) { let form = SourcesFormDefinition; init(); @@ -165,43 +165,40 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition', function initGroupBySelect(){ let add_new = false; - if($scope && $scope.source && $scope.source === 'ec2' || $scope && $scope.source && $scope.source.value && $scope.source.value === 'ec2'){ + if( _.get($scope, 'source') === 'ec2' || _.get($scope.source, 'value') === 'ec2') { $scope.group_by_choices = $scope.ec2_group_by; - $scope.groupByPopOver = "

Select which groups to create automatically. " + - $rootScope.BRAND_NAME + " will create group names similar to the following examples based on the options selected:

    " + - "
  • Availability Zone: zones » us-east-1b
  • " + - "
  • Image ID: images » ami-b007ab1e
  • " + - "
  • Instance ID: instances » i-ca11ab1e
  • " + - "
  • Instance Type: types » type_m1_medium
  • " + - "
  • Key Name: keys » key_testing
  • " + - "
  • Region: regions » us-east-1
  • " + - "
  • Security Group: security_groups » security_group_default
  • " + - "
  • Tags: tags » tag_Name » tag_Name_host1
  • " + - "
  • VPC ID: vpcs » vpc-5ca1ab1e
  • " + - "
  • Tag None: tags » tag_none
  • " + - "

If blank, all groups above are created except Instance ID.

"; - $scope.instanceFilterPopOver = "

Provide a comma-separated list of filter expressions. " + - "Hosts are imported to " + $rootScope.BRAND_NAME + " when ANY of the filters match.

" + - "Limit to hosts having a tag:
\n" + - "
tag-key=TowerManaged
\n" + - "Limit to hosts using either key pair:
\n" + - "
key-name=staging, key-name=production
\n" + - "Limit to hosts where the Name tag begins with test:
\n" + - "
tag:Name=test*
\n" + - "

View the Describe Instances documentation " + - "for a complete list of supported filters.

"; + $scope.groupByPopOver = "

" + i18n._("Select which groups to create automatically. ") + + $rootScope.BRAND_NAME + i18n._(" will create group names similar to the following examples based on the options selected:") + "

    " + + "
  • " + i18n._("Availability Zone:") + "zones » us-east-1b
  • " + + "
  • " + i18n._("Image ID:") + "images » ami-b007ab1e
  • " + + "
  • " + i18n._("Instance ID:") + "instances » i-ca11ab1e
  • " + + "
  • " + i18n._("Instance Type:") + "types » type_m1_medium
  • " + + "
  • " + i18n._("Key Name:") + "keys » key_testing
  • " + + "
  • " + i18n._("Region:") + "regions » us-east-1
  • " + + "
  • " + i18n._("Security Group:") + "security_groups » security_group_default
  • " + + "
  • " + i18n._("Tags:") + "tags » tag_Name » tag_Name_host1
  • " + + "
  • " + i18n._("VPC ID:") + "vpcs » vpc-5ca1ab1e
  • " + + "
  • " + i18n._("Tag None:") + "tags » tag_none
  • " + + "

" + i18n._("If blank, all groups above are created except") + "" + i18n._("Instance ID") + ".

"; + + $scope.instanceFilterPopOver = "

" + i18n._("Provide a comma-separated list of filter expressions. ") + + i18n._("Hosts are imported to ") + $rootScope.BRAND_NAME + i18n._(" when ") + "" + i18n._("ANY") + "" + i18n._(" of the filters match.") + "

" + + i18n._("Limit to hosts having a tag:") + "
\n" + + "
tag-key=TowerManaged
\n" + + i18n._("Limit to hosts using either key pair:") + "
\n" + + "
key-name=staging, key-name=production
\n" + + i18n._("Limit to hosts where the Name tag begins with ") + "" + i18n._("test") + ":
\n" + + "
tag:Name=test*
\n" + + "

" + i18n._("View the ") + "" + i18n._("Describe Instances documentation") + " " + + i18n._("for a complete list of supported filters.") + "

"; } - if($scope && $scope.source && $scope.source === 'vmware' || $scope && $scope.source && $scope.source.value && $scope.source.value === 'vmware'){ + if( _.get($scope, 'source') === 'vmware' || _.get($scope.source, 'value') === 'vmware') { add_new = true; $scope.group_by_choices = []; $scope.group_by = $scope.group_by_choices; - $scope.groupByPopOver = `Specify which groups to create automatically. - Group names will be created similar to the options selected. - If blank, all groups above are created. Refer to Ansible Tower documentation for more detail.`; - $scope.instanceFilterPopOver = `Provide a comma-separated list of filter expressions. - Hosts are imported when ANY of the filters match. - Refer to Ansible Tower documentation for more detail.`; - } + $scope.groupByPopOver = i18n._("Specify which groups to create automatically. Group names will be created similar to the options selected. If blank, all groups above are created. Refer to Ansible Tower documentation for more detail."); + $scope.instanceFilterPopOver = i18n._("Provide a comma-separated list of filter expressions. Hosts are imported when all of the filters match. Refer to Ansible Tower documentation for more detail."); + } CreateSelect2({ element: '#inventory_source_group_by', multiple: true, diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js index e9beb3a906..9cf465364c 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/edit/sources-edit.controller.js @@ -8,12 +8,12 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', 'rbacUiControlService', 'ToJSON', 'ParseTypeChange', 'GroupsService', 'GetChoices', 'GetBasePath', 'CreateSelect2', 'GetSourceTypeOptions', 'inventorySourceData', 'SourcesService', 'inventoryData', 'inventorySourcesOptions', 'Empty', - 'Wait', 'Rest', 'Alert', '$rootScope', + 'Wait', 'Rest', 'Alert', '$rootScope', 'i18n', function($state, $stateParams, $scope, ParseVariableString, rbacUiControlService, ToJSON,ParseTypeChange, GroupsService, GetChoices, GetBasePath, CreateSelect2, GetSourceTypeOptions, inventorySourceData, SourcesService, inventoryData, inventorySourcesOptions, Empty, - Wait, Rest, Alert, $rootScope) { + Wait, Rest, Alert, $rootScope, i18n) { function init() { $scope.projectBasePath = GetBasePath('projects') + '?not__status=never updated'; @@ -243,46 +243,45 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString', function initGroupBySelect(){ let add_new = false; - if($scope && $scope.source && $scope.source === 'ec2' || $scope && $scope.source && $scope.source.value && $scope.source.value === 'ec2'){ + if( _.get($scope, 'source') === 'ec2' || _.get($scope.source, 'value') === 'ec2') { $scope.group_by_choices = $scope.ec2_group_by; let group_by = inventorySourceData.group_by.split(','); $scope.group_by = _.map(group_by, (item) => _.find($scope.ec2_group_by, { value: item })); - $scope.groupByPopOver = "

Select which groups to create automatically. " + - $rootScope.BRAND_NAME + " will create group names similar to the following examples based on the options selected:

    " + - "
  • Availability Zone: zones » us-east-1b
  • " + - "
  • Image ID: images » ami-b007ab1e
  • " + - "
  • Instance ID: instances » i-ca11ab1e
  • " + - "
  • Instance Type: types » type_m1_medium
  • " + - "
  • Key Name: keys » key_testing
  • " + - "
  • Region: regions » us-east-1
  • " + - "
  • Security Group: security_groups » security_group_default
  • " + - "
  • Tags: tags » tag_Name » tag_Name_host1
  • " + - "
  • VPC ID: vpcs » vpc-5ca1ab1e
  • " + - "
  • Tag None: tags » tag_none
  • " + - "

If blank, all groups above are created except Instance ID.

"; - $scope.instanceFilterPopOver = "

Provide a comma-separated list of filter expressions. " + - "Hosts are imported to " + $rootScope.BRAND_NAME + " when ANY of the filters match.

" + - "Limit to hosts having a tag:
\n" + + + $scope.groupByPopOver = "

" + i18n._("Select which groups to create automatically. ") + + $rootScope.BRAND_NAME + i18n._(" will create group names similar to the following examples based on the options selected:") + "

    " + + "
  • " + i18n._("Availability Zone:") + "zones » us-east-1b
  • " + + "
  • " + i18n._("Image ID:") + "images » ami-b007ab1e
  • " + + "
  • " + i18n._("Instance ID:") + "instances » i-ca11ab1e
  • " + + "
  • " + i18n._("Instance Type:") + "types » type_m1_medium
  • " + + "
  • " + i18n._("Key Name:") + "keys » key_testing
  • " + + "
  • " + i18n._("Region:") + "regions » us-east-1
  • " + + "
  • " + i18n._("Security Group:") + "security_groups » security_group_default
  • " + + "
  • " + i18n._("Tags:") + "tags » tag_Name » tag_Name_host1
  • " + + "
  • " + i18n._("VPC ID:") + "vpcs » vpc-5ca1ab1e
  • " + + "
  • " + i18n._("Tag None:") + "tags » tag_none
  • " + + "

" + i18n._("If blank, all groups above are created except") + "" + i18n._("Instance ID") + ".

"; + + + $scope.instanceFilterPopOver = "

" + i18n._("Provide a comma-separated list of filter expressions. ") + + i18n._("Hosts are imported to ") + $rootScope.BRAND_NAME + i18n._(" when ") + "" + i18n._("ANY") + "" + i18n._(" of the filters match.") + "

" + + i18n._("Limit to hosts having a tag:") + "
\n" + "
tag-key=TowerManaged
\n" + - "Limit to hosts using either key pair:
\n" + + i18n._("Limit to hosts using either key pair:") + "
\n" + "
key-name=staging, key-name=production
\n" + - "Limit to hosts where the Name tag begins with test:
\n" + + i18n._("Limit to hosts where the Name tag begins with ") + "" + i18n._("test") + ":
\n" + "
tag:Name=test*
\n" + - "

View the Describe Instances documentation " + - "for a complete list of supported filters.

"; + "

" + i18n._("View the ") + "" + i18n._("Describe Instances documentation") + " " + + i18n._("for a complete list of supported filters.") + "

"; } - if($scope && $scope.source && $scope.source === 'vmware' || $scope && $scope.source && $scope.source.value && $scope.source.value === 'vmware'){ + if( _.get($scope, 'source') === 'vmware' || _.get($scope.source, 'value') === 'vmware') { add_new = true; $scope.group_by_choices = (inventorySourceData.group_by) ? inventorySourceData.group_by.split(',') .map((i) => ({name: i, label: i, value: i})) : []; $scope.group_by = $scope.group_by_choices; - $scope.groupByPopOver = `Specify which groups to create automatically. - Group names will be created similar to the options selected. - If blank, all groups above are created. Refer to Ansible Tower documentation for more detail.`; - $scope.instanceFilterPopOver = `Provide a comma-separated list of filter expressions. - Hosts are imported when ANY of the filters match. - Refer to Ansible Tower documentation for more detail.`; + $scope.groupByPopOver = i18n._(`Specify which groups to create automatically. Group names will be created similar to the options selected. If blank, all groups above are created. Refer to Ansible Tower documentation for more detail.`); + $scope.instanceFilterPopOver = i18n._(`Provide a comma-separated list of filter expressions. Hosts are imported when all of the filters match. Refer to Ansible Tower documentation for more detail.`); } CreateSelect2({ element: '#inventory_source_group_by', diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js index a7e4b4acfd..d091289d18 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/sources/sources.form.js @@ -109,7 +109,7 @@ return { inventory_file: { label: i18n._('Inventory File'), type:'select', - defaultText: 'Choose an inventory file', + defaultText: i18n._('Choose an inventory file'), ngOptions: 'file for file in inventory_files track by file', ngShow: "source && source.value === 'scm'", ngDisabled: "!(inventory_source_obj.summary_fields.user_capabilities.edit || canAdd) || disableInventoryFileBecausePermissionDenied", @@ -119,7 +119,8 @@ return { init: "true" }, column: 1, - awPopOver: "

" + i18n._("Select the inventory file to be synced by this source. You can select from the dropdown or enter a file within the input.") + "

", + awPopOver: "

" + i18n._("Select the inventory file to be synced by this source. " + + "You can select from the dropdown or enter a file within the input.") + "

", dataTitle: i18n._('Inventory File'), dataPlacement: 'right', dataContainer: "body", @@ -141,10 +142,10 @@ return { subForm: 'sourceSubForm' }, instance_filters: { - label: i18n._('Instance Filters'), + label: i18n._("Instance Filters"), type: 'text', ngShow: "source && (source.value == 'ec2' || source.value == 'vmware')", - dataTitle: 'Instance Filters', + dataTitle: i18n._('Instance Filters'), dataPlacement: 'right', awPopOverWatch: 'instanceFilterPopOver', awPopOver: '{{ instanceFilterPopOver }}', @@ -158,7 +159,7 @@ return { ngShow: "source && (source.value == 'ec2' || source.value == 'vmware')", ngOptions: 'source.label for source in group_by_choices track by source.value', multiSelect: true, - dataTitle: 'Only Group By', + dataTitle: i18n._("Only Group By"), dataPlacement: 'right', awPopOverWatch: 'groupByPopOver', awPopOver: '{{ groupByPopOver }}', @@ -192,14 +193,14 @@ return { parseTypeName: 'envParseType', dataTitle: i18n._("Environment Variables"), dataPlacement: 'right', - awPopOver: "

Provide environment variables to pass to the custom inventory script.

" + - "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + - "JSON:
\n" + + awPopOver: "

" + i18n._("Provide environment variables to pass to the custom inventory script.") + "

" + + "

" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + + i18n._("JSON:") + "
\n" + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + + i18n._("YAML:") + "
\n" + "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', + "

" + i18n._("View JSON examples at ") + 'www.json.org

' + + "

" + i18n._("View YAML examples at ") + 'docs.ansible.com

', dataContainer: 'body', subForm: 'sourceSubForm' }, @@ -214,16 +215,16 @@ return { parseTypeName: 'envParseType', dataTitle: i18n._("Source Variables"), dataPlacement: 'right', - awPopOver: "

Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables " + + awPopOver: "

" + i18n._("Override variables found in ec2.ini and used by the inventory update script. For a detailed description of these variables ") + "" + - "view ec2.ini in the Ansible github repo.

" + - "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + - "JSON:
\n" + + i18n._("view ec2.ini in the Ansible github repo.") + "

" + + "

" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + + i18n._("JSON:") + "
\n" + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + + i18n._("YAML:") + "
\n" + "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', + "

" + i18n._("View JSON examples at ") + 'www.json.org

' + + "

" + i18n._("View YAML examples at ") + 'docs.ansible.com

', dataContainer: 'body', subForm: 'sourceSubForm' }, @@ -236,18 +237,18 @@ return { rows: 6, 'default': '---', parseTypeName: 'envParseType', - dataTitle: "Source Variables", + dataTitle: i18n._("Source Variables"), dataPlacement: 'right', - awPopOver: "

Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables " + + awPopOver: "

" + i18n._("Override variables found in vmware.ini and used by the inventory update script. For a detailed description of these variables ") + "" + - "view vmware_inventory.ini in the Ansible github repo.

" + - "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + - "JSON:
\n" + + i18n._("view vmware_inventory.ini in the Ansible github repo.") + "

" + + "

" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + + i18n._("JSON:") + "
\n" + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + + i18n._("YAML:") + "
\n" + "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', + "

" + i18n._("View JSON examples at ") + 'www.json.org

' + + "

" + i18n._("View YAML examples at ") + 'docs.ansible.com

', dataContainer: 'body', subForm: 'sourceSubForm' }, @@ -260,18 +261,18 @@ return { rows: 6, 'default': '---', parseTypeName: 'envParseType', - dataTitle: "Source Variables", + dataTitle: i18n._("Source Variables"), dataPlacement: 'right', - awPopOver: "

Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration " + + awPopOver: "

" + i18n._("Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration ") + "" + - "view openstack.yml in the Ansible github repo.

" + - "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + - "JSON:
\n" + + i18n._("view openstack.yml in the Ansible github repo.") + "

" + + "

" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + + i18n._("JSON:") + "
\n" + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + + i18n._("YAML:") + "
\n" + "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', + "

" + i18n._("View JSON examples at ") + 'www.json.org

' + + "

" + i18n._("View YAML examples at ") + 'docs.ansible.com

', dataContainer: 'body', subForm: 'sourceSubForm' }, @@ -284,18 +285,18 @@ return { rows: 6, 'default': '---', parseTypeName: 'envParseType', - dataTitle: "Source Variables", + dataTitle: i18n._("Source Variables"), dataPlacement: 'right', - awPopOver: "

Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration " + + awPopOver: "

" + i18n._("Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration ") + "" + - "view openstack.yml in the Ansible github repo.

" + - "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + - "JSON:
\n" + + i18n._("view openstack.yml in the Ansible github repo.") + "

" + + "

" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + + i18n._("JSON:") + "
\n" + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + + i18n._("YAML:") + "
\n" + "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', + "

" + i18n._("View JSON examples at ") + 'www.json.org

' + + "

" + i18n._("View YAML examples at ") + 'docs.ansible.com

', dataContainer: 'body', subForm: 'sourceSubForm' }, @@ -308,18 +309,18 @@ return { rows: 6, 'default': '---', parseTypeName: 'envParseType', - dataTitle: "Source Variables", + dataTitle: i18n._("Source Variables"), dataPlacement: 'right', - awPopOver: "

Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration " + + awPopOver: "

" + i18n._("Override variables found in openstack.yml and used by the inventory update script. For an example variable configuration ") + "" + - "view openstack.yml in the Ansible github repo.

" + - "

Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.

" + - "JSON:
\n" + + i18n._("view openstack.yml in the Ansible github repo.") + "

" + + "

" + i18n._("Enter variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + + i18n._("JSON:") + "
\n" + "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + + i18n._("YAML:") + "
\n" + "
---
somevar: somevalue
password: magic
\n" + - '

View JSON examples at www.json.org

' + - '

View YAML examples at docs.ansible.com

', + "

" + i18n._("View JSON examples at ") + 'www.json.org

' + + "

" + i18n._("View YAML examples at ") + 'docs.ansible.com

', dataContainer: 'body', subForm: 'sourceSubForm' }, @@ -348,9 +349,8 @@ return { label: i18n._('Overwrite'), type: 'checkbox', ngShow: "source.value !== '' && source.value !== null", - awPopOver: '

If checked, all child groups and hosts not found on the external source will be deleted from ' + - 'the local inventory.

When not checked, local child hosts and groups not found on the external source will ' + - 'remain untouched by the inventory update process.

', + awPopOver: "

" + i18n._("If checked, all child groups and hosts not found on the external source will be deleted from the local inventory.") + '

' + + i18n._("When not checked, local child hosts and groups not found on the external source will remain untouched by the inventory update process.") + "

", dataTitle: i18n._('Overwrite'), dataContainer: 'body', dataPlacement: 'right', @@ -361,9 +361,8 @@ return { label: i18n._('Overwrite Variables'), type: 'checkbox', ngShow: "source.value !== '' && source.value !== null", - awPopOver: '

If checked, all variables for child groups and hosts will be removed and replaced by those ' + - 'found on the external source.

When not checked, a merge will be performed, combining local variables with ' + - 'those found on the external source.

', + awPopOver: "

" + i18n._("If checked, all variables for child groups and hosts will be removed and replaced by those found on the external source.") + '

' + + i18n._("When not checked, a merge will be performed, combining local variables with those found on the external source.") + "

", dataTitle: i18n._('Overwrite Variables'), dataContainer: 'body', dataPlacement: 'right', @@ -374,8 +373,8 @@ return { label: i18n._('Update on Launch'), type: 'checkbox', ngShow: "source.value !== '' && source.value !== null", - awPopOver: '

Each time a job runs using this inventory, refresh the inventory from the selected source before ' + - 'executing job tasks.

', + awPopOver: "

" + i18n._("Each time a job runs using this inventory, " + + "refresh the inventory from the selected source before executing job tasks.") + "

", dataTitle: i18n._('Update on Launch'), dataContainer: 'body', dataPlacement: 'right', @@ -386,9 +385,9 @@ return { label: i18n._('Update on Project Change'), type: 'checkbox', ngShow: "source.value === 'scm'", - awPopOver: '

After every project update where the SCM revision changes, refresh the inventory ' + - 'from the selected source before executing job tasks. This is intended for ' + - 'static content, like the Ansible inventory .ini file format.

', + awPopOver: "

" + i18n._("After every project update where the SCM revision changes, " + + "refresh the inventory from the selected source before executing job tasks. " + + "This is intended for static content, like the Ansible inventory .ini file format.") + "

", dataTitle: i18n._('Update on Project Update'), dataContainer: 'body', dataPlacement: 'right', @@ -406,9 +405,9 @@ return { ngShow: "source && source.value !== '' && update_on_launch", spinner: true, "default": 0, - awPopOver: '

Time in seconds to consider an inventory sync to be current. During job runs and callbacks the task system will ' + - 'evaluate the timestamp of the latest sync. If it is older than Cache Timeout, it is not considered current, ' + - 'and a new inventory sync will be performed.

', + awPopOver: "

" + i18n._("Time in seconds to consider an inventory sync to be current. " + + "During job runs and callbacks the task system will evaluate the timestamp of the latest sync. " + + "If it is older than Cache Timeout, it is not considered current, and a new inventory sync will be performed.") + "

", dataTitle: i18n._('Cache Timeout'), dataPlacement: 'right', dataContainer: "body", From d2595944fc3295bea4e7b2d406bd0899265858a0 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 18 Aug 2017 14:01:12 -0400 Subject: [PATCH 021/138] make sure all toggles on ctit form are disabled --- awx/ui/client/src/scheduler/scheduleToggle.block.less | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/awx/ui/client/src/scheduler/scheduleToggle.block.less b/awx/ui/client/src/scheduler/scheduleToggle.block.less index bb30e7e60f..3cdb8f8f40 100644 --- a/awx/ui/client/src/scheduler/scheduleToggle.block.less +++ b/awx/ui/client/src/scheduler/scheduleToggle.block.less @@ -1,5 +1,14 @@ /** @define ScheduleToggle */ +.Form-formGroup--disabled .ScheduleToggle { + cursor: not-allowed; + border-color: @default-link !important; + .ScheduleToggle-switch { + background-color: @d7grey !important; + cursor: not-allowed; + } +} + .ScheduleToggle { border-radius: 5px; border: 1px solid @default-link; From 438d41c986161971c4eea3c42c1665f4adf20984 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 18 Aug 2017 14:10:19 -0400 Subject: [PATCH 022/138] make `vault_password` required for Vault credentials see: https://github.com/ansible/ansible-tower/issues/7468 --- awx/main/models/credential.py | 1 + .../tests/functional/api/test_credential.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 720e3c2fa3..b7d76581dc 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -690,6 +690,7 @@ def vault(cls): 'secret': True, 'ask_at_runtime': True }], + 'required': ['vault_password'], } ) diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index ac9c1be960..201732a7b9 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -922,6 +922,25 @@ def test_vault_create_ok(post, organization, admin, version, params): assert decrypt_field(cred, 'vault_password') == 'some_password' +@pytest.mark.django_db +def test_vault_password_required(post, organization, admin): + vault = CredentialType.defaults['vault']() + vault.save() + response = post( + reverse('api:credential_list', kwargs={'version': 'v2'}), + { + 'credential_type': vault.pk, + 'organization': organization.id, + 'name': 'Best credential ever', + 'inputs': {} + }, + admin + ) + assert response.status_code == 400 + assert response.data['inputs'] == {'vault_password': ['required for Vault']} + assert Credential.objects.count() == 0 + + # # Net Credentials # From 08574428f10d2c680914e63621f730b82004c92f Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Fri, 18 Aug 2017 14:28:25 -0400 Subject: [PATCH 023/138] add disabled toggle to job templte diff mode --- .../client/src/job-submission/job-submission.partial.html | 3 ++- awx/ui/client/src/shared/form-generator.js | 8 +++++--- .../src/templates/job_templates/job-template.form.js | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/job-submission/job-submission.partial.html b/awx/ui/client/src/job-submission/job-submission.partial.html index 0452e77e4d..53d043060b 100644 --- a/awx/ui/client/src/job-submission/job-submission.partial.html +++ b/awx/ui/client/src/job-submission/job-submission.partial.html @@ -258,7 +258,8 @@
- +
diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 10b6ae0a62..3f1cec8af0 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -758,10 +758,12 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += label(labelOptions); - html += `
- - + +
`; } diff --git a/awx/ui/client/src/templates/job_templates/job-template.form.js b/awx/ui/client/src/templates/job_templates/job-template.form.js index a25cfeccd8..e5e0e4c257 100644 --- a/awx/ui/client/src/templates/job_templates/job-template.form.js +++ b/awx/ui/client/src/templates/job_templates/job-template.form.js @@ -272,6 +272,7 @@ function(NotificationsList, CompletedJobsList, i18n) { variable: 'ask_diff_mode_on_launch', text: i18n._('Prompt on launch') }, + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' }, checkbox_group: { label: i18n._('Options'), From 0121e5c22bfa75b219b620445fe4f43be6c2456f Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Fri, 18 Aug 2017 11:59:58 -0400 Subject: [PATCH 024/138] Update API help text for Tower configuration. --- awx/sso/conf.py | 115 +++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 61 deletions(-) diff --git a/awx/sso/conf.py b/awx/sso/conf.py index 636b39daf0..e04f091851 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -29,9 +29,9 @@ class SocialAuthCallbackURL(object): SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT = _('''\ Mapping to organization admins/users from social auth accounts. This setting -controls which users are placed into which Tower organizations based on -their username and email address. Configuration details are available in -Tower documentation.\ +controls which users are placed into which Tower organizations based on their +username and email address. Configuration details are available in the Ansible +Tower documentation.'\ ''') # FIXME: /regex/gim (flags) @@ -152,11 +152,9 @@ register( default='', validators=[validate_ldap_bind_dn], label=_('LDAP Bind DN'), - help_text=_('DN (Distinguished Name) of user to bind for all search queries. ' - 'Normally in the format "CN=Some User,OU=Users,DC=example,DC=com" ' - 'but may also be specified as "DOMAIN\username" for Active Directory. ' - 'This is the system user account we will use to login to query LDAP ' - 'for other user information.'), + help_text=_('DN (Distinguished Name) of user to bind for all search queries. This' + ' is the system user account we will use to login to query LDAP for other' + ' user information. Refer to the Ansible Tower documentation for example syntax.'), category=_('LDAP'), category_slug='ldap', feature_required='ldap', @@ -213,7 +211,7 @@ register( label=_('LDAP User Search'), help_text=_('LDAP search query to find users. Any user that matches the given ' 'pattern will be able to login to Tower. The user should also be ' - 'mapped into an Tower organization (as defined in the ' + 'mapped into a Tower organization (as defined in the ' 'AUTH_LDAP_ORGANIZATION_MAP setting). If multiple search queries ' 'need to be supported use of "LDAPUnion" is possible. See ' 'Tower documentation for details.'), @@ -235,7 +233,7 @@ register( default=None, label=_('LDAP User DN Template'), help_text=_('Alternative to user search, if user DNs are all of the same ' - 'format. This approach will be more efficient for user lookups than ' + 'format. This approach is more efficient for user lookups than ' 'searching if it is usable in your organizational environment. If ' 'this setting has a value it will be used instead of ' 'AUTH_LDAP_USER_SEARCH.'), @@ -250,11 +248,10 @@ register( field_class=fields.LDAPUserAttrMapField, default={}, label=_('LDAP User Attribute Map'), - help_text=_('Mapping of LDAP user schema to Tower API user attributes (key is ' - 'user attribute name, value is LDAP attribute name). The default ' - 'setting is valid for ActiveDirectory but users with other LDAP ' - 'configurations may need to change the values (not the keys) of ' - 'the dictionary/hash-table.'), + help_text=_('Mapping of LDAP user schema to Tower API user attributes. The default' + ' setting is valid for ActiveDirectory but users with other LDAP' + ' configurations may need to change the values. Refer to the Ansible' + ' Tower documentation for additonal details.'), category=_('LDAP'), category_slug='ldap', placeholder=collections.OrderedDict([ @@ -270,10 +267,9 @@ register( field_class=fields.LDAPSearchField, default=[], label=_('LDAP Group Search'), - help_text=_('Users are mapped to organizations based on their ' - 'membership in LDAP groups. This setting defines the LDAP search ' - 'query to find groups. Note that this, unlike the user search ' - 'above, does not support LDAPSearchUnion.'), + help_text=_('Users are mapped to organizations based on their membership in LDAP' + ' groups. This setting defines the LDAP search query to find groups. ' + 'Unlike the user search, group search does not support LDAPSearchUnion.'), category=_('LDAP'), category_slug='ldap', placeholder=( @@ -335,12 +331,9 @@ register( field_class=fields.LDAPUserFlagsField, default={}, label=_('LDAP User Flags By Group'), - help_text=_('User profile flags updated from group membership (key is user ' - 'attribute name, value is group DN). These are boolean fields ' - 'that are matched based on whether the user is a member of the ' - 'given group. So far only is_superuser and is_system_auditor ' - 'are settable via this method. This flag is set both true and ' - 'false at login time based on current LDAP settings.'), + help_text=_('Retrieve users from a given group. At this time, superuser and system' + ' auditors are the only groups supported. Refer to the Ansible Tower' + ' documentation for more detail.'), category=_('LDAP'), category_slug='ldap', placeholder=collections.OrderedDict([ @@ -355,9 +348,9 @@ register( default={}, label=_('LDAP Organization Map'), help_text=_('Mapping between organization admins/users and LDAP groups. This ' - 'controls what users are placed into what Tower organizations ' + 'controls which users are placed into which Tower organizations ' 'relative to their LDAP group memberships. Configuration details ' - 'are available in Tower documentation.'), + 'are available in the Ansible Tower documentation.'), category=_('LDAP'), category_slug='ldap', placeholder=collections.OrderedDict([ @@ -382,8 +375,8 @@ register( field_class=fields.LDAPTeamMapField, default={}, label=_('LDAP Team Map'), - help_text=_('Mapping between team members (users) and LDAP groups.' - 'Configuration details are available in Tower documentation.'), + help_text=_('Mapping between team members (users) and LDAP groups. Configuration' + ' details are available in the Ansible Tower documentation.'), category=_('LDAP'), category_slug='ldap', placeholder=collections.OrderedDict([ @@ -411,7 +404,7 @@ register( allow_blank=True, default='', label=_('RADIUS Server'), - help_text=_('Hostname/IP of RADIUS server. RADIUS authentication will be ' + help_text=_('Hostname/IP of RADIUS server. RADIUS authentication is ' 'disabled if this setting is empty.'), category=_('RADIUS'), category_slug='radius', @@ -522,10 +515,9 @@ register( read_only=True, default=SocialAuthCallbackURL('google-oauth2'), label=_('Google OAuth2 Callback URL'), - help_text=_('Create a project at https://console.developers.google.com/ to ' - 'obtain an OAuth2 key and secret for a web application. Ensure ' - 'that the Google+ API is enabled. Provide this URL as the ' - 'callback URL for your application.'), + help_text=_('Provide this URL as the callback URL for your application as part ' + 'of your registration process. Refer to the Ansible Tower ' + 'documentation for more detail.'), category=_('Google OAuth2'), category_slug='google-oauth2', depends_on=['TOWER_URL_BASE'], @@ -537,7 +529,7 @@ register( allow_blank=True, default='', label=_('Google OAuth2 Key'), - help_text=_('The OAuth2 key from your web application at https://console.developers.google.com/.'), + help_text=_('The OAuth2 key from your web application.'), category=_('Google OAuth2'), category_slug='google-oauth2', placeholder='528620852399-gm2dt4hrl2tsj67fqamk09k1e0ad6gd8.apps.googleusercontent.com', @@ -549,7 +541,7 @@ register( allow_blank=True, default='', label=_('Google OAuth2 Secret'), - help_text=_('The OAuth2 secret from your web application at https://console.developers.google.com/.'), + help_text=_('The OAuth2 secret from your web application.'), category=_('Google OAuth2'), category_slug='google-oauth2', placeholder='q2fMVCmEregbg-drvebPp8OW', @@ -573,10 +565,10 @@ register( field_class=fields.DictField, default={}, label=_('Google OAuth2 Extra Arguments'), - help_text=_('Extra arguments for Google OAuth2 login. When only allowing a ' - 'single domain to authenticate, set to `{"hd": "yourdomain.com"}` ' - 'and Google will not display any other accounts even if the user ' - 'is logged in with multiple Google accounts.'), + help_text=_('Extra arguments for Google OAuth2 login. You can restrict it to' + ' only allow a single domain to authenticate, even if the user is' + ' logged in with multple Google accounts. Refer to the Ansible Tower' + ' documentation for more detail.'), category=_('Google OAuth2'), category_slug='google-oauth2', placeholder={'hd': 'example.com'}, @@ -616,10 +608,9 @@ register( read_only=True, default=SocialAuthCallbackURL('github'), label=_('GitHub OAuth2 Callback URL'), - help_text=_('Create a developer application at ' - 'https://github.com/settings/developers to obtain an OAuth2 ' - 'key (Client ID) and secret (Client Secret). Provide this URL ' - 'as the callback URL for your application.'), + help_text=_('Provide this URL as the callback URL for your application as part ' + 'of your registration process. Refer to the Ansible Tower ' + 'documentation for more detail.'), category=_('GitHub OAuth2'), category_slug='github', depends_on=['TOWER_URL_BASE'], @@ -682,10 +673,9 @@ register( read_only=True, default=SocialAuthCallbackURL('github-org'), label=_('GitHub Organization OAuth2 Callback URL'), - help_text=_('Create an organization-owned application at ' - 'https://github.com/organizations//settings/applications ' - 'and obtain an OAuth2 key (Client ID) and secret (Client Secret). ' - 'Provide this URL as the callback URL for your application.'), + help_text=_('Provide this URL as the callback URL for your application as part ' + 'of your registration process. Refer to the Ansible Tower ' + 'documentation for more detail.'), category=_('GitHub Organization OAuth2'), category_slug='github-org', depends_on=['TOWER_URL_BASE'], @@ -838,10 +828,9 @@ register( read_only=True, default=SocialAuthCallbackURL('azuread-oauth2'), label=_('Azure AD OAuth2 Callback URL'), - help_text=_('Register an Azure AD application as described by ' - 'https://msdn.microsoft.com/en-us/library/azure/dn132599.aspx ' - 'and obtain an OAuth2 key (Client ID) and secret (Client Secret). ' - 'Provide this URL as the callback URL for your application.'), + help_text=_('Provide this URL as the callback URL for your application as part' + ' of your registration process. Refer to the Ansible Tower' + ' documentation for more detail. '), category=_('Azure AD OAuth2'), category_slug='azuread-oauth2', depends_on=['TOWER_URL_BASE'], @@ -984,7 +973,8 @@ register( field_class=fields.SAMLOrgInfoField, required=True, label=_('SAML Service Provider Organization Info'), - help_text=_('Configure this setting with information about your app.'), + help_text=_('Provide the URL, display name, and the name of your app. Refer to' + ' the Ansible Tower documentation for example syntax.'), category=_('SAML'), category_slug='saml', placeholder=collections.OrderedDict([ @@ -1003,7 +993,9 @@ register( allow_blank=True, required=True, label=_('SAML Service Provider Technical Contact'), - help_text=_('Configure this setting with your contact information.'), + help_text=_('Provide the name and email address of the technical contact for' + ' your service provider. Refer to the Ansible Tower documentation' + ' for example syntax.'), category=_('SAML'), category_slug='saml', placeholder=collections.OrderedDict([ @@ -1019,7 +1011,9 @@ register( allow_blank=True, required=True, label=_('SAML Service Provider Support Contact'), - help_text=_('Configure this setting with your contact information.'), + help_text=_('Provide the name and email address of the support contact for your' + ' service provider. Refer to the Ansible Tower documentation for' + ' example syntax.'), category=_('SAML'), category_slug='saml', placeholder=collections.OrderedDict([ @@ -1034,12 +1028,11 @@ register( field_class=fields.SAMLEnabledIdPsField, default={}, label=_('SAML Enabled Identity Providers'), - help_text=_('Configure the Entity ID, SSO URL and certificate for each ' - 'identity provider (IdP) in use. Multiple SAML IdPs are supported. ' - 'Some IdPs may provide user data using attribute names that differ ' - 'from the default OIDs ' - '(https://github.com/omab/python-social-auth/blob/master/social/backends/saml.py#L16). ' - 'Attribute names may be overridden for each IdP.'), + help_text=_('Configure the Entity ID, SSO URL and certificate for each identity' + ' provider (IdP) in use. Multiple SAML IdPs are supported. Some IdPs' + ' may provide user data using attribute names that differ from the' + ' default OIDs. Attribute names may be overridden for each IdP. Refer' + ' to the Ansible documentation for additional details and syntax.'), category=_('SAML'), category_slug='saml', placeholder=collections.OrderedDict([ From da70c11da5fdabb419e534f384b84042bf115e1b Mon Sep 17 00:00:00 2001 From: Ryan Fitzpatrick Date: Fri, 18 Aug 2017 16:09:13 -0400 Subject: [PATCH 025/138] Minor credential help text correction --- awx/main/models/credential.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 720e3c2fa3..4906d7d14e 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -822,7 +822,7 @@ def vmware(cls): 'id': 'host', 'label': 'VCenter Host', 'type': 'string', - 'help_text': ('Enter the hostname or IP address which corresponds ' + 'help_text': ('Enter the hostname or IP address that corresponds ' 'to your VMware vCenter.') }, { 'id': 'username', @@ -850,7 +850,7 @@ def satellite6(cls): 'id': 'host', 'label': 'Satellite 6 URL', 'type': 'string', - 'help_text': ('Enter the URL which corresponds to your Red Hat ' + 'help_text': ('Enter the URL that corresponds to your Red Hat ' 'Satellite 6 server. For example, https://satellite.example.org') }, { 'id': 'username', @@ -877,7 +877,7 @@ def cloudforms(cls): 'id': 'host', 'label': 'CloudForms URL', 'type': 'string', - 'help_text': ('Enter the URL for the virtual machine which ' + 'help_text': ('Enter the URL for the virtual machine that ' 'corresponds to your CloudForm instance. ' 'For example, https://cloudforms.example.org') }, { @@ -912,8 +912,9 @@ def gce(cls): 'label': 'Project', 'type': 'string', 'help_text': ('The Project ID is the GCE assigned identification. ' - 'It is constructed as two words followed by a three ' - 'digit number. Example: adjective-noun-000') + 'It is often constructed as three words or two words ' + 'followed by a three-digit number. Examples: project-id-000 ' + 'and another-project-id') }, { 'id': 'ssh_key_data', 'label': 'RSA Private Key', From 50782b94654ff0fe80566c9805680661bf4c0ba2 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 18 Aug 2017 15:46:28 -0400 Subject: [PATCH 026/138] add required fields for RHSatellite6 credentials see: https://github.com/ansible/ansible-tower/issues/7467 --- awx/main/models/credential.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 4906d7d14e..037c71b39c 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -861,7 +861,8 @@ def satellite6(cls): 'label': 'Password', 'type': 'string', 'secret': True, - }] + }], + 'required': ['host', 'username', 'password'], } ) From 90b5d98e5ccbda1ae6f643bc6076f1e97198c407 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 18 Aug 2017 15:48:57 -0400 Subject: [PATCH 027/138] add required fields for network credentials see: https://github.com/ansible/ansible-tower/issues/7466 --- awx/main/models/credential.py | 3 ++- awx/main/tests/functional/api/test_credential.py | 1 + awx/main/tests/functional/test_credential.py | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 037c71b39c..d2a69918ca 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -735,7 +735,8 @@ def net(cls): 'dependencies': { 'ssh_key_unlock': ['ssh_key_data'], 'authorize_password': ['authorize'], - } + }, + 'required': ['username'], } ) diff --git a/awx/main/tests/functional/api/test_credential.py b/awx/main/tests/functional/api/test_credential.py index ac9c1be960..fbd8889a06 100644 --- a/awx/main/tests/functional/api/test_credential.py +++ b/awx/main/tests/functional/api/test_credential.py @@ -748,6 +748,7 @@ def test_falsey_field_data(get, post, organization, admin, field_value): 'credential_type': net.pk, 'organization': organization.id, 'inputs': { + 'username': 'joe-user', # username is required 'authorize': field_value } } diff --git a/awx/main/tests/functional/test_credential.py b/awx/main/tests/functional/test_credential.py index cdb54c2265..4a51565d04 100644 --- a/awx/main/tests/functional/test_credential.py +++ b/awx/main/tests/functional/test_credential.py @@ -226,7 +226,7 @@ def test_credential_creation_validation_failure(organization_factory, inputs): [EXAMPLE_PRIVATE_KEY.replace('=', '\u003d'), None, True], # automatically fix JSON-encoded GCE keys ]) def test_ssh_key_data_validation(organization, kind, ssh_key_data, ssh_key_unlock, valid): - inputs = {} + inputs = {'username': 'joe-user'} if ssh_key_data: inputs['ssh_key_data'] = ssh_key_data if ssh_key_unlock: From 5ba76f28ce608deca4f0b869002b25ed7150bfd8 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 18 Aug 2017 15:50:00 -0400 Subject: [PATCH 028/138] add required fields for azure credentials see: https://github.com/ansible/ansible-tower/issues/7465 --- awx/main/models/credential.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index d2a69918ca..799b421aee 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -994,7 +994,8 @@ def azure_rm(cls): 'id': 'tenant', 'label': 'Tenant ID', 'type': 'string' - }] + }], + 'required': ['subscription'], } ) From fc73bdcc187572c645ba10a09be40653d93a48ff Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 18 Aug 2017 15:51:07 -0400 Subject: [PATCH 029/138] add required fields for azure classic credentials https://github.com/ansible/ansible-tower/issues/7464 --- awx/main/models/credential.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 799b421aee..9f66965651 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -954,7 +954,8 @@ def azure(cls): 'help_text': ('Paste the contents of the PEM file that corresponds ' 'to the certificate you uploaded in the Microsoft ' 'Azure console.') - }] + }], + 'required': ['username', 'ssh_key_data'], } ) From bcd8e13c2422968991f2ff4d77937778e7d34220 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 18 Aug 2017 15:51:58 -0400 Subject: [PATCH 030/138] add required fields for gce credentials see: https://github.com/ansible/ansible-tower/issues/7463 --- awx/main/models/credential.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 9f66965651..7a18c1bf52 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -926,7 +926,8 @@ def gce(cls): 'multiline': True, 'help_text': ('Paste the contents of the PEM file associated ' 'with the service account email.') - }] + }], + 'required': ['username', 'ssh_key_data'], } ) From b0a1988c29a05641c18dbe9c57d95059b8b2a9b8 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 18 Aug 2017 15:53:38 -0400 Subject: [PATCH 031/138] add required fields for cloudforms credentials see: https://github.com/ansible/ansible-tower/issues/7462 --- awx/main/models/credential.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 7a18c1bf52..7c1c60c47a 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -891,7 +891,8 @@ def cloudforms(cls): 'label': 'Password', 'type': 'string', 'secret': True, - }] + }], + 'required': ['host', 'username', 'password'], } ) From a1083d45c5946a2c4dd3d96ea6967a14fbc0f4c5 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Sat, 19 Aug 2017 07:05:37 -0400 Subject: [PATCH 032/138] optimizations for unified job subtypes --- awx/main/access.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/awx/main/access.py b/awx/main/access.py index bf722c0e2c..694565548f 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -834,6 +834,10 @@ class InventoryUpdateAccess(BaseAccess): def get_queryset(self): qs = InventoryUpdate.objects.distinct() qs = qs.select_related('created_by', 'modified_by', 'inventory_source__inventory') + qs = qs.prefetch_related( + 'unified_job_template', + 'instance_group' + ) inventory_sources_qs = self.user.get_queryset(InventorySource) return qs.filter(inventory_source__in=inventory_sources_qs) @@ -1080,11 +1084,17 @@ class ProjectUpdateAccess(BaseAccess): def get_queryset(self): if self.user.is_superuser or self.user.is_system_auditor: - return self.model.objects.all() - qs = ProjectUpdate.objects.distinct() + qs = self.model.objects.all() + else: + qs = self.model.objects.filter( + project__in=Project.accessible_pk_qs(self.user, 'read_role') + ) qs = qs.select_related('created_by', 'modified_by', 'project') - project_ids = set(self.user.get_queryset(Project).values_list('id', flat=True)) - return qs.filter(project_id__in=project_ids) + qs = qs.prefetch_related( + 'unified_job_template', + 'instance_group' + ) + return qs @check_superuser def can_cancel(self, obj): @@ -1304,7 +1314,11 @@ class JobAccess(BaseAccess): qs = self.model.objects qs = qs.select_related('created_by', 'modified_by', 'job_template', 'inventory', 'project', 'credential', 'job_template') - qs = qs.prefetch_related('unified_job_template') + qs = qs.prefetch_related( + 'unified_job_template', + 'instance_group', + Prefetch('labels', queryset=Label.objects.all().order_by('name')) + ) if self.user.is_superuser or self.user.is_system_auditor: return qs.all() From c352ea7596dbab80c8dca9065a518cd1df2ea368 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 21 Aug 2017 01:53:21 -0400 Subject: [PATCH 033/138] Update HostManager to return only a single matching hostname for SmartInventory filter --- awx/api/views.py | 2 +- awx/main/managers.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index ea53e4e973..c9bc76e894 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1936,7 +1936,7 @@ class HostList(ListCreateAPIView): if filter_string: filter_qs = SmartFilter.query_from_string(filter_string) qs &= filter_qs - return qs.distinct() + return qs.order_by('pk').distinct() def list(self, *args, **kwargs): try: diff --git a/awx/main/managers.py b/awx/main/managers.py index 2deeeb7046..3614caab8c 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -42,7 +42,7 @@ class HostManager(models.Manager): # injected by the related object mapper. self.core_filters = {} qs = qs & q - return qs.distinct() + return qs.order_by('pk').distinct('name') return qs From eb6a27653fa18947ae9c45b3044dc0baf069f991 Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 21 Aug 2017 04:01:58 -0400 Subject: [PATCH 034/138] Adjust HostManager and update summary host query --- awx/api/views.py | 2 +- awx/main/managers.py | 6 ++++-- awx/main/models/jobs.py | 19 +------------------ 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/awx/api/views.py b/awx/api/views.py index c9bc76e894..ea53e4e973 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1936,7 +1936,7 @@ class HostList(ListCreateAPIView): if filter_string: filter_qs = SmartFilter.query_from_string(filter_string) qs &= filter_qs - return qs.order_by('pk').distinct() + return qs.distinct() def list(self, *args, **kwargs): try: diff --git a/awx/main/managers.py b/awx/main/managers.py index 3614caab8c..d6cd6b65e0 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -40,9 +40,11 @@ class HostManager(models.Manager): # # If we don't disable this, a filter of {'inventory': self.instance} gets automatically # injected by the related object mapper. - self.core_filters = {} + self.core_filters.pop('inventory', None) + qs = qs & q - return qs.order_by('pk').distinct('name') + unique_by_name = qs.order_by('name', 'pk').distinct('name') + return qs.filter(pk__in=unique_by_name) return qs diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 235a1f648d..bf96074ba6 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -1195,25 +1195,9 @@ class JobEvent(CreatedModifiedModel): pass return hostnames - def _update_smart_inventory_hosts(self, hostnames): - '''If the job the job_event is for was run using a Smart Inventory - update the hosts fields related to job history and summary. - ''' - with ignore_inventory_computed_fields(): - if hasattr(self.job, 'inventory') and self.job.inventory.kind == 'smart': - logger.debug(self.job.inventory) - smart_hosts = self.job.inventory.hosts.filter(name__in=hostnames) - for smart_host in smart_hosts: - host_summary = self.job.job_host_summaries.get(host_name=smart_host.name) - smart_host.inventory.jobs.add(self.job) - smart_host.last_job_id = self.job_id - smart_host.last_job_host_summary_id = host_summary.pk - smart_host.save() - def _update_host_summary_from_stats(self, hostnames): with ignore_inventory_computed_fields(): - from awx.main.models.inventory import Host - qs = Host.objects.filter(inventory__jobs__id=self.job_id, name__in=hostnames) + qs = self.job.inventory.hosts.filter(name__in=hostnames) job = self.job for host in hostnames: host_stats = {} @@ -1269,7 +1253,6 @@ class JobEvent(CreatedModifiedModel): hostnames = self._hostnames() self._update_host_summary_from_stats(hostnames) - self._update_smart_inventory_hosts(hostnames) self.job.inventory.update_computed_fields() emit_channel_notification('jobs-summary', dict(group_name='jobs', unified_job_id=self.job.id)) From f8c2b466a877c93af7ef1f5055a58bf5f635207c Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 21 Aug 2017 06:03:37 -0400 Subject: [PATCH 035/138] sometimes core_filters is not an attribute, so just set it to empty instead of pop --- awx/main/managers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/managers.py b/awx/main/managers.py index d6cd6b65e0..d1f351bb6c 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -40,7 +40,7 @@ class HostManager(models.Manager): # # If we don't disable this, a filter of {'inventory': self.instance} gets automatically # injected by the related object mapper. - self.core_filters.pop('inventory', None) + self.core_filters = {} qs = qs & q unique_by_name = qs.order_by('name', 'pk').distinct('name') From ca9a1a0ca176e17da54ebdf1799efb18c8311bcb Mon Sep 17 00:00:00 2001 From: Wayne Witzel III Date: Mon, 21 Aug 2017 06:16:44 -0400 Subject: [PATCH 036/138] remove tests that execute distinct logic, sqlite3 does not support --- .../tests/functional/api/test_inventory.py | 16 ------ .../functional/api/test_script_endpoint.py | 55 ++----------------- .../tests/functional/models/test_inventory.py | 32 ----------- 3 files changed, 6 insertions(+), 97 deletions(-) diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index 8f6f6a6f22..c1b84401a5 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -204,22 +204,6 @@ def test_delete_inventory_group(delete, group, alice, role_field, expected_statu delete(reverse('api:group_detail', kwargs={'pk': group.id}), alice, expect=expected_status_code) -@pytest.mark.django_db -def test_create_inventory_smarthost(post, get, inventory, admin_user, organization): - data = { 'name': 'Host 1', 'description': 'Test Host'} - smart_inventory = Inventory(name='smart', - kind='smart', - organization=organization, - host_filter='inventory_sources__source=ec2') - smart_inventory.save() - post(reverse('api:inventory_hosts_list', kwargs={'pk': smart_inventory.id}), data, admin_user) - resp = get(reverse('api:inventory_hosts_list', kwargs={'pk': smart_inventory.id}), admin_user) - jdata = json.loads(resp.content) - - assert getattr(smart_inventory, 'kind') == 'smart' - assert jdata['count'] == 0 - - @pytest.mark.django_db def test_create_inventory_smartgroup(post, get, inventory, admin_user, organization): data = { 'name': 'Group 1', 'description': 'Test Group'} diff --git a/awx/main/tests/functional/api/test_script_endpoint.py b/awx/main/tests/functional/api/test_script_endpoint.py index eadf1868da..4423d2347f 100644 --- a/awx/main/tests/functional/api/test_script_endpoint.py +++ b/awx/main/tests/functional/api/test_script_endpoint.py @@ -7,35 +7,21 @@ from awx.main.models import Inventory, Host @pytest.mark.django_db def test_empty_inventory(post, get, admin_user, organization, group_factory): - inventory = Inventory(name='basic_inventory', - kind='', + inventory = Inventory(name='basic_inventory', + kind='', organization=organization) inventory.save() resp = get(reverse('api:inventory_script_view', kwargs={'version': 'v2', 'pk': inventory.pk}), admin_user) jdata = json.loads(resp.content) - + assert inventory.hosts.count() == 0 assert jdata == {} - - -@pytest.mark.django_db -def test_empty_smart_inventory(post, get, admin_user, organization, group_factory): - smart_inventory = Inventory(name='smart', - kind='smart', - organization=organization, - host_filter='enabled=True') - smart_inventory.save() - resp = get(reverse('api:inventory_script_view', kwargs={'version': 'v2', 'pk': smart_inventory.pk}), admin_user) - smartjdata = json.loads(resp.content) - assert smart_inventory.hosts.count() == 0 - assert smartjdata == {} - - + @pytest.mark.django_db def test_ungrouped_hosts(post, get, admin_user, organization, group_factory): - inventory = Inventory(name='basic_inventory', - kind='', + inventory = Inventory(name='basic_inventory', + kind='', organization=organization) inventory.save() Host.objects.create(name='first_host', inventory=inventory) @@ -44,32 +30,3 @@ def test_ungrouped_hosts(post, get, admin_user, organization, group_factory): jdata = json.loads(resp.content) assert inventory.hosts.count() == 2 assert len(jdata['all']['hosts']) == 2 - - -@pytest.mark.django_db -def test_grouped_hosts_smart_inventory(post, get, admin_user, organization, group_factory): - inventory = Inventory(name='basic_inventory', - kind='', - organization=organization) - inventory.save() - groupA = group_factory('test_groupA') - host1 = Host.objects.create(name='first_host', inventory=inventory) - host2 = Host.objects.create(name='second_host', inventory=inventory) - Host.objects.create(name='third_host', inventory=inventory) - groupA.hosts.add(host1) - groupA.hosts.add(host2) - smart_inventory = Inventory(name='smart_inventory', - kind='smart', - organization=organization, - host_filter='enabled=True') - smart_inventory.save() - resp = get(reverse('api:inventory_script_view', kwargs={'version': 'v2', 'pk': inventory.pk}), admin_user) - jdata = json.loads(resp.content) - resp = get(reverse('api:inventory_script_view', kwargs={'version': 'v2', 'pk': smart_inventory.pk}), admin_user) - smartjdata = json.loads(resp.content) - - assert getattr(smart_inventory, 'kind') == 'smart' - assert inventory.hosts.count() == 3 - assert len(jdata['all']['hosts']) == 1 - assert smart_inventory.hosts.count() == 3 - assert len(smartjdata['all']['hosts']) == 3 diff --git a/awx/main/tests/functional/models/test_inventory.py b/awx/main/tests/functional/models/test_inventory.py index fd31e50315..fde2723dc3 100644 --- a/awx/main/tests/functional/models/test_inventory.py +++ b/awx/main/tests/functional/models/test_inventory.py @@ -104,40 +104,8 @@ def setup_inventory_groups(inventory, group_factory): @pytest.mark.django_db class TestHostManager: - def test_host_filter_change(self, setup_ec2_gce, organization): - smart_inventory = Inventory(name='smart', - kind='smart', - organization=organization, - host_filter='inventory_sources__source=ec2') - smart_inventory.save() - assert len(smart_inventory.hosts.all()) == 2 - - smart_inventory.host_filter = 'inventory_sources__source=gce' - smart_inventory.save() - assert len(smart_inventory.hosts.all()) == 1 - def test_host_filter_not_smart(self, setup_ec2_gce, organization): smart_inventory = Inventory(name='smart', organization=organization, host_filter='inventory_sources__source=ec2') assert len(smart_inventory.hosts.all()) == 0 - - def test_host_objects_manager(self, setup_ec2_gce, organization): - smart_inventory = Inventory(kind='smart', - name='smart', - organization=organization, - host_filter='inventory_sources__source=ec2') - smart_inventory.save() - - hosts = smart_inventory.hosts.all() - assert len(hosts) == 2 - assert hosts[0].inventory_sources.first().source == 'ec2' - assert hosts[1].inventory_sources.first().source == 'ec2' - - def test_host_objects_no_dupes(self, setup_inventory_groups, organization): - smart_inventory = Inventory(name='smart', - kind='smart', - organization=organization, - host_filter='groups__name=test_groupA or groups__name=test_groupB') - smart_inventory.save() - assert len(smart_inventory.hosts.all()) == 1 From 0daa203fe076c2aac7709cb3a42a824878437833 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 21 Aug 2017 09:23:34 -0400 Subject: [PATCH 037/138] Rolled back several survey question enhancements which caused drag and drop to break --- awx/ui/client/src/shared/Utilities.js | 4 ---- .../survey-maker/render/multiple-choice.directive.js | 3 +-- .../survey-maker/render/survey-question.partial.html | 1 + 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/awx/ui/client/src/shared/Utilities.js b/awx/ui/client/src/shared/Utilities.js index 531bbc357c..c1b43c73bf 100644 --- a/awx/ui/client/src/shared/Utilities.js +++ b/awx/ui/client/src/shared/Utilities.js @@ -640,10 +640,6 @@ angular.module('Utilities', ['RestServices', 'Utilities']) // Don't toggle the dropdown when a multiselect option is // being removed if (multiple) { - if (params.disabledOptions) { - $(element).on('select2:selecting', e => e.preventDefault()); - } - $(element).on('select2:opening', (e) => { var unselecting = $(e.target).data('select2-unselecting'); if (unselecting === true) { diff --git a/awx/ui/client/src/templates/survey-maker/render/multiple-choice.directive.js b/awx/ui/client/src/templates/survey-maker/render/multiple-choice.directive.js index 2f782c84a6..9eccb7c195 100644 --- a/awx/ui/client/src/templates/survey-maker/render/multiple-choice.directive.js +++ b/awx/ui/client/src/templates/survey-maker/render/multiple-choice.directive.js @@ -17,8 +17,7 @@ function link($timeout, CreateSelect2, scope, element, attrs, ngModel) { element: element.find('select'), multiple: scope.isMultipleSelect(), minimumResultsForSearch: scope.isMultipleSelect() ? Infinity : 10, - customDropdownAdapter: true, - disabledOptions: scope.preview ? true : false + customDropdownAdapter: scope.preview ? false : true }); }); diff --git a/awx/ui/client/src/templates/survey-maker/render/survey-question.partial.html b/awx/ui/client/src/templates/survey-maker/render/survey-question.partial.html index fe42936d0e..0be50a89b2 100644 --- a/awx/ui/client/src/templates/survey-maker/render/survey-question.partial.html +++ b/awx/ui/client/src/templates/survey-maker/render/survey-question.partial.html @@ -11,6 +11,7 @@ choices="choices" ng-required="isRequired === 'true'" ng-model="defaultValue" + ng-disabled="isDisabled === 'true'" preview="preview">
From ff51fe30503905e9fd1225f01fe6ae84c89e115c Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Fri, 18 Aug 2017 11:14:20 -0400 Subject: [PATCH 038/138] Prevent accessing None attributes in host summary __unicode__ --- awx/main/models/jobs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 18202e18a9..235a1f648d 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -830,8 +830,9 @@ class JobHostSummary(CreatedModifiedModel): failed = models.BooleanField(default=False, editable=False) def __unicode__(self): + hostname = self.host.name if self.host else 'N/A' return '%s changed=%d dark=%d failures=%d ok=%d processed=%d skipped=%s' % \ - (self.host.name, self.changed, self.dark, self.failures, self.ok, + (hostname, self.changed, self.dark, self.failures, self.ok, self.processed, self.skipped) def get_absolute_url(self, request=None): From 0b68ad9b10f05fe75d655b0f8dc1c2c9bc2ddc12 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Mon, 21 Aug 2017 10:13:02 -0400 Subject: [PATCH 039/138] properly sanitize conf.settings debug logs cache.set() and cache.get() arguments are logged when the log level is DEBUG; this _may_ include plaintext secrets; strip sensitive values before logging them see: https://github.com/ansible/ansible-tower/issues/7476 --- awx/conf/settings.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/awx/conf/settings.py b/awx/conf/settings.py index d288ba3a16..eefcfd2da7 100644 --- a/awx/conf/settings.py +++ b/awx/conf/settings.py @@ -69,6 +69,12 @@ def _log_database_error(): pass +def filter_sensitive(registry, key, value): + if registry.is_setting_encrypted(key): + return '$encrypted$' + return value + + class EncryptedCacheProxy(object): def __init__(self, cache, registry, encrypter=None, decrypter=None): @@ -105,9 +111,13 @@ class EncryptedCacheProxy(object): six.text_type(value) except UnicodeDecodeError: value = value.decode('utf-8') + logger.debug('cache get(%r, %r) -> %r', key, empty, filter_sensitive(self.registry, key, value)) return value - def set(self, key, value, **kwargs): + def set(self, key, value, log=True, **kwargs): + if log is True: + logger.debug('cache set(%r, %r, %r)', key, filter_sensitive(self.registry, key, value), + SETTING_CACHE_TIMEOUT) self.cache.set( key, self._handle_encryption(self.encrypter, key, value), @@ -115,8 +125,13 @@ class EncryptedCacheProxy(object): ) def set_many(self, data, **kwargs): + filtered_data = dict( + (key, filter_sensitive(self.registry, key, value)) + for key, value in data.items() + ) + logger.debug('cache set_many(%r, %r)', filtered_data, SETTING_CACHE_TIMEOUT) for key, value in data.items(): - self.set(key, value, **kwargs) + self.set(key, value, log=False, **kwargs) def _handle_encryption(self, method, key, value): TransientSetting = namedtuple('TransientSetting', ['pk', 'value']) @@ -277,7 +292,6 @@ class SettingsWrapper(UserSettingsHolder): logger.debug('Saving id in cache for encrypted setting %s', k) self.cache.set(Setting.get_cache_id_key(k), id_val) settings_to_cache['_awx_conf_preload_expires'] = self._awx_conf_preload_expires - logger.debug('cache set_many(%r, %r)', settings_to_cache, SETTING_CACHE_TIMEOUT) self.cache.set_many(settings_to_cache, timeout=SETTING_CACHE_TIMEOUT) def _get_local(self, name): @@ -287,7 +301,6 @@ class SettingsWrapper(UserSettingsHolder): cache_value = self.cache.get(cache_key, default=empty) except ValueError: cache_value = empty - logger.debug('cache get(%r, %r) -> %r', cache_key, empty, cache_value) if cache_value == SETTING_CACHE_NOTSET: value = empty elif cache_value == SETTING_CACHE_NONE: @@ -326,9 +339,6 @@ class SettingsWrapper(UserSettingsHolder): if value is None and SETTING_CACHE_NOTSET == SETTING_CACHE_NONE: value = SETTING_CACHE_NOTSET if cache_value != value: - logger.debug('cache set(%r, %r, %r)', cache_key, - get_cache_value(value), - SETTING_CACHE_TIMEOUT) if setting_id: logger.debug('Saving id in cache for encrypted setting %s', cache_key) self.cache.set(Setting.get_cache_id_key(cache_key), setting_id) From d243b587f4a859bac40a7d08a1f6bc12790e0d44 Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Mon, 21 Aug 2017 10:27:42 -0400 Subject: [PATCH 040/138] Update Tower Configuration help text to remove \n escape. --- awx/ui/conf.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/awx/ui/conf.py b/awx/ui/conf.py index 00f1e043d8..8a8677cdf1 100644 --- a/awx/ui/conf.py +++ b/awx/ui/conf.py @@ -32,9 +32,7 @@ register( help_text=_('If needed, you can add specific information (such as a legal ' 'notice or a disclaimer) to a text box in the login modal using ' 'this setting. Any content added must be in plain text, as ' - 'custom HTML or other markup languages are not supported. If ' - 'multiple paragraphs of text are needed, new lines (paragraphs) ' - 'must be escaped as `\\n` within the block of text.'), + 'custom HTML or other markup languages are not supported.'), category=_('UI'), category_slug='ui', feature_required='rebranding', From 9b84670cf09cb51742af1b4b4d6e35f4fda54550 Mon Sep 17 00:00:00 2001 From: adamscmRH Date: Wed, 16 Aug 2017 11:32:16 -0400 Subject: [PATCH 041/138] Removes auth field from Access List serializer --- awx/api/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index f36688ce46..fca1fa38d3 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -779,10 +779,10 @@ class UserSerializer(BaseSerializer): 'username', 'first_name', 'last_name', 'email', 'is_superuser', 'is_system_auditor', 'password', 'ldap_dn', 'external_account') - def to_representation(self, obj): + def to_representation(self, obj): # TODO: Remove in 3.3 ret = super(UserSerializer, self).to_representation(obj) ret.pop('password', None) - if obj: + if obj and type(self) is UserSerializer or self.version == 1: ret['auth'] = obj.social_auth.values('provider', 'uid') return ret From 7a5899a968acf3d8e903b5b717c0db86fc25ed11 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 18 Aug 2017 15:27:12 -0400 Subject: [PATCH 042/138] popover content audit for JT form --- .../job_templates/job-template.form.js | 37 +++++-------------- 1 file changed, 9 insertions(+), 28 deletions(-) diff --git a/awx/ui/client/src/templates/job_templates/job-template.form.js b/awx/ui/client/src/templates/job_templates/job-template.form.js index a25cfeccd8..b8092c37a2 100644 --- a/awx/ui/client/src/templates/job_templates/job-template.form.js +++ b/awx/ui/client/src/templates/job_templates/job-template.form.js @@ -51,11 +51,7 @@ function(NotificationsList, CompletedJobsList, i18n) { "default": 0, required: true, column: 1, - awPopOver: "

" + i18n.sprintf(i18n._("When this template is submitted as a job, setting the type to %s will execute the playbook, running tasks " + - " on the selected hosts."), "run") + "

" + - i18n.sprintf(i18n._("Setting the type to %s will not execute the playbook."), "check") + " " + - i18n.sprintf(i18n._("Instead, %s will check playbook " + - " syntax, test environment setup and report problems."), "ansible") + "

", + awPopOver: i18n._('For job templates, select run to execute the playbook. Select check to only check playbook syntax, test environment setup, and report problems without executing the playbook.'), dataTitle: i18n._('Job Type'), dataPlacement: 'right', dataContainer: "body", @@ -139,7 +135,7 @@ function(NotificationsList, CompletedJobsList, i18n) { field-is-disabled="!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)"> `, required: true, - awPopOver: "

" + i18n._("Select credentials that allow {{BRAND_NAME}} to access the nodes this job will be ran against. You can only select one credential of each type.

You must select either a machine (SSH) credential or \"Prompt on launch\". \"Prompt on launch\" requires you to select a machine credential at run time.

If you select credentials AND check the \"Prompt on launch\" box, you make the selected credentials the defaults that can be updated at run time.") + "

", + awPopOver: i18n._('Select credentials that allow Tower to access the nodes this job will be ran against. You can only select one credential of each type. For machine credentials (SSH), checking "Prompt on launch" without selecting credentials will require you to select a machine credential at run time. If you select credentials and check "Prompt on launch", the selected credential(s) become the defaults that can be updated at run time.'), dataTitle: i18n._('Credentials'), dataPlacement: 'right', dataContainer: "body", @@ -158,10 +154,7 @@ function(NotificationsList, CompletedJobsList, i18n) { spinner: true, 'class': "input-small", column: 1, - awPopOver: '

' + i18n.sprintf(i18n._('The number of parallel or simultaneous processes to use while executing the playbook. Inputting no value will use ' + - 'the default value from the %sansible configuration file%s.'), '' + - '', '') +'

', + awPopOver: i18n._('The number of parallel or simultaneous processes to use while executing the playbook. Value defaults to 0. Refer to the Ansible documentation for details about the configuration file.'), placeholder: 'DEFAULT', dataTitle: i18n._('Forks'), dataPlacement: 'right', @@ -172,10 +165,7 @@ function(NotificationsList, CompletedJobsList, i18n) { label: i18n._('Limit'), type: 'text', column: 1, - awPopOver: "

" + i18n.sprintf(i18n._("Provide a host pattern to further constrain the list of hosts that will be managed or affected by the playbook. " + - "Multiple patterns can be separated by %s %s or %s"), ";", ":", ",") + "

" + - i18n.sprintf(i18n._("For more information and examples see " + - "%sthe Patterns topic at docs.ansible.com%s."), "", "") + "

", + awPopOver: i18n._('Provide a host pattern to further constrain the list of hosts that will be managed or affected by the playbook. Multiple patterns are allowed. Refer to Ansible documentation for more information and examples on patterns.'), dataTitle: i18n._('Limit'), dataPlacement: 'right', dataContainer: "body", @@ -218,9 +208,7 @@ function(NotificationsList, CompletedJobsList, i18n) { 'elementClass': 'Form-textInput', ngOptions: 'tag.label for tag in job_tag_options track by tag.value', column: 2, - awPopOver: "

" + i18n._("Provide a comma separated list of tags.") + "

\n" + - "

" + i18n._("Tags are useful when you have a large playbook, and you want to run a specific part of a play or task.") + "

" + - "

" + i18n._("Consult the Ansible documentation for further details on the usage of tags.") + "

", + awPopOver: i18n._('Tags are useful when you have a large playbook, and you want to run a specific part of a play or task. Use commas to separate multiple tags. Refer to Ansible Tower documentation for details on the usage of tags.'), dataTitle: i18n._("Job Tags"), dataPlacement: "right", dataContainer: "body", @@ -237,9 +225,7 @@ function(NotificationsList, CompletedJobsList, i18n) { 'elementClass': 'Form-textInput', ngOptions: 'tag.label for tag in skip_tag_options track by tag.value', column: 2, - awPopOver: "

" + i18n._("Provide a comma separated list of tags.") + "

\n" + - "

" + i18n._("Skip tags are useful when you have a large playbook, and you want to skip specific parts of a play or task.") + "

" + - "

" + i18n._("Consult the Ansible documentation for further details on the usage of tags.") + "

", + awPopOver: i18n._('Skip tags are useful when you have a large playbook, and you want to skip specific parts of a play or task. Use commas to separate multiple tags. Refer to Ansible Tower documentation for details on the usage of tags.'), dataTitle: i18n._("Skip Tags"), dataPlacement: "right", dataContainer: "body", @@ -282,9 +268,9 @@ function(NotificationsList, CompletedJobsList, i18n) { label: i18n._('Enable Privilege Escalation'), type: 'checkbox', column: 2, - awPopOver: "

" + i18n.sprintf(i18n._("If enabled, run this playbook as an administrator. This is the equivalent of passing the %s option to the %s command."), '--become', 'ansible-playbook') + "

", + awPopOver: i18n._('If enabled, run this playbook as an administrator.'), dataPlacement: 'right', - dataTitle: i18n._('Become Privilege Escalation'), + dataTitle: i18n._('Enable Privilege Escalation'), dataContainer: "body", labelClass: 'stack-inline', ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAddJobTemplate)' @@ -363,12 +349,7 @@ function(NotificationsList, CompletedJobsList, i18n) { rows: 6, "default": "---", column: 2, - awPopOver: "

" + i18n.sprintf(i18n._("Pass extra command line variables to the playbook. This is the %s or %s command line parameter " + - "for %s. Provide key/value pairs using either YAML or JSON."), '-e', '--extra-vars', 'ansible-playbook') + "

" + - "JSON:
\n" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n", + awPopOver: i18n._('Pass extra command line variables to the playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Tower documentation for example syntax.'), dataTitle: i18n._('Extra Variables'), dataPlacement: 'right', dataContainer: "body", From 50852e406adfc1aca760d7c1108b655d57958307 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 18 Aug 2017 15:44:10 -0400 Subject: [PATCH 043/138] popover content for notification templates form --- .../notificationTemplates.form.js | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/awx/ui/client/src/notifications/notificationTemplates.form.js b/awx/ui/client/src/notifications/notificationTemplates.form.js index 0e15356be5..3f4e01ed55 100644 --- a/awx/ui/client/src/notifications/notificationTemplates.form.js +++ b/awx/ui/client/src/notifications/notificationTemplates.form.js @@ -97,8 +97,7 @@ export default ['i18n', function(i18n) { label: i18n._('Recipient List'), type: 'textarea', rows: 3, - awPopOver: '

' + i18n._('Type an option on each line.') + '

'+ - '

' + i18n._('For example:') + '
alias1@email.com
\n alias2@email.com
\n', + awPopOver: i18n._('Enter one email address per line to create a recipient list for this type of notification.'), dataTitle: i18n._('Recipient List'), dataPlacement: 'right', dataContainer: "body", @@ -140,8 +139,7 @@ export default ['i18n', function(i18n) { label: i18n._('Destination Channels'), type: 'textarea', rows: 3, - awPopOver: '

' + i18n._('Type an option on each line. The pound symbol (#) is not required.') + '

'+ - '

' + i18n._('For example:') + '
engineering
\n #support
\n', + awPopOver: i18n._('Enter one Slack channel per line. The pound symbol (#) is not required.'), dataTitle: i18n._('Destination Channels'), dataPlacement: 'right', dataContainer: "body", @@ -157,8 +155,7 @@ export default ['i18n', function(i18n) { label: i18n._('Destination Channels'), type: 'textarea', rows: 3, - awPopOver: '

' + i18n._('Type an option on each line. The pound symbol (#) is not required.') + '

'+ - '

' + i18n._('For example:') + '
engineering
\n #support
\n', + awPopOver: i18n._('Enter one HipChat channel per line. The pound symbol (#) is not required.'), dataTitle: i18n._('Destination Channels'), dataPlacement: 'right', dataContainer: "body", @@ -198,8 +195,7 @@ export default ['i18n', function(i18n) { label: i18n._('Source Phone Number'), dataTitle: i18n._('Source Phone Number'), type: 'text', - awPopOver: '

' + i18n._('Number associated with the "Messaging Service" in Twilio.') + '

'+ - '

' + i18n.sprintf(i18n._('This must be of the form %s.'), '+18005550199') + '

', + awPopOver: i18n._('Enter the number associated with the "Messaging Service" in Twilio in the format +18005550199.'), awRequiredWhen: { reqExpression: "twilio_required", init: "false" @@ -213,8 +209,7 @@ export default ['i18n', function(i18n) { dataTitle: i18n._('Destination SMS Number'), type: 'textarea', rows: 3, - awPopOver: '

' + i18n._('Type an option on each line.') + '

'+ - '

' + i18n._('For example:') + '
+12125552368
\n+19105556162
\n', + awPopOver: i18n._('Enter one phone number per line to specify where to route SMS messages.'), dataPlacement: 'right', dataContainer: "body", awRequiredWhen: { @@ -297,8 +292,7 @@ export default ['i18n', function(i18n) { dataTitle: i18n._('Notification Color'), type: 'select', ngOptions: 'color for color in hipchatColors track by color', - awPopOver: '

' + i18n.sprintf(i18n._('Color can be one of %s.'), 'yellow, green, red, ' + - 'purple, gray, random') + '\n', + awPopOver: i18n._('Specify a notification color. Acceptable colors are: yellow, green, red purple, gray or random.'), awRequiredWhen: { reqExpression: "hipchat_required", init: "false" @@ -336,13 +330,7 @@ export default ['i18n', function(i18n) { reqExpression: "webhook_required", init: "false" }, - awPopOver: '

' + i18n._('Specify HTTP Headers in JSON format') + '

' + - '

' + i18n._('For example:') + '

\n' +
-                           '{\n' +
-                           '  "X-Auth-Token": "828jf0",\n' +
-                           '  "X-Ansible": "Is great!"\n' +
-                           '}\n' +
-                           '

', + awPopOver: i18n._('Specify HTTP Headers in JSON format. Refer to the Ansible Tower documentation for example syntax.'), dataPlacement: 'right', ngShow: "notification_type.value == 'webhook' ", subForm: 'typeSubForm', @@ -374,9 +362,8 @@ export default ['i18n', function(i18n) { label: i18n._('Destination Channels or Users'), type: 'textarea', rows: 3, - awPopOver: '

' + i18n._('Type an option on each line. The pound symbol (#) is not required.') + '

'+ - '

' + i18n._('For example:') + '
' + i18n.sprintf(i18n._('%s or %s'), '#support', 'support') + '
\n ' + i18n.sprintf(i18n._('%s or %s'), '@username', 'username') + '
\n', - dataTitle: i18n._('Destination Channels'), + awPopOver: i18n._('Enter one IRC channel or username per line. The pound symbol (#) for channels, and the at (@) symbol for users, are not required.'), + dataTitle: i18n._('Destination Channels or Users'), dataPlacement: 'right', dataContainer: "body", awRequiredWhen: { From 22afd8b0449d85cd2f60ef342a515221df096378 Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 18 Aug 2017 15:48:34 -0400 Subject: [PATCH 044/138] tooltip content for inventory scripts form --- awx/ui/client/src/inventory-scripts/inventory-scripts.form.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js b/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js index 644f4ecdee..f7f1b581d5 100644 --- a/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js +++ b/awx/ui/client/src/inventory-scripts/inventory-scripts.form.js @@ -59,8 +59,7 @@ export default ['i18n', function(i18n) { ngDisabled: '!(inventory_script_obj.summary_fields.user_capabilities.edit || canAdd)', ngTrim: false, rows: 10, - awPopOver: "

" + i18n._("Drag and drop your custom inventory script file here or create one in the field to import your custom inventory.") + " " + - "

" + i18n.sprintf(i18n._("Script must begin with a hashbang sequence: i.e.... %s"), "#!/usr/bin/env python") + "

", + awPopOver: i18n._('Drag and drop your custom inventory script file here or create one in the field to import your custom inventory. Refer to the Ansible Tower documentation for example syntax.'), dataTitle: i18n._('Custom Script'), dataPlacement: 'right', dataContainer: "body" From a589bc4562b561ee49513ba73de61c61aaee7e4d Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 18 Aug 2017 15:58:22 -0400 Subject: [PATCH 045/138] tooltip content audit for inventory form --- .../inventories/standard-inventory/inventory.form.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js b/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js index fa4ed8d106..0562137f9e 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/standard-inventory/inventory.form.js @@ -82,7 +82,7 @@ function(i18n, InventoryCompletedJobsList) { instance_groups: { label: i18n._('Instance Groups'), type: 'custom', - awPopOver: "

" + i18n._("Select the Instance Groups for this Inventory to run on.") + "

", + awPopOver: i18n._('Select the Instance Groups for this Inventory to run on. Refer to the Ansible Tower documentation for more detail.'), dataTitle: i18n._('Instance Groups'), dataPlacement: 'right', dataContainer: 'body', @@ -95,14 +95,8 @@ function(i18n, InventoryCompletedJobsList) { class: 'Form-formGroup--fullWidth', rows: 6, "default": "---", - awPopOver: "

" + i18n._("Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two.") + "

" + - "JSON:
\n" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n" + - '

' + i18n.sprintf(i18n._('View JSON examples at %s'), 'www.json.org') + '

' + - '

' + i18n.sprintf(i18n._('View YAML examples at %s'), 'docs.ansible.com') + '

', - dataTitle: i18n._('Inventory Variables'), + awPopOver: i18n._('Enter inventory variables using either JSON or YAML syntax. Use the radio button to toggle between the two. Refer to the Ansible Tower documentation for example syntax.'), + dataTitle: i18n._('Variables'), dataPlacement: 'right', dataContainer: 'body', ngDisabled: '!(inventory_obj.summary_fields.user_capabilities.edit || canAdd)' // TODO: get working From e5efbcf42f97fe40428c9dba49a5fd4801b45fbb Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 18 Aug 2017 16:05:48 -0400 Subject: [PATCH 046/138] tooltip content audit for workflows form --- awx/ui/client/src/templates/workflows.form.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/awx/ui/client/src/templates/workflows.form.js b/awx/ui/client/src/templates/workflows.form.js index b03b7bef47..106d011cb6 100644 --- a/awx/ui/client/src/templates/workflows.form.js +++ b/awx/ui/client/src/templates/workflows.form.js @@ -77,12 +77,7 @@ export default ['NotificationsList', 'i18n', function(NotificationsList, i18n) { rows: 6, "default": "---", column: 2, - awPopOver: "

" + i18n.sprintf(i18n._("Pass extra command line variables to the playbook. This is the %s or %s command line parameter " + - "for %s. Provide key/value pairs using either YAML or JSON."), "-e", "--extra-vars", "ansible-playbook") + "

" + - "JSON:
\n" + - "
{
 \"somevar\": \"somevalue\",
 \"password\": \"magic\"
}
\n" + - "YAML:
\n" + - "
---
somevar: somevalue
password: magic
\n", + awPopOver:i18n._('Pass extra command line variables to the playbook. This is the -e or --extra-vars command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON. Refer to the Ansible Tower documentaton for example syntax.'), dataTitle: i18n._('Extra Variables'), dataPlacement: 'right', dataContainer: "body", From b281b563e0cdfa521208f84343771a2d22776e4e Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 18 Aug 2017 16:18:44 -0400 Subject: [PATCH 047/138] tooltip content audit for survey maker --- .../shared/question-definition.form.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/awx/ui/client/src/templates/survey-maker/shared/question-definition.form.js b/awx/ui/client/src/templates/survey-maker/shared/question-definition.form.js index 764bbf439d..f63df7978c 100644 --- a/awx/ui/client/src/templates/survey-maker/shared/question-definition.form.js +++ b/awx/ui/client/src/templates/survey-maker/shared/question-definition.form.js @@ -42,8 +42,7 @@ export default ['i18n', function(i18n){ realName: 'variable', type: 'custom', control:''+ '
'+ '
Please enter an answer variable name.
'+ @@ -62,7 +61,10 @@ export default ['i18n', function(i18n){ defaultText: i18n._('Choose an answer type'), ngOptions: 'answer_types.name for answer_types in answer_types track by answer_types.type', required: true, - + awPopOver: i18n._('Choose an answer type or format you want as the prompt for the user. Refer to the Ansible Tower Documentation for more additional information about each option.'), + dataTitle: i18n._('Answer Type'), + dataPlacement: 'right', + dataContainer: "body", column: 2, ngChange: 'typeChange()', class: 'Form-formGroup--singleColumn' @@ -76,12 +78,6 @@ export default ['i18n', function(i18n){ ngRequired: "type.type=== 'multiselect' || type.type=== 'multiplechoice' " , ngShow: 'type.type=== "multiselect" || type.type=== "multiplechoice" ', - awPopOver: '

Type an option on each line.

'+ - '

For example the following input:

Apple
\n Banana
\n Cherry

would be displayed as:

\n'+ - '
  1. Apple
  2. Banana
  3. Cherry
', - dataTitle: i18n._('Multiple Choice Options'), - dataPlacement: 'right', - dataContainer: "body", column: 2, class: 'Form-formGroup--singleColumn' }, From 3d03855cd92391aa5bde530d0ebf27c8fc795956 Mon Sep 17 00:00:00 2001 From: mabashian Date: Mon, 21 Aug 2017 11:16:06 -0400 Subject: [PATCH 048/138] Fix management job scheduler time inputs by resetting invalid input to 00 --- .../management-jobs/scheduler/schedulerForm.partial.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html b/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html index a4704cc3de..9723193d39 100644 --- a/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html +++ b/awx/ui/client/src/management-jobs/scheduler/schedulerForm.partial.html @@ -75,7 +75,7 @@ placeholder="HH24" aw-min="0" min="0" aw-max="23" max="23" data-zero-pad="2" required - ng-change="scheduleTimeChange()" > + ng-change="timeChange()" > : @@ -91,7 +91,7 @@ placeholder="MM" min="0" max="59" data-zero-pad="2" required - ng-change="scheduleTimeChange()" > + ng-change="timeChange()" > : @@ -107,7 +107,7 @@ placeholder="SS" min="0" max="59" data-zero-pad="2" required - ng-change="scheduleTimeChange()" > + ng-change="timeChange()" > - {{ instance.hostname }} - - {{ instance.consumed_capacity }}% + {{ instance.hostname }} {{ instance.jobs_running }} + + {{ instance.consumed_capacity }}% + diff --git a/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html b/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html index 0d358f802f..1a5f8bad1a 100644 --- a/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html +++ b/awx/ui/client/src/instance-groups/list/instance-groups-list.partial.html @@ -26,12 +26,12 @@ "{{'Name' | translate}}" - - Used Capacity - Running Jobs + + Used Capacity + @@ -41,14 +41,14 @@ {{ instance_group.name }} {{ instance_group.instances }} - - {{ instance_group.consumed_capacity }}% - {{ instance_group.jobs_running }} + + {{ instance_group.consumed_capacity }}% + From 5cbdadc3e8338f26fd7c061c269f0360016fa0a3 Mon Sep 17 00:00:00 2001 From: Ryan Fitzpatrick Date: Thu, 31 Aug 2017 13:17:16 -0400 Subject: [PATCH 123/138] Fix typo in scan_packages plugin --- awx/plugins/library/scan_packages.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/plugins/library/scan_packages.py b/awx/plugins/library/scan_packages.py index 5195a051a6..3fd2edc1fa 100755 --- a/awx/plugins/library/scan_packages.py +++ b/awx/plugins/library/scan_packages.py @@ -74,7 +74,7 @@ def rpm_package_list(): def deb_package_list(): import apt apt_cache = apt.Cache() - installed_packages = [] + installed_packages = {} apt_installed_packages = [pk for pk in apt_cache.keys() if apt_cache[pk].is_installed] for package in apt_installed_packages: ac_pkg = apt_cache[package].installed From 839f3a4d2c58d9c846d1a64941903c8b34d38e39 Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Thu, 31 Aug 2017 14:39:39 -0400 Subject: [PATCH 124/138] Enhace query string in job event save to consider smart inventory --- awx/main/models/jobs.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index 455286ec9d..b9e6a00359 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -1166,7 +1166,6 @@ class JobEvent(CreatedModifiedModel): def _update_hosts(self, extra_host_pks=None): # Update job event hosts m2m from host_name, propagate to parent events. - from awx.main.models.inventory import Host extra_host_pks = set(extra_host_pks or []) hostnames = set() if self.host_name: @@ -1177,7 +1176,7 @@ class JobEvent(CreatedModifiedModel): hostnames.update(v.keys()) except AttributeError: # In case event_data or v isn't a dict. pass - qs = Host.objects.filter(inventory__jobs__id=self.job_id) + qs = self.job.inventory.hosts.all() qs = qs.filter(Q(name__in=hostnames) | Q(pk__in=extra_host_pks)) qs = qs.exclude(job_events__pk=self.id).only('id') for host in qs: @@ -1224,7 +1223,6 @@ class JobEvent(CreatedModifiedModel): host_summary.save(update_fields=update_fields) def save(self, *args, **kwargs): - from awx.main.models.inventory import Host # If update_fields has been specified, add our field names to it, # if it hasn't been specified, then we're just doing a normal save. update_fields = kwargs.get('update_fields', []) @@ -1239,7 +1237,7 @@ class JobEvent(CreatedModifiedModel): update_fields.append(field) # Update host related field from host_name. if not self.host_id and self.host_name: - host_qs = Host.objects.filter(inventory__jobs__id=self.job_id, name=self.host_name) + host_qs = self.job.inventory.hosts.filter(name=self.host_name) host_id = host_qs.only('id').values_list('id', flat=True).first() if host_id != self.host_id: self.host_id = host_id From 1617700ee0035db8e37b2d8f215b666cfe7a1383 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 31 Aug 2017 14:42:36 -0400 Subject: [PATCH 125/138] Removed extra refresh call --- awx/ui/client/src/projects/list/projects-list.controller.js | 1 - 1 file changed, 1 deletion(-) diff --git a/awx/ui/client/src/projects/list/projects-list.controller.js b/awx/ui/client/src/projects/list/projects-list.controller.js index fe8b9b9c42..9ce0f5b807 100644 --- a/awx/ui/client/src/projects/list/projects-list.controller.js +++ b/awx/ui/client/src/projects/list/projects-list.controller.js @@ -220,7 +220,6 @@ export default ['$scope', '$rootScope', '$log', 'Rest', 'Alert', Rest.post() .success(function () { Alert(i18n._('SCM Update Cancel'), i18n._('Your request to cancel the update was submitted to the task manager.'), 'alert-info'); - $scope.refresh(); }) .error(function (data, status) { ProcessErrors($scope, data, status, null, { hdr: i18n._('Error!'), msg: i18n.sprintf(i18n._('Call to %s failed. POST status: '), url) + status }); From 41799521c6bf338e2a20c4c44c47d8785794aa66 Mon Sep 17 00:00:00 2001 From: mabashian Date: Thu, 31 Aug 2017 14:49:40 -0400 Subject: [PATCH 126/138] Removed rogue console.logs --- .../hosts/related/groups/hosts-related-groups.controller.js | 2 +- .../job-submission-factories/check-passwords.factory.js | 2 +- awx/ui/client/src/shared/form-generator.js | 2 +- awx/ui/client/src/users/add/users-add.controller.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/hosts/related/groups/hosts-related-groups.controller.js b/awx/ui/client/src/inventories-hosts/hosts/related/groups/hosts-related-groups.controller.js index 6e346c5b4f..81ae0ff882 100644 --- a/awx/ui/client/src/inventories-hosts/hosts/related/groups/hosts-related-groups.controller.js +++ b/awx/ui/client/src/inventories-hosts/hosts/related/groups/hosts-related-groups.controller.js @@ -56,7 +56,7 @@ $state.go('inventories.edit.groups.edit', {inventory_id: $scope.inventory_id, group_id: id}); }; - $scope.goToGroupGroups = function(id){console.log(); + $scope.goToGroupGroups = function(id){ $state.go('inventories.edit.groups.edit.nested_groups', {inventory_id: $scope.inventory_id, group_id: id}); }; diff --git a/awx/ui/client/src/job-submission/job-submission-factories/check-passwords.factory.js b/awx/ui/client/src/job-submission/job-submission-factories/check-passwords.factory.js index 06d6566168..850b043663 100644 --- a/awx/ui/client/src/job-submission/job-submission-factories/check-passwords.factory.js +++ b/awx/ui/client/src/job-submission/job-submission-factories/check-passwords.factory.js @@ -12,7 +12,7 @@ export default .success(function (data) { credentialTypesLookup() .then(kinds => { - if(data.credential_type === kinds.Machine && data.inputs){console.log(data.inputs); + if(data.credential_type === kinds.Machine && data.inputs){ if(data.inputs.password === "ASK" ){ passwords.push("ssh_password"); } diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 704ed857f9..d548cffc13 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -1351,7 +1351,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += (field.placeholder) ? this.attr(field, 'placeholder') : ""; html += (options.mode === 'edit' && field.editRequired) ? "required " : ""; html += (field.readonly || field.showonly) ? "readonly " : ""; - if(field.awRequiredWhen) {console.log(field.awRequiredWhen); + if(field.awRequiredWhen) { html += field.awRequiredWhen.init ? "data-awrequired-init=\"" + field.awRequiredWhen.init + "\" " : ""; html += field.awRequiredWhen.reqExpression ? "aw-required-when=\"" + field.awRequiredWhen.reqExpression + "\" " : ""; html += field.awRequiredWhen.alwaysShowAsterisk ? "data-awrequired-always-show-asterisk=true " : ""; diff --git a/awx/ui/client/src/users/add/users-add.controller.js b/awx/ui/client/src/users/add/users-add.controller.js index de1fa32f09..0f425bb316 100644 --- a/awx/ui/client/src/users/add/users-add.controller.js +++ b/awx/ui/client/src/users/add/users-add.controller.js @@ -17,7 +17,7 @@ export default ['$scope', '$rootScope', 'UserForm', 'GenerateForm', 'Rest', 'Wait', 'CreateSelect2', '$state', '$location', 'i18n', function($scope, $rootScope, UserForm, GenerateForm, Rest, Alert, ProcessErrors, ReturnToCaller, GetBasePath, Wait, CreateSelect2, - $state, $location, i18n) {console.log($scope); + $state, $location, i18n) { var defaultUrl = GetBasePath('organizations'), form = UserForm; From faa45cc606d2904613ad358395c50bd7ce510496 Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Thu, 31 Aug 2017 15:34:18 -0400 Subject: [PATCH 127/138] Align key toggle button to role dropdown in user team permissions modal --- .../rbac-user-team.partial.html | 60 +++++++++---------- awx/ui/client/src/access/add-rbac.block.less | 9 +++ 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html index 23d6ed36b1..276184ec21 100644 --- a/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html +++ b/awx/ui/client/src/access/add-rbac-user-team/rbac-user-team.partial.html @@ -98,11 +98,6 @@ 2 Please assign roles to the selected resources -
- Key -
-
-
+
+ +
+ +
+
+ Key +
+
+ +
+
{{ key.name }}
@@ -158,29 +168,17 @@
+
- -
- -
- - - -
- - -
- + + +
+ +
diff --git a/awx/ui/client/src/access/add-rbac.block.less b/awx/ui/client/src/access/add-rbac.block.less index 177388c07e..1f9ea39ea2 100644 --- a/awx/ui/client/src/access/add-rbac.block.less +++ b/awx/ui/client/src/access/add-rbac.block.less @@ -220,3 +220,12 @@ .AddPermissions-keyDescription { flex: 1 0 auto; } + +.AddPermissions-roleSet { + display: flex; + + .AddPermissions-roleSet-dropdown { + flex: 1; + margin-right: 20px; + } +} \ No newline at end of file From a3c8bd6b6f100f4e5388aac1df5f4645b8d9b07a Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Mon, 28 Aug 2017 15:46:07 -0700 Subject: [PATCH 128/138] On JT form, Show credential tags from summary_fields if user doesn't have view permission on the credential --- .../multi-credential.service.js | 62 ++++++++++++------- 1 file changed, 40 insertions(+), 22 deletions(-) diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js index e114b92050..362c9d7e4b 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js @@ -177,7 +177,7 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro }, credTypes, credTypeOptions, credTags; let credDefers = []; - + let job_template_obj = data; // get machine credential if (data.related.credential) { Rest.setUrl(data.related.credential); @@ -186,14 +186,20 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro selectedCredentials.machine = data; }) .catch(({data, status}) => { - ProcessErrors( - null, data, status, null, - { - hdr: 'Error!', - msg: 'Failed to get machine credential. ' + - 'Get returned status: ' + - status + if (status === 403) { + /* User doesn't have read access to the machine credential, so use summary_fields */ + selectedCredentials.machine = job_template_obj.summary_fields.credential; + selectedCredentials.machine.credential_type = job_template_obj.summary_fields.credential.credential_type_id; + } else { + ProcessErrors( + null, data, status, null, + { + hdr: 'Error!', + msg: 'Failed to get machine credential. ' + + 'Get returned status: ' + + status }); + } })); } @@ -204,14 +210,20 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro selectedCredentials.vault = data; }) .catch(({data, status}) => { - ProcessErrors( - null, data, status, null, - { - hdr: 'Error!', - msg: 'Failed to get machine credential. ' + - 'Get returned status: ' + - status + if (status === 403) { + /* User doesn't have read access to the vault credential, so use summary_fields */ + selectedCredentials.vault = job_template_obj.summary_fields.vault_credential; + selectedCredentials.vault.credential_type = job_template_obj.summary_fields.vault_credential.credential_type_id; + } else { + ProcessErrors( + null, data, status, null, + { + hdr: 'Error!', + msg: 'Failed to get machine credential. ' + + 'Get returned status: ' + + status }); + } })); } @@ -223,13 +235,19 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro selectedCredentials.extra = data.results; }) .catch(({data, status}) => { - ProcessErrors(null, data, status, null, - { - hdr: 'Error!', - msg: 'Failed to get extra credentials. ' + - 'Get returned status: ' + - status - }); + if (status === 403) { + /* User doesn't have read access to the extra credentials, so use summary_fields */ + selectedCredentials.extra = job_template_obj.summary_fields.extra_credentials; + _.map(selectedCredentials.extra, (cred) => {cred.credential_type = cred.credential_type_id;}); + } else { + ProcessErrors(null, data, status, null, + { + hdr: 'Error!', + msg: 'Failed to get extra credentials. ' + + 'Get returned status: ' + + status + }); + } })); } From a36a141141ace1a1ba6ee759b002562d0a91b615 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 31 Aug 2017 23:10:09 -0400 Subject: [PATCH 129/138] fuller validation for host_filter --- awx/api/serializers.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 88ea77b783..0e6dd72863 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1171,15 +1171,30 @@ class InventorySerializer(BaseSerializerWithVariables): ret['organization'] = None return ret + def validate_host_filter(self, host_filter): + if host_filter: + try: + SmartFilter().query_from_string(host_filter) + except RuntimeError, e: + raise models.base.ValidationError(e) + return host_filter + def validate(self, attrs): - kind = attrs.get('kind', 'standard') - if kind == 'smart': - host_filter = attrs.get('host_filter') - if host_filter is not None: - try: - SmartFilter().query_from_string(host_filter) - except RuntimeError, e: - raise models.base.ValidationError(e) + kind = None + if 'kind' in attrs: + kind = attrs['kind'] + elif self.instance: + kind = self.instance.kind + + host_filter = None + if 'host_filter' in attrs: + host_filter = attrs['host_filter'] + elif self.instance: + host_filter = self.instance.host_filter + + if kind == 'smart' and not host_filter: + raise serializers.ValidationError({'host_filter': _( + 'Smart inventories must specify host_filter')}) return super(InventorySerializer, self).validate(attrs) From 7318cbae9bd44f91c8d32fbc3433ed58103b3982 Mon Sep 17 00:00:00 2001 From: mabashian Date: Fri, 1 Sep 2017 09:27:48 -0400 Subject: [PATCH 130/138] Fixed host filter clearall --- .../host-filter-modal/host-filter-modal.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-host-filter/host-filter-modal/host-filter-modal.directive.js b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-host-filter/host-filter-modal/host-filter-modal.directive.js index 541214e5db..4f6bc0c15d 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-host-filter/host-filter-modal/host-filter-modal.directive.js +++ b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-host-filter/host-filter-modal/host-filter-modal.directive.js @@ -29,7 +29,7 @@ export default ['templateUrl', function(templateUrl) { $scope.host_default_params = { order_by: 'name', page_size: 5, - inventory__organization: null + inventory__organization: $scope.organization }; $scope.host_queryset = _.merge({ From c866b9d7681bc9dfd40a22c39179daebdc2c4d0a Mon Sep 17 00:00:00 2001 From: Aaron Tan Date: Thu, 31 Aug 2017 15:45:06 -0400 Subject: [PATCH 131/138] Enhance query string in ad hoc command event save to consider smart inventory --- awx/main/models/ad_hoc_commands.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index 00773abcd8..ce02f900c8 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -347,7 +347,6 @@ class AdHocCommandEvent(CreatedModifiedModel): return u'%s @ %s' % (self.get_event_display(), self.created.isoformat()) def save(self, *args, **kwargs): - from awx.main.models.inventory import Host # If update_fields has been specified, add our field names to it, # if it hasn't been specified, then we're just doing a normal save. update_fields = kwargs.get('update_fields', []) @@ -364,16 +363,16 @@ class AdHocCommandEvent(CreatedModifiedModel): self.host_name = self.event_data.get('host', '').strip() if 'host_name' not in update_fields: update_fields.append('host_name') - try: - if not self.host_id and self.host_name: - host_qs = Host.objects.filter(inventory__ad_hoc_commands__id=self.ad_hoc_command_id, name=self.host_name) + if not self.host_id and self.host_name: + host_qs = self.ad_hoc_command.inventory.hosts.filter(name=self.host_name) + try: host_id = host_qs.only('id').values_list('id', flat=True) if host_id.exists(): self.host_id = host_id[0] if 'host_id' not in update_fields: update_fields.append('host_id') - except (IndexError, AttributeError): - pass + except (IndexError, AttributeError): + pass super(AdHocCommandEvent, self).save(*args, **kwargs) @classmethod From 2857bfd8a0fe18c8d1084cafe550f8d9c0d6617a Mon Sep 17 00:00:00 2001 From: Jared Tabor Date: Fri, 1 Sep 2017 11:03:03 -0700 Subject: [PATCH 132/138] feedback from PR --- .../multi-credential/multi-credential.service.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js index 362c9d7e4b..644097b451 100644 --- a/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js +++ b/awx/ui/client/src/templates/job_templates/multi-credential/multi-credential.service.js @@ -238,7 +238,10 @@ export default ['Rest', 'ProcessErrors', '$q', 'GetBasePath', function(Rest, Pro if (status === 403) { /* User doesn't have read access to the extra credentials, so use summary_fields */ selectedCredentials.extra = job_template_obj.summary_fields.extra_credentials; - _.map(selectedCredentials.extra, (cred) => {cred.credential_type = cred.credential_type_id;}); + _.map(selectedCredentials.extra, (cred) => { + cred.credential_type = cred.credential_type_id; + return cred; + }); } else { ProcessErrors(null, data, status, null, { From a09bd5a90df9ebd854c11e01c4d83166c24a23d3 Mon Sep 17 00:00:00 2001 From: mabashian Date: Fri, 1 Sep 2017 16:32:09 -0400 Subject: [PATCH 133/138] Remove delete and edit buttons from smart inventory host list. Only option should be view. --- .../related/hosts/edit/host-edit.controller.js | 1 + .../inventories/related/hosts/related-host.form.js | 14 +++++++------- .../smart-inventory/smart-inventory-hosts.route.js | 3 +++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/edit/host-edit.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/edit/host-edit.controller.js index 4a66d48025..52661b4129 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/edit/host-edit.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/edit/host-edit.controller.js @@ -7,6 +7,7 @@ export default ['$scope', '$state', '$stateParams', 'GenerateForm', 'ParseTypeChange', 'HostsService', 'host', '$rootScope', function($scope, $state, $stateParams, GenerateForm, ParseTypeChange, HostsService, host, $rootScope){ + $scope.isSmartInvHost = $state.includes('inventories.editSmartInventory.hosts.edit'); $scope.parseType = 'yaml'; $scope.formCancel = function(){ $state.go('^', null, {reload: true}); diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js index 047d7ce456..2ee6de78a2 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/hosts/related-host.form.js @@ -37,7 +37,7 @@ function(i18n) { " set by the inventory sync process.") + "

", dataTitle: i18n._('Host Enabled'), - ngDisabled: '!host.summary_fields.user_capabilities.edit || host.has_inventory_sources' + ngDisabled: '!host.summary_fields.user_capabilities.edit || host.has_inventory_sources || isSmartInvHost' } }, fields: { @@ -56,11 +56,11 @@ function(i18n) { dataTitle: i18n._('Host Name'), dataPlacement: 'right', dataContainer: 'body', - ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd) || isSmartInvHost' }, description: { label: i18n._('Description'), - ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)', + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd) || isSmartInvHost', type: 'text' }, host_variables: { @@ -79,23 +79,23 @@ function(i18n) { dataTitle: i18n._('Host Variables'), dataPlacement: 'right', dataContainer: 'body', - ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(host.summary_fields.user_capabilities.edit || canAdd) || isSmartInvHost' } }, buttons: { cancel: { ngClick: 'formCancel()', - ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '(host.summary_fields.user_capabilities.edit || canAdd) && !isSmartInvHost' }, close: { ngClick: 'formCancel()', - ngShow: '!(host.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '!(host.summary_fields.user_capabilities.edit || canAdd) || isSmartInvHost' }, save: { ngClick: 'formSave()', ngDisabled: true, - ngShow: '(host.summary_fields.user_capabilities.edit || canAdd)' + ngShow: '(host.summary_fields.user_capabilities.edit || canAdd) && !isSmartInvHost' } }, diff --git a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-hosts.route.js b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-hosts.route.js index 4924ec39f5..befc2c2d66 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-hosts.route.js +++ b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-hosts.route.js @@ -35,6 +35,9 @@ export default { list.basePath = GetBasePath('inventory') + $stateParams.smartinventory_id + '/hosts'; delete list.actions.create; delete list.fields.groups; + delete list.fieldActions.delete; + delete list.fieldActions.edit; + delete list.fieldActions.view.ngShow; list.fields.name.columnClass = 'col-lg-8 col-md-11 col-sm-8 col-xs-7'; return list; }], From 86a6e5ee6353b7b4bb545f344a3920733090e068 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 5 Sep 2017 12:29:07 -0400 Subject: [PATCH 134/138] fix ctit logging toggle from being showed for log types other than https --- .../client/src/configuration/configuration.controller.js | 4 +++- .../system-form/sub-forms/system-logging.form.js | 3 ++- awx/ui/client/src/shared/form-generator.js | 7 ++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/awx/ui/client/src/configuration/configuration.controller.js b/awx/ui/client/src/configuration/configuration.controller.js index 7d35be84e3..72cae0f2d2 100644 --- a/awx/ui/client/src/configuration/configuration.controller.js +++ b/awx/ui/client/src/configuration/configuration.controller.js @@ -394,7 +394,9 @@ export default [ // Default AD_HOC_COMMANDS to an empty list payload[key] = $scope[key].value || []; } else { - payload[key] = $scope[key].value; + if ($scope[key]) { + payload[key] = $scope[key].value; + } } } } else if($scope.configDataResolve[key].type === 'list' && $scope[key] !== null) { diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js index ac585102bc..8ae268c762 100644 --- a/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js @@ -62,7 +62,8 @@ disableChooseOption: true }, LOG_AGGREGATOR_VERIFY_CERT: { - type: 'toggleSwitch' + type: 'toggleSwitch', + ngShow: 'LOG_AGGREGATOR_PROTOCOL.value === "https"' } }, diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index 3052788485..b65bf76184 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -762,9 +762,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += label(labelOptions); - html += `
- From 03932c7bdb0b3b7b5432458f62c52a5af3885f3f Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Tue, 5 Sep 2017 14:55:29 -0400 Subject: [PATCH 135/138] Fix error message when calling remove on undefined DOM element --- awx/ui/client/src/shared/directives.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/ui/client/src/shared/directives.js b/awx/ui/client/src/shared/directives.js index 71684933d7..39700f5b9f 100644 --- a/awx/ui/client/src/shared/directives.js +++ b/awx/ui/client/src/shared/directives.js @@ -442,7 +442,7 @@ function(ConfigurationUtils, i18n, $rootScope) { } else if (!isRequired) { elm.removeAttr('required'); if (!attrs.awrequiredAlwaysShowAsterisk) { - $(label).find('span.Form-requiredAsterisk')[0].remove(); + $(label).find('span.Form-requiredAsterisk').remove(); } } if (isRequired && (viewValue === undefined || viewValue === null || viewValue === '')) { From 1238c788e1b4c13193d02998b4000b25b0a5d881 Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Tue, 5 Sep 2017 16:56:00 -0400 Subject: [PATCH 136/138] fix to console error of conditional toggle showing --- .../configuration/system-form/sub-forms/system-logging.form.js | 2 +- awx/ui/client/src/shared/form-generator.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js b/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js index 8ae268c762..388cfb00f8 100644 --- a/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js +++ b/awx/ui/client/src/configuration/system-form/sub-forms/system-logging.form.js @@ -63,7 +63,7 @@ }, LOG_AGGREGATOR_VERIFY_CERT: { type: 'toggleSwitch', - ngShow: 'LOG_AGGREGATOR_PROTOCOL.value === "https"' + ngShow: "LOG_AGGREGATOR_PROTOCOL.value === 'https'" } }, diff --git a/awx/ui/client/src/shared/form-generator.js b/awx/ui/client/src/shared/form-generator.js index b65bf76184..8e39bad207 100644 --- a/awx/ui/client/src/shared/form-generator.js +++ b/awx/ui/client/src/shared/form-generator.js @@ -762,7 +762,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat html += label(labelOptions); - html += `
`; html += `