1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-31 06:51:10 +03:00

Implement adhoc commands for normal inventories

This commit is contained in:
Michael Abashian 2017-04-24 11:36:46 -04:00 committed by Jared Tabor
parent 0fa9aa6bcb
commit 8734ad738f
14 changed files with 638 additions and 21 deletions

View File

@ -203,7 +203,7 @@ table, tbody {
.List-buttonDefault {
background-color: @btn-bg;
color: @btn-txt;
border-color: @btn-bord;
border-color: @b7grey;
}
.List-buttonDefault:hover,
@ -212,6 +212,11 @@ table, tbody {
color: @btn-txt;
}
.List-buttonDefault[disabled] {
color: @d7grey;
border-color: @d7grey;
}
.List-searchDropdown {
border-top-left-radius: 5px!important;
border-bottom-left-radius: 5px!important;

View File

@ -0,0 +1,308 @@
/*************************************************
* Copyright (c) 2016 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name controllers.function:Adhoc
* @description This controller controls the adhoc form creation, command launching and navigating to standard out after command has been succesfully ran.
*/
function adhocController($q, $scope, $stateParams,
$state, CheckPasswords, PromptForPasswords, CreateLaunchDialog, CreateSelect2, adhocForm,
GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath, GetChoices,
KindChange, Wait, ParseTypeChange) {
ClearScope();
// this is done so that we can access private functions for testing, but
// we don't want to populate the "public" scope with these internal
// functions
var privateFn = {};
this.privateFn = privateFn;
var id = $stateParams.inventory_id,
hostPattern = $stateParams.pattern;
// note: put any urls that the controller will use in here!!!!
privateFn.setAvailableUrls = function() {
return {
adhocUrl: GetBasePath('inventory') + id + '/ad_hoc_commands/',
inventoryUrl: GetBasePath('inventory') + id + '/',
machineCredentialUrl: GetBasePath('credentials') + '?kind=ssh'
};
};
var urls = privateFn.setAvailableUrls();
// set the default options for the selects of the adhoc form
privateFn.setFieldDefaults = function(verbosity_options, forks_default) {
var verbosity;
for (verbosity in verbosity_options) {
if (verbosity_options[verbosity].isDefault) {
$scope.verbosity = verbosity_options[verbosity];
}
}
$("#forks-number").spinner("value", forks_default);
$scope.forks = forks_default;
};
// set when "working" starts and stops
privateFn.setLoadingStartStop = function() {
var asyncHelper = {},
formReadyPromise = 0;
Wait('start');
if (asyncHelper.removeChoicesReady) {
asyncHelper.removeChoicesReady();
}
asyncHelper.removeChoicesReady = $scope.$on('adhocFormReady',
isFormDone);
// check to see if all requests have completed
function isFormDone() {
formReadyPromise++;
if (formReadyPromise === 2) {
privateFn.setFieldDefaults($scope.adhoc_verbosity_options,
$scope.forks_field.default);
CreateSelect2({
element: '#adhoc_module_name',
multiple: false
});
CreateSelect2({
element: '#adhoc_verbosity',
multiple: false
});
Wait('stop');
}
}
};
// set the arguments help to watch on change of the module
privateFn.instantiateArgumentHelp = function() {
$scope.$watch('module_name', function(val) {
if (val) {
// give the docs for the selected module in the popover
$scope.argsPopOver = '<p>These arguments are used with the ' +
'specified module. You can find information about the ' +
val.value + ' module <a ' +
'id=\"adhoc_module_arguments_docs_link_for_module_' +
val.value + '\" href=\"http://docs.ansible.com/' +
val.value + '_module.html\" ' +
'target=\"_blank\">here</a>.</p>';
} else {
// no module selected
$scope.argsPopOver = "<p>These arguments are used with the" +
" specified module.</p>";
}
}, true);
// initially set to the same as no module selected
$scope.argsPopOver = "<p>These arguments are used with the " +
"specified module.</p>";
};
// pre-populate host patterns from the inventory page and
// delete the value off of rootScope
privateFn.instantiateHostPatterns = function(hostPattern) {
$scope.limit = hostPattern;
$scope.providedHostPatterns = $scope.limit;
};
// call helpers to initialize lookup and select fields through get
// requests
privateFn.initializeFields = function(machineCredentialUrl, adhocUrl) {
// setup module name select
GetChoices({
scope: $scope,
url: adhocUrl,
field: 'module_name',
variable: 'adhoc_module_options',
callback: 'adhocFormReady'
});
// setup verbosity options select
GetChoices({
scope: $scope,
url: adhocUrl,
field: 'verbosity',
variable: 'adhoc_verbosity_options',
callback: 'adhocFormReady'
});
};
// instantiate all variables on scope for display in the partial
privateFn.initializeForm = function(id, urls, hostPattern) {
// inject the adhoc command form
GenerateForm.inject(adhocForm,
{ mode: 'add', related: true, scope: $scope });
// set when "working" starts and stops
privateFn.setLoadingStartStop();
// put the inventory id on scope for the partial to use
$scope.inv_id = id;
// set the arguments help to watch on change of the module
privateFn.instantiateArgumentHelp();
// pre-populate host patterns from the inventory page and
// delete the value off of rootScope
privateFn.instantiateHostPatterns(hostPattern);
privateFn.initializeFields(urls.machineCredentialUrl, urls.adhocUrl);
};
privateFn.initializeForm(id, urls, hostPattern);
// init codemirror
$scope.extra_vars = '---';
$scope.parseType = 'yaml';
$scope.envParseType = 'yaml';
ParseTypeChange({ scope: $scope, field_id: 'adhoc_extra_vars' , variable: "extra_vars"});
$scope.formCancel = function(){
$state.go('inventoryManage');
};
// remove all data input into the form and reset the form back to defaults
$scope.formReset = function () {
GenerateForm.reset();
// pre-populate host patterns from the inventory page and
// delete the value off of rootScope
privateFn.instantiateHostPatterns($scope.providedHostPatterns);
KindChange({ scope: $scope, form: adhocForm, reset: false });
// set the default options for the selects of the adhoc form
privateFn.setFieldDefaults($scope.adhoc_verbosity_options,
$scope.forks_default);
};
// launch the job with the provided form data
$scope.launchJob = function () {
var adhocUrl = GetBasePath('inventory') + $stateParams.inventory_id +
'/ad_hoc_commands/', fld, data={}, html;
html = '<form class="ng-valid ng-valid-required" ' +
'name="job_launch_form" id="job_launch_form" autocomplete="off" ' +
'nonvalidate>';
// stub the payload with defaults from DRF
data = {
"job_type": "run",
"limit": "",
"credential": "",
"module_name": "command",
"module_args": "",
"forks": 0,
"verbosity": 0,
"extra_vars": "",
"privilege_escalation": ""
};
GenerateForm.clearApiErrors($scope);
// populate data with the relevant form values
for (fld in adhocForm.fields) {
if (adhocForm.fields[fld].type === 'select') {
data[fld] = $scope[fld].value;
} else if ($scope[fld]) {
data[fld] = $scope[fld];
}
}
Wait('start');
if ($scope.removeStartAdhocRun) {
$scope.removeStartAdhocRun();
}
$scope.removeStartAdhocRun = $scope.$on('StartAdhocRun', function() {
var password;
for (password in $scope.passwords) {
data[$scope.passwords[password]] = $scope[
$scope.passwords[password]
];
}
// Launch the adhoc job
Rest.setUrl(GetBasePath('inventory') +
$stateParams.inventory_id + '/ad_hoc_commands/');
Rest.post(data)
.success(function (data) {
Wait('stop');
$state.go('adHocJobStdout', {id: data.id});
})
.error(function (data, status) {
ProcessErrors($scope, data, status, adhocForm, {
hdr: 'Error!',
msg: 'Failed to launch adhoc command. POST ' +
'returned status: ' + status });
});
});
if ($scope.removeCreateLaunchDialog) {
$scope.removeCreateLaunchDialog();
}
$scope.removeCreateLaunchDialog = $scope.$on('CreateLaunchDialog',
function(e, html, url) {
CreateLaunchDialog({
scope: $scope,
html: html,
url: url,
callback: 'StartAdhocRun'
});
});
if ($scope.removePromptForPasswords) {
$scope.removePromptForPasswords();
}
$scope.removePromptForPasswords = $scope.$on('PromptForPasswords',
function(e, passwords_needed_to_start,html, url) {
PromptForPasswords({ scope: $scope,
passwords: passwords_needed_to_start,
callback: 'CreateLaunchDialog',
html: html,
url: url
});
});
if ($scope.removeContinueCred) {
$scope.removeContinueCred();
}
$scope.removeContinueCred = $scope.$on('ContinueCred', function(e,
passwords) {
if(passwords.length>0){
$scope.passwords_needed_to_start = passwords;
// only go through the password prompting steps if there are
// passwords to prompt for
$scope.$emit('PromptForPasswords', passwords, html, adhocUrl);
} else {
// if not, go straight to trying to run the job.
$scope.$emit('StartAdhocRun', adhocUrl);
}
});
// start adhoc launching routine
CheckPasswords({
scope: $scope,
credential: $scope.credential,
callback: 'ContinueCred'
});
};
}
export default ['$q', '$scope', '$stateParams',
'$state', 'CheckPasswords', 'PromptForPasswords', 'CreateLaunchDialog', 'CreateSelect2',
'adhocForm', 'GenerateForm', 'Rest', 'ProcessErrors', 'ClearScope', 'GetBasePath',
'GetChoices', 'KindChange', 'Wait', 'ParseTypeChange',
adhocController];

View File

@ -0,0 +1,159 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
/**
* @ngdoc function
* @name forms.function:Adhoc
* @description This form is for executing an adhoc command
*/
export default ['i18n', function(i18n) {
return {
addTitle: 'EXECUTE COMMAND',
name: 'adhoc',
well: true,
forceListeners: true,
fields: {
module_name: {
label: 'Module',
excludeModal: true,
type: 'select',
ngOptions: 'module.label for module in adhoc_module_options' +
' track by module.value',
ngChange: 'moduleChange()',
required: true,
awPopOver:'<p>These are the modules that Tower supports ' +
'running commands against.',
dataTitle: 'Module',
dataPlacement: 'right',
dataContainer: 'body'
},
module_args: {
label: 'Arguments',
type: 'text',
awPopOverWatch: 'argsPopOver',
awPopOver: '{{ argsPopOver }}',
dataTitle: 'Arguments',
dataPlacement: 'right',
dataContainer: 'body',
autocomplete: false
},
limit: {
label: 'Limit',
type: 'text',
awPopOver: '<p>The pattern used to target hosts in the ' +
'inventory. Leaving the field blank, all, and * will ' +
'all target all hosts in the inventory. You can find ' +
'more information about Ansible\'s host patterns ' +
'<a id=\"adhoc_form_hostpatterns_doc_link\"' +
'href=\"http://docs.ansible.com/intro_patterns.html\" ' +
'target=\"_blank\">here</a>.</p>',
dataTitle: 'Limit',
dataPlacement: 'right',
dataContainer: 'body'
},
credential: {
label: 'Machine Credential',
type: 'lookup',
list: 'CredentialList',
basePath: 'credentials',
sourceModel: 'credential',
sourceField: 'name',
class: 'squeeze',
awPopOver: '<p>Select the credential you want to use when ' +
'accessing the remote hosts to run the command. ' +
'Choose the credential containing ' +
'the username and SSH key or password that Ansbile ' +
'will need to log into the remote hosts.</p>',
dataTitle: 'Credential',
dataPlacement: 'right',
dataContainer: 'body',
awRequiredWhen: {
reqExpression: 'credRequired',
init: 'false'
}
},
become_enabled: {
label: 'Enable Privilege Escalation',
type: 'checkbox',
column: 2,
awPopOver: "<p>If enabled, run this playbook as an administrator. This is the equivalent of passing the<code> --become</code> option to the <code> ansible</code> command. </p>",
dataPlacement: 'right',
dataTitle: 'Become Privilege Escalation',
dataContainer: "body"
},
verbosity: {
label: 'Verbosity',
excludeModal: true,
type: 'select',
ngOptions: 'verbosity.label for verbosity in ' +
'adhoc_verbosity_options ' +
'track by verbosity.value',
required: true,
awPopOver:'<p>These are the verbosity levels for standard ' +
'out of the command run that are supported.',
dataTitle: 'Verbosity',
dataPlacement: 'right',
dataContainer: 'body',
"default": 1
},
forks: {
label: 'Forks',
id: 'forks-number',
type: 'number',
integer: true,
min: 0,
spinner: true,
"default": 0,
required: true,
'class': "input-small",
column: 1,
awPopOver: '<p>The number of parallel or simultaneous processes to use while executing the command. 0 signifies ' +
'the default value from the <a id="ansible_forks_docs" href=\"http://docs.ansible.com/intro_configuration.html#the-ansible-configuration-file\" ' +
' target=\"_blank\">ansible configuration file</a>.</p>',
dataTitle: 'Forks',
dataPlacement: 'right',
dataContainer: "body"
},
extra_vars: {
label: i18n._('Extra Variables'),
type: 'textarea',
class: 'Form-textAreaLabel Form-formGroup--fullWidth',
rows: 6,
"default": "---",
column: 2,
awPopOver: "<p>" + i18n.sprintf(i18n._("Pass extra command line variables. This is the %s or %s command line parameter " +
"for %s. Provide key/value pairs using either YAML or JSON."), '<code>-e</code>', '<code>--extra-vars</code>', '<code>ansible</code>') + "</p>" +
"JSON:<br />\n" +
"<blockquote>{<br />&emsp;\"somevar\": \"somevalue\",<br />&emsp;\"password\": \"magic\"<br /> }</blockquote>\n" +
"YAML:<br />\n" +
"<blockquote>---<br />somevar: somevalue<br />password: magic<br /></blockquote>\n",
dataTitle: i18n._('Extra Variables'),
dataPlacement: 'right',
dataContainer: "body"
}
},
buttons: {
reset: {
ngClick: 'formReset()',
ngDisabled: true,
label: 'Reset',
'class': 'btn btn-sm Form-cancelButton'
},
launch: {
label: 'Save',
ngClick: 'launchJob()',
ngDisabled: true,
'class': 'btn btn-sm List-buttonSubmit launchButton'
}
},
related: {}
};
}];

View File

@ -0,0 +1 @@
<div ng-cloak id="htmlTemplate"></div>

View File

@ -0,0 +1,28 @@
/*************************************************
* Copyright (c) 2015 Ansible, Inc.
*
* All Rights Reserved
*************************************************/
import {templateUrl} from '../../shared/template-url/template-url.factory';
import { N_ } from '../../i18n';
export default {
url: '/adhoc',
params:{
pattern: {
value: 'all',
squash: true
}
},
name: 'inventories.edit.adhoc',
views: {
'adhocForm@inventories': {
templateUrl: templateUrl('inventories/adhoc/adhoc'),
controller: 'adhocController'
}
},
ncyBreadcrumb: {
label: N_("RUN COMMAND")
}
};

View File

@ -0,0 +1,7 @@
import adhocController from './adhoc.controller';
import form from './adhoc.form';
export default
angular.module('adhoc', [])
.controller('adhocController', adhocController)
.factory('adhocForm', form);

View File

@ -48,9 +48,8 @@ export default {
},
launch: {
mode: 'all',
// $scope.$parent is governed by InventoryManageController,
ngDisabled: '!$parent.groupsSelected && !$parent.hostsSelected',
ngClick: '$parent.setAdhocPattern()',
ngDisabled: '!groupsSelected',
ngClick: 'setAdhocPattern()',
awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.",
dataTipWatch: "adhocCommandTooltip",
actionClass: 'btn List-buttonDefault',

View File

@ -43,6 +43,22 @@
$scope.inventory_id = $stateParams.inventory_id;
_.forEach($scope[list.name], buildStatusIndicators);
$scope.$on('selectedOrDeselected', function(e, value) {
let item = value.value;
if (value.isSelected) {
if(!$scope.groupsSelected) {
$scope.groupsSelected = [];
}
$scope.groupsSelected.push(item);
} else {
_.remove($scope.groupsSelected, { id: item.id });
if($scope.groupsSelected.length === 0) {
$scope.groupsSelected = null;
}
}
});
}
function buildStatusIndicators(group){
@ -195,11 +211,6 @@
groupsArr.push(id);
$state.go('inventoryManage.editGroup.schedules', {group_id: id, group: groupsArr}, {reload: true});
};
// $scope.$parent governed by InventoryManageController, for unified multiSelect options
$scope.$on('multiSelectList.selectionChanged', (event, selection) => {
$scope.$parent.groupsSelected = selection.length > 0 ? true : false;
$scope.$parent.groupsSelectedItems = selection.selectedItems;
});
$scope.copyMoveGroup = function(id){
$state.go('inventoryManage.copyMoveGroup', {group_id: id, groups: $stateParams.groups});
@ -221,4 +232,13 @@
cleanUpStateChangeListener();
});
$scope.setAdhocPattern = function(){
var pattern = _($scope.groupsSelected)
.map(function(item){
return item.name;
}).value().join(':');
$state.go('^.adhoc', {pattern: pattern});
};
}];

