From d744679d22793c0d370a24a0c96b0560025f024f Mon Sep 17 00:00:00 2001 From: mabashian Date: Tue, 27 Mar 2018 11:21:15 -0400 Subject: [PATCH 1/6] Fixed bug where the machine credential was being stripped from a workflow node if the edge type was changed. --- .../workflow-maker.controller.js | 168 ++++++++---------- 1 file changed, 78 insertions(+), 90 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 a86b38dc2e..5037f15644 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 @@ -74,12 +74,13 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', let buildSendableNodeData = function() { // Create the node let sendableNodeData = { - unified_job_template: params.node.unifiedJobTemplate.id + unified_job_template: params.node.unifiedJobTemplate.id, + credential: _.get(params, 'node.originalNodeObj.credential') || null }; - if(_.has(params, 'node.promptData.extraVars')) { - if(_.get(params, 'node.promptData.launchConf.defaults.extra_vars')) { - if(!sendableNodeData.extra_data) { + if (_.has(params, 'node.promptData.extraVars')) { + if (_.get(params, 'node.promptData.launchConf.defaults.extra_vars')) { + if (!sendableNodeData.extra_data) { sendableNodeData.extra_data = {}; } @@ -87,15 +88,15 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', // Only include extra vars that differ from the template default vars _.forOwn(params.node.promptData.extraVars, (value, key) => { - if(!defaultVars[key] || defaultVars[key] !== value) { + if (!defaultVars[key] || defaultVars[key] !== value) { sendableNodeData.extra_data[key] = value; } }); - if(_.isEmpty(sendableNodeData.extra_data)) { + if (_.isEmpty(sendableNodeData.extra_data)) { delete sendableNodeData.extra_data; } } else { - if(_.has(params, 'node.promptData.extraVars') && !_.isEmpty(params.node.promptData.extraVars)) { + if (_.has(params, 'node.promptData.extraVars') && !_.isEmpty(params.node.promptData.extraVars)) { sendableNodeData.extra_data = params.node.promptData.extraVars; } } @@ -104,7 +105,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', // Check to see if the user has provided any prompt values that are different // from the defaults in the job template - if(params.node.unifiedJobTemplate.type === "job_template" && params.node.promptData) { + if (params.node.unifiedJobTemplate.type === "job_template" && params.node.promptData) { sendableNodeData = PromptService.bundlePromptDataForSaving({ promptData: params.node.promptData, dataToSave: sendableNodeData @@ -117,26 +118,23 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', let continueRecursing = function(parentId) { $scope.totalIteratedNodes++; - if($scope.totalIteratedNodes === $scope.treeData.data.totalNodes) { + if ($scope.totalIteratedNodes === $scope.treeData.data.totalNodes) { // We're done recursing, lets move on completionCallback(); - } - else { - if(params.node.children && params.node.children.length > 0) { + } else { + if (params.node.children && params.node.children.length > 0) { _.forEach(params.node.children, function(child) { - if(child.edgeType === "success") { + if (child.edgeType === "success") { recursiveNodeUpdates({ parentId: parentId, node: child }, completionCallback); - } - else if(child.edgeType === "failure") { + } else if (child.edgeType === "failure") { recursiveNodeUpdates({ parentId: parentId, node: child }, completionCallback); - } - else if(child.edgeType === "always") { + } else if (child.edgeType === "always") { recursiveNodeUpdates({ parentId: parentId, node: child @@ -147,7 +145,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', } }; - if(params.node.isNew) { + if (params.node.isNew) { TemplatesService.addWorkflowNode({ url: $scope.treeData.workflow_job_template_obj.related.workflow_nodes, @@ -155,7 +153,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }) .then(function(data) { - if(!params.node.isRoot) { + if (!params.node.isRoot) { associateRequests.push({ parentId: params.parentId, nodeId: data.data.id, @@ -163,7 +161,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }); } - if(_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')){ + if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')){ // This finds the credentials that were selected in the prompt but don't occur // in the template defaults let credentialsToPost = params.node.promptData.prompts.credentials.value.filter(function(credFromPrompt) { @@ -193,18 +191,17 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', error.status }); }); - } - else { - if(params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) { + } else { + if (params.node.edited || !params.node.originalParentId || (params.node.originalParentId && params.parentId !== params.node.originalParentId)) { - if(params.node.edited) { + if (params.node.edited) { editRequests.push({ id: params.node.nodeId, data: buildSendableNodeData() }); - if(_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')){ + if (_.get(params, 'node.promptData.launchConf.ask_credential_on_launch')){ let credentialsNotInPriorCredentials = params.node.promptData.prompts.credentials.value.filter(function(credFromPrompt) { let defaultCreds = params.node.promptData.launchConf.defaults.credentials ? params.node.promptData.launchConf.defaults.credentials : []; return !defaultCreds.some(function(defaultCred) { @@ -243,20 +240,19 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }); }); } - } - if((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep + if ((params.node.originalParentId && params.parentId !== params.node.originalParentId) || params.node.originalEdge !== params.node.edgeType) {//beep let parentIsDeleted = false; _.forEach($scope.treeData.data.deletedNodes, function(deletedNode) { - if(deletedNode === params.node.originalParentId) { + if (deletedNode === params.node.originalParentId) { parentIsDeleted = true; } }); - if(!parentIsDeleted) { + if (!parentIsDeleted) { disassociateRequests.push({ parentId: params.node.originalParentId, nodeId: params.node.nodeId, @@ -267,7 +263,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', // Can only associate if we have a parent. // If we don't have a parent then this is a root node // and the act of disassociating will make it a root node - if(params.parentId) { + if (params.parentId) { associateRequests.push({ parentId: params.parentId, nodeId: params.node.nodeId, @@ -275,8 +271,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }); } - } - else if(!params.node.originalParentId && params.parentId) { + } else if (!params.node.originalParentId && params.parentId) { // This used to be a root node but is now not a root node associateRequests.push({ parentId: params.parentId, @@ -293,7 +288,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', let updateEdgeDropdownOptions = (optionsToInclude) => { // Not passing optionsToInclude will include all by default - if(!optionsToInclude) { + if (!optionsToInclude) { $scope.edgeTypeOptions = [ { label: 'Always', @@ -312,17 +307,17 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.edgeTypeOptions = []; optionsToInclude.forEach((optionToInclude) => { - if(optionToInclude === "always") { + if (optionToInclude === "always") { $scope.edgeTypeOptions.push({ label: 'Always', value: 'always' }); - } else if(optionToInclude === "success") { + } else if (optionToInclude === "success") { $scope.edgeTypeOptions.push({ label: 'On Success', value: 'success' }); - } else if(optionToInclude === "failure") { + } else if (optionToInclude === "failure") { $scope.edgeTypeOptions.push({ label: 'On Failure', value: 'failure' @@ -346,9 +341,9 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', promptWatcher = $scope.$watchGroup(promptDataToWatch, function() { let missingPromptValue = false; - if($scope.missingSurveyValue) { + if ($scope.missingSurveyValue) { missingPromptValue = true; - } else if(!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id) { + } else if (!$scope.promptData.prompts.inventory.value || !$scope.promptData.prompts.inventory.value.id) { missingPromptValue = true; } $scope.promptModalMissingReqFields = missingPromptValue; @@ -365,7 +360,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.totalIteratedNodes = 0; - if($scope.treeData && $scope.treeData.data && $scope.treeData.data.children && $scope.treeData.data.children.length > 0) { + if ($scope.treeData && $scope.treeData.data && $scope.treeData.data.children && $scope.treeData.data.children.length > 0) { let completionCallback = function() { let disassociatePromises = disassociateRequests.map(function(request) { @@ -376,13 +371,6 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }); }); - let credentialPromises = credentialRequests.map(function(request) { - return TemplatesService.postWorkflowNodeCredential({ - id: request.id, - data: request.data - }); - }); - let editNodePromises = editRequests.map(function(request) { return TemplatesService.editWorkflowNode({ id: request.id, @@ -394,9 +382,16 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); }); - $q.all(disassociatePromises.concat(editNodePromises, deletePromises, credentialPromises)) + $q.all(disassociatePromises.concat(editNodePromises, deletePromises)) .then(function() { + let credentialPromises = credentialRequests.map(function(request) { + return TemplatesService.postWorkflowNodeCredential({ + id: request.id, + data: request.data + }); + }); + let associatePromises = associateRequests.map(function(request) { return TemplatesService.associateWorkflowNode({ parentId: request.parentId, @@ -405,7 +400,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', }); }); - $q.all(associatePromises) + $q.all(associatePromises.concat(credentialPromises)) .then(function() { $scope.closeDialog(); }); @@ -417,8 +412,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', node: child }, completionCallback); }); - } - else { + } else { let deletePromises = $scope.treeData.data.deletedNodes.map(function(nodeId) { return TemplatesService.deleteWorkflowJobTemplateNode(nodeId); @@ -522,11 +516,11 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', } } - if(promptWatcher) { + if (promptWatcher) { promptWatcher(); } - if(surveyQuestionWatcher) { + if (surveyQuestionWatcher) { surveyQuestionWatcher(); } @@ -549,11 +543,11 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.nodeBeingEdited.isActiveEdit = false; } - if(promptWatcher) { + if (promptWatcher) { promptWatcher(); } - if(surveyQuestionWatcher) { + if (surveyQuestionWatcher) { surveyQuestionWatcher(); } @@ -601,12 +595,12 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', let jobTemplate = new JobTemplate(); - if(!_.isEmpty($scope.nodeBeingEdited.promptData)) { + if (!_.isEmpty($scope.nodeBeingEdited.promptData)) { $scope.promptData = _.cloneDeep($scope.nodeBeingEdited.promptData); - } else if($scope.nodeBeingEdited.unifiedJobTemplate){ + } else if ($scope.nodeBeingEdited.unifiedJobTemplate){ let promises = [jobTemplate.optionsLaunch($scope.nodeBeingEdited.unifiedJobTemplate.id), jobTemplate.getLaunch($scope.nodeBeingEdited.unifiedJobTemplate.id)]; - if(_.has($scope, 'nodeBeingEdited.originalNodeObj.related.credentials')) { + if (_.has($scope, 'nodeBeingEdited.originalNodeObj.related.credentials')) { Rest.setUrl($scope.nodeBeingEdited.originalNodeObj.related.credentials); promises.push(Rest.get()); } @@ -630,8 +624,8 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', const credentialHasScheduleOverride = (templateDefaultCred) => { let credentialHasOverride = false; workflowNodeCredentials.forEach((scheduleCred) => { - if(templateDefaultCred.credential_type === scheduleCred.credential_type) { - if( + if (templateDefaultCred.credential_type === scheduleCred.credential_type) { + if ( (!templateDefaultCred.vault_id && !scheduleCred.inputs.vault_id) || (templateDefaultCred.vault_id && scheduleCred.inputs.vault_id && templateDefaultCred.vault_id === scheduleCred.inputs.vault_id) ) { @@ -643,9 +637,9 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', return credentialHasOverride; }; - if(_.has(launchConf, 'defaults.credentials')) { + if (_.has(launchConf, 'defaults.credentials')) { launchConf.defaults.credentials.forEach((defaultCred) => { - if(!credentialHasScheduleOverride(defaultCred)) { + if (!credentialHasScheduleOverride(defaultCred)) { defaultCredsWithoutOverrides.push(defaultCred); } }); @@ -653,7 +647,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', prompts.credentials.value = workflowNodeCredentials.concat(defaultCredsWithoutOverrides); - if(!launchConf.survey_enabled && + if (!launchConf.survey_enabled && !launchConf.ask_inventory_on_launch && !launchConf.ask_credential_on_launch && !launchConf.ask_verbosity_on_launch && @@ -671,11 +665,11 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', } else { $scope.showPromptButton = true; - if(launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeBeingEdited.originalNodeObj.summary_fields.inventory')) { + if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory') && !_.has($scope, 'nodeBeingEdited.originalNodeObj.summary_fields.inventory')) { $scope.promptModalMissingReqFields = true; } - if(responses[1].data.survey_enabled) { + if (responses[1].data.survey_enabled) { // go out and get the survey questions jobTemplate.getSurveyQuestions($scope.nodeBeingEdited.unifiedJobTemplate.id) .then((surveyQuestionRes) => { @@ -700,7 +694,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => { let missingSurveyValue = false; _.each($scope.promptData.surveyQuestions, (question) => { - if(question.required && (Empty(question.model) || question.model === [])) { + if (question.required && (Empty(question.model) || question.model === [])) { missingSurveyValue = true; } }); @@ -709,8 +703,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', watchForPromptChanges(); }); - } - else { + } else { $scope.nodeBeingEdited.promptData = $scope.promptData = { launchConf: launchConf, launchOptions: launchOptions, @@ -729,7 +722,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.selectedTemplate = $scope.nodeBeingEdited.unifiedJobTemplate; - if($scope.selectedTemplate.unified_job_type) { + if ($scope.selectedTemplate.unified_job_type) { switch ($scope.selectedTemplate.unified_job_type) { case "job": $scope.workflowMakerFormConfig.activeTab = "jobs"; @@ -741,8 +734,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.workflowMakerFormConfig.activeTab = "inventory_sync"; break; } - } - else if($scope.selectedTemplate.type) { + } else if ($scope.selectedTemplate.type) { switch ($scope.selectedTemplate.type) { case "job_template": $scope.workflowMakerFormConfig.activeTab = "jobs"; @@ -767,19 +759,19 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', switch($scope.nodeBeingEdited.edgeType) { case "always": $scope.edgeType = {label: "Always", value: "always"}; - if(siblingConnectionTypes.length === 1 && _.includes(siblingConnectionTypes, "always")) { + if (siblingConnectionTypes.length === 1 && _.includes(siblingConnectionTypes, "always")) { edgeDropdownOptions = ["always"]; } break; case "success": $scope.edgeType = {label: "On Success", value: "success"}; - if(siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) { + if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) { edgeDropdownOptions = ["success", "failure"]; } break; case "failure": $scope.edgeType = {label: "On Failure", value: "failure"}; - if(siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) { + if (siblingConnectionTypes.length !== 0 && (!_.includes(siblingConnectionTypes, "always"))) { edgeDropdownOptions = ["success", "failure"]; } break; @@ -857,13 +849,12 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.$broadcast("refreshWorkflowChart"); - if($scope.placeholderNode) { + if ($scope.placeholderNode) { let edgeType = {label: "On Success", value: "success"}; - if($scope.placeholderNode.isRoot) { + if ($scope.placeholderNode.isRoot) { updateEdgeDropdownOptions(["always"]); edgeType = {label: "Always", value: "always"}; - } - else { + } else { // we need to update the possible edges based on any new siblings let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({ tree: $scope.treeData.data, @@ -889,8 +880,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', } $scope.edgeType = edgeType; - } - else if($scope.nodeBeingEdited) { + } else if ($scope.nodeBeingEdited) { let siblingConnectionTypes = WorkflowService.getSiblingConnectionTypes({ tree: $scope.treeData.data, parentId: $scope.nodeBeingEdited.parent.id, @@ -958,14 +948,14 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', $scope.selectedTemplate = angular.copy(selectedTemplate); - if(selectedTemplate.type === "job_template") { + if (selectedTemplate.type === "job_template") { let jobTemplate = new JobTemplate(); $q.all([jobTemplate.optionsLaunch(selectedTemplate.id), jobTemplate.getLaunch(selectedTemplate.id)]) .then((responses) => { let launchConf = responses[1].data; - if(!launchConf.survey_enabled && + if (!launchConf.survey_enabled && !launchConf.ask_inventory_on_launch && !launchConf.ask_credential_on_launch && !launchConf.ask_verbosity_on_launch && @@ -983,11 +973,11 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', } else { $scope.showPromptButton = true; - if(launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) { + if (launchConf.ask_inventory_on_launch && !_.has(launchConf, 'defaults.inventory')) { $scope.promptModalMissingReqFields = true; } - if(launchConf.survey_enabled) { + if (launchConf.survey_enabled) { // go out and get the survey questions jobTemplate.getSurveyQuestions(selectedTemplate.id) .then((surveyQuestionRes) => { @@ -1012,7 +1002,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', surveyQuestionWatcher = $scope.$watch('promptData.surveyQuestions', () => { let missingSurveyValue = false; _.each($scope.promptData.surveyQuestions, (question) => { - if(question.required && (Empty(question.model) || question.model === [])) { + if (question.required && (Empty(question.model) || question.model === [])) { missingSurveyValue = true; } }); @@ -1021,8 +1011,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', watchForPromptChanges(); }); - } - else { + } else { $scope.promptData = { launchConf: responses[1].data, launchOptions: responses[0].data, @@ -1098,7 +1087,7 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', // TODO: I think that the workflow chart directive (and eventually d3) is meddling with // this treeData object and removing the children object for some reason (?) // This happens on occasion and I think is a race condition (?) - if(!$scope.treeData.data.children) { + if (!$scope.treeData.data.children) { $scope.treeData.data.children = []; } @@ -1116,12 +1105,11 @@ export default ['$scope', 'WorkflowService', 'GetBasePath', 'TemplatesService', for(var i=0; i Date: Mon, 26 Mar 2018 12:20:36 -0400 Subject: [PATCH 2/6] Hide Ansible Environment form fields when there are no custom venvs --- .../src/organizations/organizations.form.js | 3 +- awx/ui/client/src/projects/projects.form.js | 28 ++++++++++--------- .../src/shared/upgrade/upgrade.block.less | 6 ++-- .../job_templates/job-template.form.js | 3 +- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/awx/ui/client/src/organizations/organizations.form.js b/awx/ui/client/src/organizations/organizations.form.js index 14883a3428..f33dd00526 100644 --- a/awx/ui/client/src/organizations/organizations.form.js +++ b/awx/ui/client/src/organizations/organizations.form.js @@ -52,7 +52,8 @@ export default ['NotificationsList', 'i18n', dataTitle: i18n._('Ansible Environment'), dataContainer: 'body', dataPlacement: 'right', - ngDisabled: '!(organization_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(organization_obj.summary_fields.user_capabilities.edit || canAdd)', + ngShow: 'custom_virtualenvs_options.length > 0' } }, diff --git a/awx/ui/client/src/projects/projects.form.js b/awx/ui/client/src/projects/projects.form.js index 38c151215d..baa204bcee 100644 --- a/awx/ui/client/src/projects/projects.form.js +++ b/awx/ui/client/src/projects/projects.form.js @@ -52,17 +52,6 @@ export default ['i18n', 'NotificationsList', 'TemplateList', ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd) || !canEditOrg', awLookupWhen: '(project_obj.summary_fields.user_capabilities.edit || canAdd) && canEditOrg' }, - custom_virtualenv: { - label: i18n._('Ansible Environment'), - type: 'select', - defaultText: i18n._('Select Ansible Environment'), - ngOptions: 'venv for venv in custom_virtualenvs_options track by venv', - awPopOver: "

