/********************************************* * Copyright (c) 2014 AnsibleWorks, Inc. * * JobSubmission.js * */ /** * @ngdoc function * @name helpers.function:JobSubmission * @description */ 'use strict'; export default angular.module('JobSubmissionHelper', [ 'RestServices', 'Utilities', 'CredentialFormDefinition', 'CredentialsListDefinition', 'LookUpHelper', 'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog', 'FormGenerator', 'JobVarsPromptFormDefinition']) .factory('LaunchJob', ['Rest', 'Wait', 'ProcessErrors', 'ToJSON', 'Empty', 'GetBasePath', function(Rest, Wait, ProcessErrors, ToJSON, Empty, GetBasePath) { return function(params) { var scope = params.scope, callback = params.callback || 'JobLaunched', job_launch_data = {}, url = params.url, vars_url = GetBasePath('job_templates')+scope.job_template_id + '/', // fld, extra_vars; //found it easier to assume that there will be extra vars, and then check for a blank object at the end job_launch_data.extra_vars = {}; //gather the extra vars from the job template if survey is enabled and prompt for vars is false if (scope.removeGetExtraVars) { scope.removeGetExtraVars(); } scope.removeGetExtraVars = scope.$on('GetExtraVars', function() { Rest.setUrl(vars_url); Rest.get() .success(function (data) { if(!Empty(data.extra_vars)){ data.extra_vars = ToJSON('yaml', data.extra_vars, false); $.each(data.extra_vars, function(key,value){ job_launch_data.extra_vars[key] = value; }); } scope.$emit('BuildData'); }) .error(function (data, status) { ProcessErrors(scope, data, status, { hdr: 'Error!', msg: 'Failed to retrieve job template extra variables.' }); }); }); //build the data object to be sent to the job launch endpoint. Any variables gathered from the survey and the extra variables text editor are inserted into the extra_vars dict of the job_launch_data if (scope.removeBuildData) { scope.removeBuildData(); } scope.removeBuildData = scope.$on('BuildData', function() { if(!Empty(scope.passwords_needed_to_start) && scope.passwords_needed_to_start.length>0){ scope.passwords.forEach(function(password) { job_launch_data[password] = scope[password]; scope.passwords_needed_to_start.push(password+'_confirm'); // i'm pushing these values into this array for use during the survey taker parsing }); } if(scope.prompt_for_vars===true){ extra_vars = ToJSON(scope.parseType, scope.extra_vars, false); if(!Empty(extra_vars)){ $.each(extra_vars, function(key,value){ job_launch_data.extra_vars[key] = value; }); } } if(scope.survey_enabled===true){ for (var i=0; i < scope.survey_questions.length; i++){ var fld = scope.survey_questions[i].variable; // grab all survey questions that have answers if(scope[fld]) { job_launch_data.extra_vars[fld] = scope[fld]; } // for optional text and text-areas, submit a blank string if min length is 0 if(scope.survey_questions[i].required === false && (scope.survey_questions[i].type === "text" || scope.survey_questions[i].type === "textarea") && scope.survey_questions[i].min === 0 && (scope[fld] === "" || scope[fld] === undefined)){ job_launch_data.extra_vars[fld] = ""; } } } // include the credential used if the user was prompted to choose a cred if(!Empty(scope.credential)){ job_launch_data.credential_id = scope.credential; } // If the extra_vars dict is empty, we don't want to include it if we didn't prompt for anything. if(jQuery.isEmptyObject(job_launch_data.extra_vars)===true && scope.prompt_for_vars===false){ delete job_launch_data.extra_vars; } Rest.setUrl(url); Rest.post(job_launch_data) .success(function(data) { Wait('stop'); if(!$('#password-modal').is(':hidden')){ $('#password-modal').dialog('close'); } scope.$emit(callback, data); }) .error(function(data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed updating job ' + scope.job_template_id + ' with variables. POST returned: ' + status }); }); }); // if the user has a survey and does not have 'prompt for vars' selected, then we want to // include the extra vars from the job template in the job launch. so first check for these conditions // and then overlay any survey vars over those. if(scope.prompt_for_vars===false && scope.survey_enabled===true){ scope.$emit('GetExtraVars'); } else { scope.$emit('BuildData'); } }; }]) .factory('PromptForCredential', ['$location', 'Wait', 'GetBasePath', 'LookUpInit', 'JobTemplateForm', 'CredentialList', 'Rest', 'Prompt', 'ProcessErrors', function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList, Rest, Prompt, ProcessErrors) { return function(params) { var scope = params.scope, callback = params.callback || 'CredentialReady', selectionMade; Wait('stop'); scope.credential = ''; if (scope.removeShowLookupDialog) { scope.removeShowLookupDialog(); } scope.removeShowLookupDialog = scope.$on('ShowLookupDialog', function() { selectionMade = function () { scope.$emit(callback, scope.credential); }; LookUpInit({ url: GetBasePath('credentials') + '?kind=ssh', scope: scope, form: JobTemplateForm(), current_item: null, list: CredentialList, field: 'credential', hdr: 'Credential Required', instructions: "Launching this job requires a machine credential. Please select your machine credential now or Cancel to quit.", postAction: selectionMade, input_type: 'radio' }); scope.lookUpCredential(); }); if (scope.removeAlertNoCredentials) { scope.removeAlertNoCredentials(); } scope.removeAlertNoCredentials = scope.$on('AlertNoCredentials', function() { var action = function () { $('#prompt-modal').modal('hide'); $location.url('/credentials/add'); }; Prompt({ hdr: 'Machine Credential Required', body: "
There are no machine credentials defined in Tower. Launching this job requires a machine credential. " + "Create one now?", action: action }); }); Rest.setUrl(GetBasePath('credentials') + '?kind=ssh'); Rest.get() .success(function(data) { if (data.results.length > 0) { scope.$emit('ShowLookupDialog'); } else { scope.$emit('AlertNoCredentials'); } }) .error(function(data,status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Checking for machine credentials failed. GET returned: ' + status }); }); }; }]) .factory('CreateLaunchDialog', ['$compile', 'Rest', 'GetBasePath', 'TextareaResize', 'CreateDialog', 'GenerateForm', 'JobVarsPromptForm', 'Wait', 'ParseTypeChange', function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm, JobVarsPromptForm, Wait, ParseTypeChange) { return function(params) { var buttons, scope = params.scope, html = params.html, // job_launch_data = {}, callback = params.callback || 'PlaybookLaunchFinished', // url = params.url, e; // html+='
job_launch_form.$valid = {{job_launch_form.$valid}}
'; html+=''; $('#password-modal').empty().html(html); $('#password-modal').find('#job_extra_vars').before(scope.helpContainer); e = angular.element(document.getElementById('password-modal')); $compile(e)(scope); if(scope.prompt_for_vars===true){ ParseTypeChange({ scope: scope, field_id: 'job_extra_vars' , variable: "extra_vars"}); } buttons = [{ label: "Cancel", onClick: function() { $('#password-modal').dialog('close'); // scope.$emit('CancelJob'); // scope.$destroy(); }, icon: "fa-times", "class": "btn btn-default", "id": "password-cancel-button" },{ label: "Launch", onClick: function() { scope.$emit(callback); }, icon: "fa-check", "class": "btn btn-primary", "id": "password-accept-button" }]; CreateDialog({ id: 'password-modal', scope: scope, buttons: buttons, width: 620, height: 700, //(scope.passwords.length > 1) ? 700 : 500, minWidth: 500, title: 'Launch Configuration', callback: 'DialogReady', onOpen: function(){ Wait('stop'); } }); if (scope.removeDialogReady) { scope.removeDialogReady(); } scope.removeDialogReady = scope.$on('DialogReady', function() { $('#password-modal').dialog('open'); $('#password-accept-button').attr('ng-disabled', 'job_launch_form.$invalid' ); e = angular.element(document.getElementById('password-accept-button')); $compile(e)(scope); // if(scope.prompt_for_vars===true){ // setTimeout(function() { // TextareaResize({ // scope: scope, // textareaId: 'job_variables', // modalId: 'password-modal', // formId: 'job_launch_form', // parse: true // }); // }, 300); // } }); }; }]) .factory('PromptForPasswords', ['$compile', 'Wait', 'Alert', 'CredentialForm', function($compile, Wait, Alert, CredentialForm) { return function(params) { var scope = params.scope, callback = params.callback || 'PasswordsAccepted', url = params.url, form = CredentialForm, // acceptedPasswords = {}, fld, field, html=params.html || ""; scope.passwords = params.passwords; // Wait('stop'); html += "
Launching this job requires the passwords listed below. Enter and confirm each password before continuing.
\n"; // html += "
\n"; scope.passwords.forEach(function(password) { // Prompt for password field = form.fields[password]; fld = password; scope[fld] = ''; html += "
\n"; html += "\n"; html += "Please enter a password.
\n"; html += "
\n"; html += "
\n"; // Add the related confirm field if (field.associated) { fld = field.associated; field = form.fields[field.associated]; scope[fld] = ''; html += "
\n"; html += "\n"; html += "Please confirm the password.\n"; html += (field.awPassMatch) ? "This value does not match the password you entered previously. Please confirm that password.
\n" : ""; html += "
\n"; html += "\n"; } }); // html += "\n"; // $('#password-modal').empty().html(buildHtml); // e = angular.element(document.getElementById('password-modal')); // $compile(e)(scope); scope.$emit(callback, html, url); // CreateLaunchDialog({scope: scope}) // buttons = [{ // label: "Cancel", // onClick: function() { // scope.passwordCancel(); // }, // icon: "fa-times", // "class": "btn btn-default", // "id": "password-cancel-button" // },{ // label: "Continue", // onClick: function() { // scope.passwordAccept(); // }, // icon: "fa-check", // "class": "btn btn-primary", // "id": "password-accept-button" // }]; // CreateDialog({ // id: 'password-modal', // scope: scope, // buttons: buttons, // width: 600, // height: (parent_scope.passwords.length > 1) ? 700 : 500, // minWidth: 500, // title: 'parent_scope.passwords Required', // callback: 'DialogReady' // }); // if (scope.removeDialogReady) { // scope.removeDialogReady(); // } // scope.removeDialogReady = scope.$on('DialogReady', function() { // $('#password-modal').dialog('open'); // $('#password-accept-button').attr({ "disabled": "disabled" }); // }); // scope.keydown = function(e){ // if(e.keyCode===13){ // scope.passwordAccept(); // } // }; // scope.passwordAccept = function() { // if (!scope.password_form.$invalid) { // scope.passwords.forEach(function(password) { // acceptedPasswords[password] = scope[password]; // }); // $('#password-modal').dialog('close'); // scope.$emit(callback, acceptedPasswords); // } // }; // scope.passwordCancel = function() { // $('#password-modal').dialog('close'); // scope.$emit('CancelJob'); // scope.$destroy(); // }; // Password change scope.clearPWConfirm = function (fld) { // If password value changes, make sure password_confirm must be re-entered scope[fld] = ''; scope.job_launch_form[fld].$setValidity('awpassmatch', false); scope.checkStatus(); }; scope.checkStatus = function() { if (!scope.job_launch_form.$invalid) { $('#password-accept-button').removeAttr('disabled'); } else { $('#password-accept-button').attr({ "disabled": "disabled" }); } }; }; }]) .factory('PromptForVars', ['$compile', 'Rest', 'GetBasePath', 'TextareaResize', 'CreateLaunchDialog', 'GenerateForm', 'JobVarsPromptForm', 'Wait', 'ParseVariableString', 'ToJSON', 'ProcessErrors', '$routeParams' , function($compile, Rest, GetBasePath, TextareaResize,CreateLaunchDialog, GenerateForm, JobVarsPromptForm, Wait, ParseVariableString, ToJSON, ProcessErrors, $routeParams) { return function(params) { var // parent_scope = params.scope, scope = params.scope, callback = params.callback, // job = params.job, url = params.url, vars_url = GetBasePath('job_templates')+scope.job_template_id + '/', html = params.html || ""; function buildHtml(extra_vars){ html += GenerateForm.buildHTML(JobVarsPromptForm, { mode: 'edit', modal: true, scope: scope }); html = html.replace("", ""); scope.helpContainer = "
\n" + "" + " click for help
\n"; scope.helpText = "

After defining any extra variables, click Continue to start the job. Otherwise, click cancel to abort.

" + "

Extra variables are passed as command line variables to the playbook run. It is equivalent to the -e or --extra-vars " + "command line parameter for ansible-playbook. Provide key/value pairs using either YAML or JSON.

" + "JSON:
\n" + "
{
\"somevar\": \"somevalue\",
\"password\": \"magic\"
}
\n" + "YAML:
\n" + "
---
somevar: somevalue
password: magic
\n"; scope.extra_vars = ParseVariableString(extra_vars); scope.parseType = 'yaml'; scope.$emit(callback, html, url); } Rest.setUrl(vars_url); Rest.get() .success(function (data) { buildHtml(data.extra_vars); }) .error(function (data, status) { ProcessErrors(scope, data, status, { hdr: 'Error!', msg: 'Failed to retrieve organization: ' + $routeParams.id + '. GET status: ' + status }); }); }; }]) .factory('PromptForSurvey', ['$compile', 'Wait', 'Alert', 'CredentialForm', 'CreateLaunchDialog', 'SurveyControllerInit' , 'GetBasePath', 'Rest' , 'Empty', 'GenerateForm', 'ShowSurveyModal', 'ProcessErrors', '$routeParams' , function($compile, Wait, Alert, CredentialForm, CreateLaunchDialog, SurveyControllerInit, GetBasePath, Rest, Empty, GenerateForm, ShowSurveyModal, ProcessErrors, $routeParams) { return function(params) { var html = params.html || "", id= params.id, url = params.url, callback=params.callback, scope = params.scope, i, j, requiredAsterisk, requiredClasses, defaultValue, choices, element, minlength, maxlength, checked, min, max, survey_url = GetBasePath('job_templates') + id + '/survey_spec/' ; function buildHtml(question, index){ question.index = index; question.question_name = question.question_name.replace(//g, ">"); question.question_description = (question.question_description) ? question.question_description.replace(//g, ">") : undefined; requiredAsterisk = (question.required===true) ? "prepend-asterisk" : ""; requiredClasses = (question.required===true) ? "ng-pristine ng-invalid-required ng-invalid" : ""; html+='
'; html += '\n'; if(!Empty(question.question_description)){ html += '
'+question.question_description+'
\n'; } // if(question.default && question.default.indexOf('<') !== -1){ // question.default = (question.default) ? question.default.replace(/') !== -1){ // question.default = (question.default) ? question.default.replace(/>/g, ">") : undefined; // } scope[question.variable] = question.default; if(question.type === 'text' ){ minlength = (!Empty(question.min)) ? Number(question.min) : ""; maxlength =(!Empty(question.max)) ? Number(question.max) : "" ; html+=''+ '
Please enter an answer.
'+ '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ '
'; } if(question.type === "textarea"){ scope[question.variable] = question.default || question.default_textarea; minlength = (!Empty(question.min)) ? Number(question.min) : ""; maxlength =(!Empty(question.max)) ? Number(question.max) : "" ; html+=''+ '
Please enter an answer.
'+ '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ '
'; } if(question.type === 'password' ){ minlength = (!Empty(question.min)) ? Number(question.min) : ""; maxlength =(!Empty(question.max)) ? Number(question.max) : "" ; html+=''+ '
Please enter an answer.
'+ '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ '
'; html+=''+ '
Please enter an answer.
'+ '
Please enter an answer between {{'+minlength+'}} to {{'+maxlength+'}} characters long.
'+ '
'; html+= ''; } if(question.type === 'multiplechoice'){ choices = question.choices.split(/\n/); element = (question.type==="multiselect") ? "checkbox" : 'radio'; question.default = (question.default) ? question.default : (question.default_multiselect) ? question.default_multiselect : "" ; html+='
'; for( j = 0; j/g, ">"); html+= '' + ''+choices[j] +'
' ; } html+= '
Please select an answer.
'+ '
'; html+= '
'; //end survey_taker_input } if(question.type === "multiselect"){ //seperate the choices out into an array choices = question.choices.split(/\n/); question.default = (question.default) ? question.default : (question.default_multiselect) ? question.default_multiselect : "" ; //ensure that the default answers are in an array scope[question.variable] = question.default.split(/\n/); //create a new object to be used by the surveyCheckboxes directive scope[question.variable + '_object'] = { name: question.variable, value: (question.default.split(/\n/)[0]==="") ? [] : question.default.split(/\n/) , required: question.required, options:[] }; //load the options into the 'options' key of the new object for(j=0; j'+ '{{job_launch_form.'+question.variable+'_object.$error.checkbox}}'+ '
Please select at least one answer.
'; } if(question.type === 'integer'){ min = (!Empty(question.min)) ? Number(question.min) : ""; max = (!Empty(question.max)) ? Number(question.max) : "" ; html+=''+ '
Please enter an answer.
'+ '
Please enter an answer that is a valid integer.
'+ '
Please enter an answer between {{'+min+'}} and {{'+max+'}}.
'; } if(question.type === "float"){ min = (!Empty(question.min)) ? question.min : ""; max = (!Empty(question.max)) ? question.max : "" ; defaultValue = (!Empty(question.default)) ? question.default : (!Empty(question.default_float)) ? question.default_float : "" ; html+=''+ '
Please enter an answer.
'+ '
Please enter an answer that is a decimal number.
'+ '
Please enter a decimal number between {{'+min+'}} and {{'+max+'}}.
'; } html+='
'; if(question.index === scope.survey_questions.length-1){ scope.$emit(callback, html, url); } } Rest.setUrl(survey_url); Rest.get() .success(function (data) { if(!Empty(data)){ scope.survey_name = data.name; scope.survey_description = data.description; scope.survey_questions = data.spec; for(i=0; i0){ scope.passwords_needed_to_start = passwords; scope.$emit('PromptForPasswords', passwords, html, url); } else if (scope.ask_variables_on_launch){ scope.$emit('PromptForVars', html, url); } else if (!Empty(scope.survey_enabled) && scope.survey_enabled===true) { scope.$emit('PromptForSurvey', html, url); } else { scope.$emit('StartPlaybookRun', url); } } }) .error(function (data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get job template details. GET returned status: ' + status }); }); } }); // Get the job or job_template record Wait('start'); Rest.setUrl(url); Rest.get() .success(function (data) { new_job_id = data.id; launch_url = url;//data.related.start; scope.passwords_needed_to_start = data.passwords_needed_to_start; scope.prompt_for_vars = data.ask_variables_on_launch; scope.survey_enabled = data.survey_enabled; scope.ask_variables_on_launch = data.ask_variables_on_launch; scope.variables_needed_to_start = data.variables_needed_to_start; html = '
'; if(data.credential_needed_to_start === true){ scope.$emit('PromptForCredential'); } else if (!Empty(data.passwords_needed_to_start) && data.passwords_needed_to_start.length > 0) { scope.$emit('PromptForPasswords', data.passwords_needed_to_start, html, url); } else if (data.ask_variables_on_launch) { scope.$emit('PromptForVars', html, url); } else if (!Empty(data.survey_enabled) && data.survey_enabled===true) { scope.$emit('PromptForSurvey', html, url); } else { scope.$emit('StartPlaybookRun', url); } }) .error(function (data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get job template details. GET returned status: ' + status }); }); }; } ]) // Submit SCM Update request .factory('ProjectUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'ProjectsForm', 'Wait', function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, ProjectsForm, Wait) { return function (params) { var scope = params.scope, project_id = params.project_id, url = GetBasePath('projects') + project_id + '/update/', project; if (scope.removeUpdateSubmitted) { scope.removeUpdateSubmitted(); } scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function() { // Refresh the project list after update request submitted Wait('stop'); if (/\d$/.test($location.path())) { //Request submitted from projects/N page. Navigate back to the list so user can see status $location.path('/projects'); } if (scope.socketStatus === 'error') { Alert('Update Started', 'The request to start the SCM update process was submitted. ' + 'To monitor the update status, refresh the page by clicking the button.', 'alert-info'); if (scope.refresh) { scope.refresh(); } } }); if (scope.removePromptForPasswords) { scope.removePromptForPasswords(); } scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { PromptForPasswords({ scope: scope, passwords: project.passwords_needed_to_update, callback: 'StartTheUpdate' }); }); if (scope.removeStartTheUpdate) { scope.removeStartTheUpdate(); } scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); }); // Check to see if we have permission to perform the update and if any passwords are needed Wait('start'); Rest.setUrl(url); Rest.get() .success(function (data) { project = data; if (project.can_update) { if (project.passwords_needed_to_updated) { Wait('stop'); scope.$emit('PromptForPasswords'); } else { scope.$emit('StartTheUpdate', {}); } } else { Alert('Permission Denied', 'You do not have access to update this project. Please contact your system administrator.', 'alert-danger'); } }) .error(function (data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to lookup project ' + url + ' GET returned: ' + status }); }); }; } ]) // Submit Inventory Update request .factory('InventoryUpdate', ['PromptForPasswords', 'LaunchJob', 'Rest', '$location', 'GetBasePath', 'ProcessErrors', 'Alert', 'Wait', function (PromptForPasswords, LaunchJob, Rest, $location, GetBasePath, ProcessErrors, Alert, Wait) { return function (params) { var scope = params.scope, url = params.url, inventory_source; if (scope.removeUpdateSubmitted) { scope.removeUpdateSubmitted(); } scope.removeUpdateSubmitted = scope.$on('UpdateSubmitted', function () { Wait('stop'); if (scope.socketStatus === 'error') { Alert('Sync Started', 'The request to start the inventory sync process was submitted. ' + 'To monitor the status refresh the page by clicking the button.', 'alert-info'); if (scope.refreshGroups) { // inventory detail page scope.refreshGroups(); } else if (scope.refresh) { scope.refresh(); } } }); if (scope.removePromptForPasswords) { scope.removePromptForPasswords(); } scope.removePromptForPasswords = scope.$on('PromptForPasswords', function() { PromptForPasswords({ scope: scope, passwords: inventory_source.passwords_needed_to_update, callback: 'StartTheUpdate' }); }); if (scope.removeStartTheUpdate) { scope.removeStartTheUpdate(); } scope.removeStartTheUpdate = scope.$on('StartTheUpdate', function(e, passwords) { LaunchJob({ scope: scope, url: url, passwords: passwords, callback: 'UpdateSubmitted' }); }); // Check to see if we have permission to perform the update and if any passwords are needed Wait('start'); Rest.setUrl(url); Rest.get() .success(function (data) { inventory_source = data; if (data.can_update) { if (data.passwords_needed_to_update) { Wait('stop'); scope.$emit('PromptForPasswords'); } else { scope.$emit('StartTheUpdate', {}); } } else { Wait('stop'); Alert('Permission Denied', 'You do not have access to run the inventory sync. Please contact your system administrator.', 'alert-danger'); } }) .error(function (data, status) { ProcessErrors(scope, data, status, null, { hdr: 'Error!', msg: 'Failed to get inventory source ' + url + ' GET returned: ' + status }); }); }; } ]);