View File

@ -221,4 +221,13 @@
cleanUpStateChangeListener();
});
$scope.setAdhocPattern = function(){
var pattern = _($scope.groupsSelected)
.map(function(item){
return item.name;
}).value().join(':');
$state.go('^.adhoc', {pattern: pattern});
};
}];

View File

@ -50,9 +50,8 @@ export default {
},
launch: {
mode: 'all',
// $scope.$parent is governed by InventoryManageController,
ngDisabled: '!$parent.groupsSelected && !$parent.hostsSelected',
ngClick: '$parent.setAdhocPattern()',
ngDisabled: '!groupsSelected',
ngClick: 'setAdhocPattern()',
awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.",
dataTipWatch: "adhocCommandTooltip",
actionClass: 'btn List-buttonDefault',

View File

@ -1,4 +1,5 @@
<div class="tab-pane" id="inventories-panel">
<div ui-view="adhocForm"></div>
<div ui-view="groupForm"></div>
<div ui-view="hostForm"></div>
<div ui-view="sourcesForm"></div>

View File

@ -4,6 +4,7 @@
* All Rights Reserved
*************************************************/
import adhoc from './adhoc/main';
import host from './hosts/main';
import group from './groups/main';
import sources from './sources/main';
@ -17,8 +18,10 @@ import { N_ } from '../i18n';
import InventoryList from './inventory.list';
import InventoryForm from './inventory.form';
import InventoryManageService from './inventory-manage.service';
import adHocRoute from './adhoc/adhoc.route';
export default
angular.module('inventory', [
adhoc.name,
host.name,
group.name,
sources.name,
@ -81,6 +84,60 @@ angular.module('inventory', [
}
});
let adhocCredentialLookup = {
searchPrefix: 'credential',
name: 'inventories.edit.adhoc.credential',
url: '/credential',
data: {
formChildState: true
},
params: {
credential_search: {
value: {
page_size: '5'
},
squash: true,
dynamic: true
}
},
ncyBreadcrumb: {
skip: true
},
views: {
'related': {
templateProvider: function(ListDefinition, generateList) {
let list_html = generateList.build({
mode: 'lookup',
list: ListDefinition,
input_type: 'radio'
});
return `<lookup-modal>${list_html}</lookup-modal>`;
}
}
},
resolve: {
ListDefinition: ['CredentialList', function(CredentialList) {
let list = _.cloneDeep(CredentialList);
list.lookupConfirmText = 'SELECT';
return list;
}],
Dataset: ['ListDefinition', 'QuerySet', '$stateParams', 'GetBasePath',
(list, qs, $stateParams, GetBasePath) => {
let path = GetBasePath(list.name) || GetBasePath(list.basePath);
return qs.search(path, $stateParams[`${list.iterator}_search`]);
}
]
},
onExit: function($state) {
if ($state.transition) {
$('#form-modal').modal('hide');
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
}
}
};
return Promise.all([
basicInventoryAdd,
basicInventoryEdit,
@ -121,7 +178,9 @@ angular.module('inventory', [
}
]
}
})
}),
stateExtender.buildDefinition(adHocRoute),
stateExtender.buildDefinition(adhocCredentialLookup)
])
};
});