" + i18n._("Select the custom Python virtual environment for this project to run on.") + "

", - dataTitle: i18n._('Ansible Environment'), - dataContainer: 'body', - dataPlacement: 'right', - ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)' - }, scm_type: { label: i18n._('SCM Type'), type: 'select', @@ -211,8 +200,21 @@ export default ['i18n', 'NotificationsList', 'TemplateList', dataTitle: i18n._('Cache Timeout'), dataPlacement: 'right', dataContainer: "body", - ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)' - } + ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)', + subForm: 'sourceSubForm' + }, + custom_virtualenv: { + label: i18n._('Ansible Environment'), + type: 'select', + defaultText: i18n._('Select Ansible Environment'), + ngOptions: 'venv for venv in custom_virtualenvs_options track by venv', + awPopOver: "

" + i18n._("Select the custom Python virtual environment for this project to run on.") + "

", + dataTitle: i18n._('Ansible Environment'), + dataContainer: 'body', + dataPlacement: 'right', + ngDisabled: '!(project_obj.summary_fields.user_capabilities.edit || canAdd)', + ngShow: 'custom_virtualenvs_options.length > 0' + }, }, buttons: { diff --git a/awx/ui/client/src/shared/upgrade/upgrade.block.less b/awx/ui/client/src/shared/upgrade/upgrade.block.less index 1f7ecbe25f..4d0203fa32 100644 --- a/awx/ui/client/src/shared/upgrade/upgrade.block.less +++ b/awx/ui/client/src/shared/upgrade/upgrade.block.less @@ -1,15 +1,17 @@ .at-Upgrade--panel { align-items: center; background-color: @at-color-body-background-light; + border-radius: 10px; color: @at-color-body-text; display: flex; flex-direction: column; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: @at-font-size-jumbotron-text; + height: ~"calc(100vh - 40px)"; justify-content: center; - margin-top: @at-space-10x; + margin: @at-space-4x; padding: @at-space-10x; - } +} .at-Upgrade--header { display: flex; 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 fc24852e2d..cd2824b8a3 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 @@ -240,7 +240,8 @@ function(NotificationsList, i18n) { dataTitle: i18n._('Ansible Environment'), dataContainer: 'body', dataPlacement: 'right', - ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)' + ngDisabled: '!(job_template_obj.summary_fields.user_capabilities.edit || canAdd)', + ngShow: 'custom_virtualenvs_options.length > 0' }, instance_groups: { label: i18n._('Instance Groups'), From 492e74a345f79d655137216ef1d59ec39834cc3c Mon Sep 17 00:00:00 2001 From: Marliana Lara Date: Tue, 27 Mar 2018 14:26:20 -0400 Subject: [PATCH 3/6] Remove pending_deletion button bug from inventory list --- .../inventories/inventory.list.js | 2 +- .../list-generator/list-generator.factory.js | 109 +++++++++--------- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js b/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js index c075662883..a8edfa27eb 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js +++ b/awx/ui/client/src/inventories-hosts/inventories/inventory.list.js @@ -113,7 +113,7 @@ export default ['i18n', function(i18n) { "class": 'btn-danger btn-xs', awToolTip: i18n._('Copy inventory'), dataPlacement: 'top', - ngShow: 'inventory.summary_fields.user_capabilities.edit' + ngShow: '!inventory.pending_deletion && inventory.summary_fields.user_capabilities.edit' }, view: { label: i18n._('View'), 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 eeeb80d2f4..01ca45edfa 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 @@ -395,63 +395,64 @@ export default ['$compile', 'Attr', 'Icon', } if (field_action === 'pending_deletion') { innerTable += `Pending Delete`; - } - // Plug in Dropdown Component - if (field_action === 'submit' && list.fieldActions[field_action].relaunch === true) { - innerTable += ``; - } else if (field_action === 'submit' && list.fieldActions[field_action].launch === true) { - innerTable += ``; } else { - fAction = list.fieldActions[field_action]; - innerTable += ""; } - //html += (fAction.label) ? " " + list.fieldActions[field_action].label + - // "" : ""; - innerTable += ""; } } } From dc46a732bcb9fd3c58fb0208c64cdd0d73774288 Mon Sep 17 00:00:00 2001 From: chris meyers Date: Wed, 28 Mar 2018 09:12:06 -0400 Subject: [PATCH 4/6] fix ldap group type params label * copy pasted ldap group type label and did no previously update. This updates. --- awx/sso/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/awx/sso/conf.py b/awx/sso/conf.py index 504b7724d4..fef093e62d 100644 --- a/awx/sso/conf.py +++ b/awx/sso/conf.py @@ -301,8 +301,8 @@ def _register_ldap(append=None): register( 'AUTH_LDAP{}_GROUP_TYPE_PARAMS'.format(append_str), field_class=fields.LDAPGroupTypeParamsField, - label=_('LDAP Group Type'), - help_text=_('Parameters to send the chosen group type.'), + label=_('LDAP Group Type Parameters'), + help_text=_('Key value parameters to send the chosen group type init method.'), category=_('LDAP'), category_slug='ldap', default=collections.OrderedDict([ From eef6f7ecb001f36197cf1770291417514f8081b8 Mon Sep 17 00:00:00 2001 From: chris meyers Date: Wed, 28 Mar 2018 09:50:53 -0400 Subject: [PATCH 5/6] delay looking up settings SYSTEM_UUID --- awx/main/managers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/awx/main/managers.py b/awx/main/managers.py index 1683b38c4e..8748a71c5b 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -87,7 +87,11 @@ class InstanceManager(models.Manager): return node[0] raise RuntimeError("No instance found with the current cluster host id") - def register(self, uuid=settings.SYSTEM_UUID, hostname=settings.CLUSTER_HOST_ID): + def register(self, uuid=None, hostname=None): + if not uuid: + uuid = settings.SYSTEM_UUID + if not hostname: + hostname = settings.CLUSTER_HOST_ID with advisory_lock('instance_registration_%s' % hostname): instance = self.filter(hostname=hostname) if instance.exists(): From 8c167e50c95173f1717c51705d9170cd3c537d38 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 27 Mar 2018 13:40:51 -0400 Subject: [PATCH 6/6] Continuously stream data from verbose jobs In verbose unified job models (inventory updates, system jobs, etc.), do not delay dispatch just because the encoded event data is not part of the data written to the buffer. This allows output from these commands to be submitted to the callback queue as they are produced, instead of waiting until the buffer is closed. --- awx/main/tasks.py | 27 ++++++---- .../tests/unit/utils/test_event_filter.py | 54 ++++++++++++++++++- awx/main/utils/common.py | 28 +++++++++- 3 files changed, 97 insertions(+), 12 deletions(-) diff --git a/awx/main/tasks.py b/awx/main/tasks.py index c47101b988..ca6f7dbede 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -55,7 +55,7 @@ from awx.main.queue import CallbackQueueDispatcher from awx.main.expect import run, isolated_manager from awx.main.utils import (get_ansible_version, get_ssh_version, decrypt_field, update_scm_url, check_proot_installed, build_proot_temp_dir, get_licenser, - wrap_args_with_proot, OutputEventFilter, ignore_inventory_computed_fields, + wrap_args_with_proot, OutputEventFilter, OutputVerboseFilter, ignore_inventory_computed_fields, ignore_inventory_group_removal, get_type_for_model, extract_ansible_vars) from awx.main.utils.reload import restart_local_services, stop_local_services from awx.main.utils.pglock import advisory_lock @@ -811,19 +811,26 @@ class BaseTask(LogErrorsTask): def get_stdout_handle(self, instance): ''' - Return an virtual file object for capturing stdout and events. + Return an virtual file object for capturing stdout and/or events. ''' dispatcher = CallbackQueueDispatcher() - def event_callback(event_data): - event_data.setdefault(self.event_data_key, instance.id) - if 'uuid' in event_data: - cache_event = cache.get('ev-{}'.format(event_data['uuid']), None) - if cache_event is not None: - event_data.update(cache_event) - dispatcher.dispatch(event_data) + if isinstance(instance, (Job, AdHocCommand, ProjectUpdate)): + def event_callback(event_data): + event_data.setdefault(self.event_data_key, instance.id) + if 'uuid' in event_data: + cache_event = cache.get('ev-{}'.format(event_data['uuid']), None) + if cache_event is not None: + event_data.update(cache_event) + dispatcher.dispatch(event_data) - return OutputEventFilter(event_callback) + return OutputEventFilter(event_callback) + else: + def event_callback(event_data): + event_data.setdefault(self.event_data_key, instance.id) + dispatcher.dispatch(event_data) + + return OutputVerboseFilter(event_callback) def pre_run_hook(self, instance, **kwargs): ''' diff --git a/awx/main/tests/unit/utils/test_event_filter.py b/awx/main/tests/unit/utils/test_event_filter.py index 85ecc609d0..fb8f4fa144 100644 --- a/awx/main/tests/unit/utils/test_event_filter.py +++ b/awx/main/tests/unit/utils/test_event_filter.py @@ -5,7 +5,7 @@ from StringIO import StringIO from six.moves import xrange -from awx.main.utils import OutputEventFilter +from awx.main.utils import OutputEventFilter, OutputVerboseFilter MAX_WIDTH = 78 EXAMPLE_UUID = '890773f5-fe6d-4091-8faf-bdc8021d65dd' @@ -145,3 +145,55 @@ def test_large_stdout_blob(): f = OutputEventFilter(_callback) for x in range(1024 * 10): f.write('x' * 1024) + + +def test_verbose_line_buffering(): + events = [] + + def _callback(event_data): + events.append(event_data) + + f = OutputVerboseFilter(_callback) + f.write('one two\r\n\r\n') + + assert len(events) == 2 + assert events[0]['start_line'] == 0 + assert events[0]['end_line'] == 1 + assert events[0]['stdout'] == 'one two' + + assert events[1]['start_line'] == 1 + assert events[1]['end_line'] == 2 + assert events[1]['stdout'] == '' + + f.write('three') + assert len(events) == 2 + f.write('\r\nfou') + + # three is not pushed to buffer until its line completes + assert len(events) == 3 + assert events[2]['start_line'] == 2 + assert events[2]['end_line'] == 3 + assert events[2]['stdout'] == 'three' + + f.write('r\r') + f.write('\nfi') + + assert events[3]['start_line'] == 3 + assert events[3]['end_line'] == 4 + assert events[3]['stdout'] == 'four' + + f.write('ve') + f.write('\r\n') + + assert len(events) == 5 + assert events[4]['start_line'] == 4 + assert events[4]['end_line'] == 5 + assert events[4]['stdout'] == 'five' + + f.close() + + from pprint import pprint + pprint(events) + assert len(events) == 6 + + assert events[5]['event'] == 'EOF' diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index ba3413a133..be531d7e17 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -48,7 +48,7 @@ __all__ = ['get_object_or_400', 'get_object_or_403', 'camelcase_to_underscore', 'copy_m2m_relationships', 'prefetch_page_capabilities', 'to_python_boolean', 'ignore_inventory_computed_fields', 'ignore_inventory_group_removal', '_inventory_updates', 'get_pk_from_dict', 'getattrd', 'NoDefaultProvided', - 'get_current_apps', 'set_current_apps', 'OutputEventFilter', + 'get_current_apps', 'set_current_apps', 'OutputEventFilter', 'OutputVerboseFilter', 'extract_ansible_vars', 'get_search_fields', 'get_system_task_capacity', 'get_cpu_capacity', 'get_mem_capacity', 'wrap_args_with_proot', 'build_proot_temp_dir', 'check_proot_installed', 'model_to_dict', 'model_instance_diff', 'timestamp_apiformat', 'parse_yaml_or_json', 'RequireDebugTrueOrTest', @@ -1009,6 +1009,32 @@ class OutputEventFilter(object): self._current_event_data = None +class OutputVerboseFilter(OutputEventFilter): + ''' + File-like object that dispatches stdout data. + Does not search for encoded job event data. + Use for unified job types that do not encode job event data. + ''' + def write(self, data): + self._buffer.write(data) + + # if the current chunk contains a line break + if data and '\n' in data: + # emit events for all complete lines we know about + lines = self._buffer.getvalue().splitlines(True) # keep ends + remainder = None + # if last line is not a complete line, then exclude it + if '\n' not in lines[-1]: + remainder = lines.pop() + # emit all complete lines + for line in lines: + self._emit_event(line) + self._buffer = StringIO() + # put final partial line back on buffer + if remainder: + self._buffer.write(remainder) + + def is_ansible_variable(key): return key.startswith('ansible_')