/************************************************* * Copyright (c) 2015 Ansible, Inc. * * All Rights Reserved *************************************************/ /** * @ngdoc function * @name helpers.function:JobSubmission * @description * The JobSubmission.js file handles launching a job via a playbook run. There is a workflow that is involved in gathering all the * variables needed to launch a job, including credentials, passwords, extra variables, and survey data. Depending on what information * is needed to launch the job, a modal is built that prompts the user for any required information. This modal is built by creating * an html string with all the fields necessary to launch the job. This html string then gets compiled and opened in a dialog modal. * * #Workflow when user hits launch button * * A 'get' call is made to the API's 'job_templates/:job_template_id/launch' endpoint for that job template. The response from the API will specify * *``` * "credential_needed_to_start": true, * "can_start_without_user_input": false, * "ask_variables_on_launch": false, * "passwords_needed_to_start": [], * "variables_needed_to_start": [], * "survey_enabled": false *``` * #Step 1a - Check if there is a credential included in the job template: PromptForCredential * * The first step is to check if a credential was specified in the job template, by looking at the value of `credential_needed_to_start` . * If this boolean is true, then that means that the user did NOT specify a credential in the job template and we must prompt them to select a credential. * This emits a call to `PromptForCredential` which will do a lookup on the credentials endpoint and show a modal window with the list * of credentials for the user to choose from. * * #Step 1b - Check if the credential requires a password: CheckPasswords * * The second part of this process is to check if the credential the user picks requires a prompt for a password. A call is made (in the `CheckPasswords` factory) * to the chosen credential * and checks if ``password: ASK`` , ``become_password:ASK`` , or ``vault_password: ASK``. If any of these are ASK, then we begin building the html string for * each required password (see step 2). If none of these require a password, then we contine on to prompting for vars (see step 3) * * #Step 2 - Build password html string: PromptForPasswords * * We may detect from the inital 'get' call that we may need to prompt the user for passwords. The ``passwords_needed_to_start`` array from the 'get' call * will explictly tell us which passwords we must prompt for. Alternatively, we may have found that in steps 1a and 1b that * we have must prompt for passwords. Either way, we arrive in `PromptForPasswords` factory which builds the html string depending on how the particular credential is setup. * * #Step 3 - extra vars text editor: PromptForVars * * We may arrive at step three if the credential selected does not require a password, or if the password html string is already done being built. * if ``ask_variables_on_launch`` was true in the inital 'get' call, then we build the extra_vars text editor in the `PromptForVars` factory. * This factory makes a REST call to the job template and finds if any 'extra_vars' were specified in the job template. It takes any specified * extra vars and includes them in the extra_vars text editor that is built in the same factory. This code is added to the html string and passed along * to the next step. * * #Step 4 - Survey Taker: PromptForSurvey * * The last step in building the job submission modal is building the survey taker. If ``survey_enabled`` is true from the initial 'get' call, * we make a REST call to the survey endpoint for the specified job and gather the survey data. The `PromptForSurvey` factory takes the survey * data and adds to the html string any various survey question. * * #Step 5 - build the modal: CreateLaunchDialog * * At this point, we need to compile our giant html string onto the modal and open the job submission modal. This happens in the `CreateLaunch` * factory. In this factory the 'Launch' button for the job is tied to the validity of the form, which handles the validation of these fields. * * #Step 6 - Launch the job: LaunchJob * * This is maybe the most crucial step. We have setup everything we need in order to gather information from the user and now we want to be sure * we handle it correctly. And there are many scenarios to take into account. The first scenario we check for is is ``survey_enabled=true`` and * ``prompt_for_vars=false``, in which case we want to make sure to include the extra_vars from the job template in the data being * sent to the API (it is important to note that anything specified in the extra vars on job submission will override vars specified in the job template. * Likewise, any variables specified in the extra vars that are duplicated by the survey vars, will get overridden by the survey vars). * If the previous scenario is NOT the case, then we continue to gather the modal's answers regularly: gather the passwords, then the extra_vars, then * any survey results. Also note that we must gather any required survey answers, as well as any optional survey answers that happened to be provided * by the user. We also include the credential that was chosen if the user was prompted to select a credential. * At this point we have all the info we need and we are almost ready to perform a POST to the '/launch' endpoint. We must lastly check * if the user was not prompted for anything and therefore we don't want to pass any extra_vars to the POST. Once this is done we * make the REST POST call and provide all the data to hte API. The response from the API will be the job ID, which is used to redirect the user * to the job detail page for that job run. * * @Usage * This is usage information. */ '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.survey_questions[i].required || (scope.survey_questions[i].required === false && scope[fld].toString()!=="")) { 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', 'CheckPasswords', function($location, Wait, GetBasePath, LookUpInit, JobTemplateForm, CredentialList, Rest, Prompt, ProcessErrors, CheckPasswords) { return function(params) { var scope = params.scope, selectionMade; Wait('stop'); scope.credential = ''; if (scope.removeShowLookupDialog) { scope.removeShowLookupDialog(); } scope.removeShowLookupDialog = scope.$on('ShowLookupDialog', function() { selectionMade = function () { // scope.$emit(callback, scope.credential); CheckPasswords({ scope: scope, credential: scope.credential, callback: 'ContinueCred' }); }; 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: "
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" + "YAML:
\"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', ['$filter', '$compile', 'Wait', 'Alert', 'CredentialForm', 'CreateLaunchDialog', 'SurveyControllerInit' , 'GetBasePath', 'Rest' , 'Empty', 'GenerateForm', 'ShowSurveyModal', 'ProcessErrors', '$routeParams' , function($filter, $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/' ; //for toggling the input on password inputs scope.toggleInput = function(id) { var buttonId = id + "_show_input_button", inputId = id, buttonInnerHTML = $(buttonId).html(); if (buttonInnerHTML.indexOf("Show") > -1) { $(buttonId).html("Hide"); $(inputId).attr("type", "text"); } else { $(buttonId).html("Show"); $(inputId).attr("type", "password"); } }; function buildHtml(question, index){ question.index = index; question.question_name = $filter('sanitize')(question.question_name); question.question_description = (question.question_description) ? $filter('sanitize')(question.question_description) : undefined; requiredAsterisk = (question.required===true) ? "prepend-asterisk" : ""; requiredClasses = (question.required===true) ? "ng-pristine ng-invalid-required ng-invalid" : ""; html+='
somevar: somevalue
password: magic