View File

@ -143,8 +143,17 @@ export default ['$scope', 'RelatedHostsListDefinition', '$rootScope', 'GetBasePa
var hostIds = _.map($scope.hostsSelected, (host) => host.id);
$state.go('systemTracking', {
inventoryId: $state.params.inventory_id,
hosts: $scope.$parent.hostsSelectedItems,
hosts: $scope.hostsSelected,
hostIds: hostIds
});
};
$scope.setAdhocPattern = function(){
var pattern = _($scope.hostsSelected)
.map(function(item){
return item.name;
}).value().join(':');
$state.go('^.adhoc', {pattern: pattern});
};
}];

View File

@ -83,6 +83,19 @@ export default {
},
actions: {
launch: {
mode: 'all',
ngDisabled: '!hostsSelected',
ngClick: 'setAdhocPattern()',
awToolTip: "Select an inventory source by clicking the check box beside it. The inventory source can be a single group or host, a selection of multiple hosts, or a selection of multiple groups.",
dataTipWatch: "adhocCommandTooltip",
actionClass: 'btn List-buttonDefault',
buttonContent: 'RUN COMMANDS',
showTipWhenDisabled: true,
tooltipInnerClass: "Tooltip-wide",
// TODO: we don't always want to show this
ngShow: true
},
system_tracking: {
buttonContent: 'System Tracking',
ngClick: 'systemTracking()',