UI support for adhoc commands

This commit is contained in:
John Mitchell
2015-04-06 10:31:11 -04:00
parent 584abcc05b
commit ce31b98635
23 changed files with 678 additions and 30 deletions

View File

@@ -40,6 +40,7 @@ import {ScheduleEditController} from 'tower/controllers/Schedules';
import {ProjectsList, ProjectsAdd, ProjectsEdit} from 'tower/controllers/Projects'; import {ProjectsList, ProjectsAdd, ProjectsEdit} from 'tower/controllers/Projects';
import {OrganizationsList, OrganizationsAdd, OrganizationsEdit} from 'tower/controllers/Organizations'; import {OrganizationsList, OrganizationsAdd, OrganizationsEdit} from 'tower/controllers/Organizations';
import {InventoriesList, InventoriesAdd, InventoriesEdit, InventoriesManage} from 'tower/controllers/Inventories'; import {InventoriesList, InventoriesAdd, InventoriesEdit, InventoriesManage} from 'tower/controllers/Inventories';
import {AdhocForm} from 'tower/controllers/Adhoc';
import {AdminsList} from 'tower/controllers/Admins'; import {AdminsList} from 'tower/controllers/Admins';
import {UsersList, UsersAdd, UsersEdit} from 'tower/controllers/Users'; import {UsersList, UsersAdd, UsersEdit} from 'tower/controllers/Users';
import {TeamsList, TeamsAdd, TeamsEdit} from 'tower/controllers/Teams'; import {TeamsList, TeamsAdd, TeamsEdit} from 'tower/controllers/Teams';
@@ -88,6 +89,7 @@ var tower = angular.module('Tower', [
'AdminListDefinition', 'AdminListDefinition',
'CustomInventoryListDefinition', 'CustomInventoryListDefinition',
'AWDirectives', 'AWDirectives',
'AdhocFormDefinition',
'InventoriesListDefinition', 'InventoriesListDefinition',
'InventoryFormDefinition', 'InventoryFormDefinition',
'InventoryHelper', 'InventoryHelper',
@@ -168,7 +170,8 @@ var tower = angular.module('Tower', [
'ConfigureTowerHelper', 'ConfigureTowerHelper',
'ConfigureTowerJobsListDefinition', 'ConfigureTowerJobsListDefinition',
'CreateCustomInventoryHelper', 'CreateCustomInventoryHelper',
'CustomInventoryListDefinition' 'CustomInventoryListDefinition',
'AdhocHelper'
]) ])
.constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/') .constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/')
@@ -200,6 +203,11 @@ var tower = angular.module('Tower', [
controller: JobStdoutController controller: JobStdoutController
}). }).
when('/ad_hoc_commands/:id', {
templateUrl: urlPrefix + 'partials/job_stdout_adhoc.html',
controller: JobStdoutController
}).
when('/job_templates', { when('/job_templates', {
templateUrl: urlPrefix + 'partials/job_templates.html', templateUrl: urlPrefix + 'partials/job_templates.html',
controller: JobTemplatesList controller: JobTemplatesList
@@ -280,6 +288,11 @@ var tower = angular.module('Tower', [
controller: InventoriesManage controller: InventoriesManage
}). }).
when('/inventories/:inventory_id/adhoc', {
templateUrl: urlPrefix + 'partials/adhoc.html',
controller: AdhocForm
}).
when('/organizations', { when('/organizations', {
templateUrl: urlPrefix + 'partials/organizations.html', templateUrl: urlPrefix + 'partials/organizations.html',
controller: OrganizationsList controller: OrganizationsList

View File

@@ -0,0 +1,171 @@
/*************************************************
* Copyright (c) 2015 AnsibleWorks, Inc.
*
* Adhoc.js
*
* Controller functions for the Adhoc model.
*
*/
/**
* @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.
*/
export function AdhocForm($scope, $rootScope, $location, $routeParams,
AdhocForm, GenerateForm, Rest, ProcessErrors, ClearScope, GetBasePath,
GetChoices, KindChange, LookUpInit, CredentialList, Empty, OwnerChange,
LoginMethodChange, Wait) {
ClearScope();
var url = GetBasePath('inventory') + $routeParams.inventory_id
+ '/ad_hoc_commands/',
generator = GenerateForm,
form = AdhocForm,
master = {},
id = $routeParams.inventory_id;
// inject the adhoc command form
generator.inject(form, { mode: 'edit', related: true, scope: $scope });
generator.reset();
// BEGIN: populate scope with the things needed to make the adhoc form
// display
Wait('start');
$scope.id = id;
$scope.argsPopOver = "<p>These arguments are used with the"
+ " specified module.</p>";
// fix arguments help popover based on the module selected
$scope.moduleChange = function () {
// NOTE: for selenium testing link -
// link will be displayed with id adhoc_module_arguments_docs_link
// only when a module is selected
if ($scope.module_name) {
// give the docs for the selected module
$scope.argsPopOver = "<p>These arguments are used with the"
+ " specified module. You can find information about the "
+ $scope.module_name.value
+ " <a id=\"adhoc_module_arguments_docs_link_for_module_"
+ $scope.module_name.value
+ "\""
+ " href=\"http://docs.ansible.com/" + $scope.module_name.value
+ "_module.html\" target=\"_blank\">here</a>.</p>";
} else {
// no module selected
$scope.argsPopOver = "<p>These arguments are used with the"
+ " specified module.</p>";
}
};
// pre-populate hostPatterns from the inventory page and
// delete the value off of rootScope
$scope.limit = $rootScope.hostPatterns || "all";
delete $rootScope.hostPatterns;
if ($scope.removeChoicesReady) {
$scope.removeChoicesReady();
}
$scope.removeChoicesReady = $scope.$on('choicesReadyAdhoc', function () {
LookUpInit({
scope: $scope,
form: form,
current_item: (!Empty($scope.credential_id)) ? $scope.credential_id : null,
list: CredentialList,
field: 'credential',
input_type: 'radio'
});
OwnerChange({ scope: $scope });
LoginMethodChange({ scope: $scope });
Wait('stop'); // END: form population
});
// setup Machine Credential lookup
GetChoices({
scope: $scope,
url: url,
field: 'module_name',
variable: 'adhoc_module_options',
callback: 'choicesReadyAdhoc'
});
// Handle Owner change
$scope.ownerChange = function () {
OwnerChange({ scope: $scope });
};
// Handle Login Method change
$scope.loginMethodChange = function () {
LoginMethodChange({ scope: $scope });
};
// Handle Kind change
$scope.kindChange = function () {
KindChange({ scope: $scope, form: form, reset: true });
};
// launch the job with the provided form data
$scope.launchJob = function () {
var fld, data={};
// stub the payload with defaults from DRF
data = {
"job_type": "run",
"limit": "",
"credential": null,
"module_name": "command",
"module_args": "",
"forks": 0,
"verbosity": 0,
"privilege_escalation": ""
};
generator.clearApiErrors();
// populate data with the relevant form values
for (fld in form.fields) {
if (form.fields[fld].type === 'select') {
data[fld] = $scope[fld].value
} else {
data[fld] = $scope[fld];
}
}
Wait('start');
// Launch the adhoc job
Rest.setUrl(url);
Rest.post(data)
.success(function (data) {
Wait('stop');
$location.path("/ad_hoc_commands/" + data.id);
})
.error(function (data, status) {
ProcessErrors($scope, data, status, form, { hdr: 'Error!',
msg: 'Failed to launch adhoc command. POST returned status: '
+ status });
// TODO: still need to implement popping up a password prompt
// if the credential requires it. The way that the current end-
// point works is that I find out if I need to ask for a
// password from POST, thus I get an error response.
$scope.formReset();
});
};
// Remove all data input into the form
$scope.formReset = function () {
generator.reset();
for (var fld in master) {
$scope[fld] = master[fld];
}
KindChange({ scope: $scope, form: form, reset: false });
OwnerChange({ scope: $scope });
LoginMethodChange({ scope: $scope });
};
}
AdhocForm.$inject = ['$scope', '$rootScope', '$location', '$routeParams',
'AdhocForm', 'GenerateForm', 'Rest', 'ProcessErrors', 'ClearScope',
'GetBasePath', 'GetChoices', 'KindChange', 'LookUpInit', 'CredentialList',
'Empty', 'OwnerChange', 'LoginMethodChange', 'Wait'];

View File

@@ -870,6 +870,53 @@ export function InventoriesManage ($log, $scope, $rootScope, $location,
show_failures: false show_failures: false
}]; }];
// TODO: only display adhoc button if the user has permission to use it.
// TODO: figure out how to get the action-list partial to update so that
// the tooltip can be changed based off things being selected or not.
$scope.adhocButtonTipContents = "Launch adhoc command for the inventory";
// watcher for the group list checkbox changes
$scope.$on('multiSelectList.selectionChanged', function(e, selection) {
if (selection.length > 0) {
$scope.groupsSelected = true;
// $scope.adhocButtonTipContents = "Launch adhoc command for the "
// + "selected groups and hosts.";
} else {
$scope.groupsSelected = false;
// $scope.adhocButtonTipContents = "Launch adhoc command for the "
// + "inventory.";
}
$scope.groupsSelectedItems = selection.selectedItems;
});
// watcher for the host list checkbox changes
hostScope.$on('multiSelectList.selectionChanged', function(e, selection) {
// you need this so that the event doesn't bubble to the watcher above
// for the host list
e.stopPropagation();
if (selection.length > 0) {
$scope.hostsSelected = true;
// $scope.adhocButtonTipContents = "Launch adhoc command for the "
// + "selected groups and hosts.";
} else {
$scope.hostsSelected = false;
// $scope.adhocButtonTipContents = "Launch adhoc command for the "
// + "inventory.";
}
$scope.hostsSelectedItems = selection.selectedItems;
});
// populates host patterns based on selected hosts/groups
$scope.populateAdhocForm = function() {
var host_patterns = "all";
if ($scope.hostsSelected || $scope.groupsSelected) {
var allSelectedItems = $scope.groupsSelectedItems.concat($scope.hostsSelectedItems)
host_patterns = _.pluck(allSelectedItems, "name").join(":");
}
$rootScope.hostPatterns = host_patterns;
$location.path('/inventories/' + $scope.inventory.id + '/adhoc');
}
$scope.refreshHostsOnGroupRefresh = false; $scope.refreshHostsOnGroupRefresh = false;
$scope.selected_group_id = null; $scope.selected_group_id = null;

View File

@@ -11,7 +11,7 @@
*/ */
export function JobStdoutController ($log, $rootScope, $scope, $compile, $routeParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors, Socket) { export function JobStdoutController ($location, $log, $rootScope, $scope, $compile, $routeParams, ClearScope, GetBasePath, Wait, Rest, ProcessErrors, Socket) {
ClearScope(); ClearScope();
@@ -170,7 +170,9 @@ export function JobStdoutController ($log, $rootScope, $scope, $compile, $routeP
} }
}); });
Rest.setUrl(GetBasePath('jobs') + job_id + '/'); // Note: could be ad_hoc_commands or jobs
var jobType = $location.path().replace(/^\//, '').split('/')[0];
Rest.setUrl(GetBasePath(jobType) + job_id + '/');
Rest.get() Rest.get()
.success(function(data) { .success(function(data) {
$scope.job = data; $scope.job = data;
@@ -270,6 +272,5 @@ export function JobStdoutController ($log, $rootScope, $scope, $compile, $routeP
} }
JobStdoutController.$inject = [ '$log', '$rootScope', '$scope', '$compile', '$routeParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors', JobStdoutController.$inject = [ '$location', '$log', '$rootScope', '$scope', '$compile', '$routeParams', 'ClearScope', 'GetBasePath', 'Wait', 'Rest', 'ProcessErrors',
'Socket' ]; 'Socket' ];

View File

@@ -65,6 +65,15 @@ export function PermissionsList($scope, $rootScope, $location, $log, $routeParam
} }
}; };
// if the permission includes adhoc (and is not admin), display that
$scope.getPermissionText = function () {
if (this.permission.permission_type !== "admin" && this.permission.run_ad_hoc_commands) {
return this.permission.permission_type + " + ad hoc";
} else {
return this.permission.permission_type;
}
};
$scope.editPermission = function (id) { $scope.editPermission = function (id) {
$location.path($location.path() + '/' + id); $location.path($location.path() + '/' + id);
}; };
@@ -156,6 +165,11 @@ export function PermissionsAdd($scope, $rootScope, $compile, $location, $log, $r
for (fld in form.fields) { for (fld in form.fields) {
data[fld] = $scope[fld]; data[fld] = $scope[fld];
} }
// job template (or deploy) based permissions do not have the run
// ad hoc commands parameter
if (data.category === "Deploy") {
data.run_ad_hoc_commands = false;
}
url = (base === 'teams') ? GetBasePath('teams') + id + '/permissions/' : GetBasePath('users') + id + '/permissions/'; url = (base === 'teams') ? GetBasePath('teams') + id + '/permissions/' : GetBasePath('users') + id + '/permissions/';
Rest.setUrl(url); Rest.setUrl(url);
Rest.post(data) Rest.post(data)
@@ -305,6 +319,11 @@ export function PermissionsEdit($scope, $rootScope, $compile, $location, $log, $
for (fld in form.fields) { for (fld in form.fields) {
data[fld] = $scope[fld]; data[fld] = $scope[fld];
} }
// job template (or deploy) based permissions do not have the run
// ad hoc commands parameter
if (data.category === "Deploy") {
data.run_ad_hoc_commands = false;
}
Rest.setUrl(defaultUrl); Rest.setUrl(defaultUrl);
if($scope.category === "Inventory"){ if($scope.category === "Inventory"){
delete data.project; delete data.project;

View File

@@ -294,6 +294,15 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeP
$routeParams.id + '. GET status: ' + status }); $routeParams.id + '. GET status: ' + status });
}); });
// if the permission includes adhoc (and is not admin), display that
$scope.getPermissionText = function () {
if (this.permission.permission_type !== "admin" && this.permission.run_ad_hoc_commands) {
return this.permission.permission_type + " + ad hoc";
} else {
return this.permission.permission_type;
}
};
// Save changes to the parent // Save changes to the parent
$scope.formSave = function () { $scope.formSave = function () {
var data = {}, fld; var data = {}, fld;

View File

@@ -1,5 +1,6 @@
import ActivityDetail from "tower/forms/ActivityDetail"; import ActivityDetail from "tower/forms/ActivityDetail";
import Credentials from "tower/forms/Credentials"; import Credentials from "tower/forms/Credentials";
import Adhoc from "tower/forms/Adhoc";
import CustomInventory from "tower/forms/CustomInventory"; import CustomInventory from "tower/forms/CustomInventory";
import EventsViewer from "tower/forms/EventsViewer"; import EventsViewer from "tower/forms/EventsViewer";
import Groups from "tower/forms/Groups"; import Groups from "tower/forms/Groups";
@@ -30,6 +31,7 @@ import Users from "tower/forms/Users";
export export
{ ActivityDetail, { ActivityDetail,
Credentials, Credentials,
Adhoc,
CustomInventory, CustomInventory,
EventsViewer, EventsViewer,
Groups, Groups,

View File

@@ -0,0 +1,98 @@
/*********************************************
* Copyright (c) 2015 AnsibleWorks, Inc.
*
* Adhoc.js
* Form definition for the Adhoc model.
*
*/
/**
* @ngdoc function
* @name forms.function:Adhoc
* @description This form is for executing an adhoc command
*/
export default
angular.module('AdhocFormDefinition', [])
.value('AdhocForm', {
editTitle: '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()',
editRequired: 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: 'See adhoc controller...set as argsPopOver',
dataTitle: 'Arguments',
dataPlacement: 'right',
dataContainer: 'body',
editRequired: false,
autocomplete: false
},
limit: {
label: 'Host Pattern',
type: 'text',
addRequired: false,
editRequired: false,
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: 'Host Pattern',
dataPlacement: 'right',
dataContainer: 'body'
},
credential: {
label: 'Machine Credential',
type: 'lookup',
sourceModel: 'credential',
sourceField: 'name',
ngClick: 'lookUpCredential()',
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: {
variable: 'credRequired',
init: 'false'
}
}
},
buttons: {
launch: {
label: 'Launch',
ngClick: 'launchJob()',
ngDisabled: true
},
reset: {
ngClick: 'formReset()',
ngDisabled: true
}
},
related: {}
});

View File

@@ -96,6 +96,7 @@ export default
label: 'Permission', label: 'Permission',
labelClass: 'prepend-asterisk', labelClass: 'prepend-asterisk',
type: 'radio_group', type: 'radio_group',
class: 'squeeze',
options: [{ options: [{
label: 'Read', label: 'Read',
value: 'read', value: 'read',
@@ -121,11 +122,26 @@ export default
value: 'check', value: 'check',
ngShow: "category == 'Deploy'" ngShow: "category == 'Deploy'"
}], }],
// hack: attach helpCollapse here if the permissions
// category is deploy
helpCollapse: [{
hdr: 'Permission',
ngBind: 'permissionTypeHelp',
ngHide: "category == 'Inventory'"
}]
},
run_ad_hoc_commands: {
label: 'Execute commands',
type: 'checkbox',
// hack: attach helpCollapse here if the permissions
// category is inventory
helpCollapse: [{ helpCollapse: [{
hdr: 'Permission', hdr: 'Permission',
ngBind: 'permissionTypeHelp' ngBind: 'permissionTypeHelp'
}] }],
} ngShow: "category == 'Inventory'",
associated: 'permission_type'
},
}, },
buttons: { buttons: {

View File

@@ -211,9 +211,9 @@ export default
ngBind: 'permission.summary_fields.project.name' ngBind: 'permission.summary_fields.project.name'
}, },
permission_type: { permission_type: {
label: 'Permission' label: 'Permission',
ngBind: 'getPermissionText()'
} }
}, },
fieldActions: { fieldActions: {

View File

@@ -39,6 +39,7 @@ import Refresh from "tower/helpers/refresh";
import RelatedSearch from "tower/helpers/related-search"; import RelatedSearch from "tower/helpers/related-search";
import Search from "tower/helpers/search"; import Search from "tower/helpers/search";
import Teams from "tower/helpers/teams"; import Teams from "tower/helpers/teams";
import AdhocHelper from "tower/helpers/Adhoc";
export export
{ AboutAnsible, { AboutAnsible,
@@ -78,5 +79,6 @@ export
Refresh, Refresh,
RelatedSearch, RelatedSearch,
Search, Search,
Teams Teams,
AdhocHelper
}; };

View File

@@ -0,0 +1,147 @@
/*********************************************
* Copyright (c) 2015 AnsibleWorks, Inc.
*
* AdhocHelper
*
* Routines shared by adhoc related controllers:
*/
/**
* @ngdoc function
* @name helpers.function:Adhoc
* @description routines shared by adhoc related controllers
* AdhocRun is currently only used for _relaunching_ an adhoc command
* from the Jobs page.
* TODO: once the API endpoint is figured out for running an adhoc command
* from the form is figured out, the rest work should probably be excised from
* the controller and moved into here. See the todo statements in the
* controller for more information about this.
*/
export default
angular.module('AdhocHelper', ['RestServices', 'Utilities',
'CredentialFormDefinition', 'CredentialsListDefinition', 'LookUpHelper',
'JobSubmissionHelper', 'JobTemplateFormDefinition', 'ModalDialog',
'FormGenerator', 'JobVarsPromptFormDefinition'])
/**
* @ngdoc method
* @name helpers.function:JobSubmission#AdhocRun
* @methodOf helpers.function:JobSubmission
* @description The adhoc Run function is run when the user clicks the relaunch button
*
*/
// Submit request to run an adhoc comamand
.factory('AdhocRun', ['$location','$routeParams', 'LaunchJob',
'PromptForPasswords', 'Rest', 'GetBasePath', 'Alert', 'ProcessErrors',
'Wait', 'Empty', 'PromptForCredential', 'PromptForVars',
'PromptForSurvey' , 'CreateLaunchDialog',
function ($location, $routeParams, LaunchJob, PromptForPasswords,
Rest, GetBasePath, Alert, ProcessErrors, Wait, Empty,
PromptForCredential, PromptForVars, PromptForSurvey,
CreateLaunchDialog) {
return function (params) {
var id = params.project_id,
scope = params.scope.$new(),
new_job_id,
launch_url,
html,
url;
// this is used to cancel a running adhoc command from
// the jobs page
if (scope.removeCancelJob) {
scope.removeCancelJob();
}
scope.removeCancelJob = scope.$on('CancelJob', function() {
// Delete the job
Wait('start');
Rest.setUrl(GetBasePath('ad_hoc_commands') + new_job_id + '/');
Rest.destroy()
.success(function() {
Wait('stop');
})
.error(function (data, status) {
ProcessErrors(scope, data, status,
null, { hdr: 'Error!',
msg: 'Call to ' + url
+ ' failed. DELETE returned status: '
+ status });
});
});
if (scope.removeAdhocLaunchFinished) {
scope.removeAdhocLaunchFinished();
}
scope.removeAdhocLaunchFinished = scope.$on('AdhocLaunchFinished',
function(e, data) {
$location.path('/ad_hoc_commands/' + data.id);
});
if (scope.removeStartAdhocRun) {
scope.removeStartAdhocRun();
}
scope.removeStartAdhocRun = scope.$on('StartAdhocRun', function() {
LaunchJob({
scope: scope,
url: url,
callback: 'AdhocLaunchFinished' // send to the adhoc
// standard out page
});
});
// start routine only if passwords need to be prompted
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
});
}); // end password prompting routine
// start the adhoc relaunch routine
Wait('start');
url = GetBasePath('ad_hoc_commands') + id + '/relaunch/';
Rest.setUrl(url);
Rest.get()
.success(function (data) {
var new_job_id = data.id,
launch_url = url;
scope.passwords_needed_to_start = data.passwords_needed_to_start;
if (!Empty(data.passwords_needed_to_start) &&
data.passwords_needed_to_start.length > 0) {
// go through the password prompt routine before
// starting the adhoc run
scope.$emit('PromptForPasswords', data.passwords_needed_to_start, html, url);
}
else {
// no prompting of passwords needed
scope.$emit('StartAdhocRun');
}
})
.error(function (data, status) {
ProcessErrors(scope, data, status, null, { hdr: 'Error!',
msg: 'Failed to get job template details. GET returned status: ' + status });
});
};
}]);

View File

@@ -70,6 +70,8 @@ angular.module('JobTemplatesHelper', ['Utilities'])
scope.example_template_id = 'N'; scope.example_template_id = 'N';
scope.setCallbackHelp(); scope.setCallbackHelp();
// this fills the job template form both on copy of the job template
// and on edit
scope.fillJobTemplate = function(){ scope.fillJobTemplate = function(){
// id = id || $rootScope.copy.id; // id = id || $rootScope.copy.id;
// Retrieve detail record and prepopulate the form // Retrieve detail record and prepopulate the form

View File

@@ -16,7 +16,7 @@ import listGenerator from 'tower/shared/list-generator/main';
export default export default
angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'JobSummaryDefinition', 'InventoryHelper', 'GeneratorHelpers', angular.module('JobsHelper', ['Utilities', 'RestServices', 'FormGenerator', 'JobSummaryDefinition', 'InventoryHelper', 'GeneratorHelpers',
'JobSubmissionHelper', 'LogViewerHelper', 'SearchHelper', 'PaginationHelpers', listGenerator.name]) 'JobSubmissionHelper', 'LogViewerHelper', 'SearchHelper', 'PaginationHelpers', 'AdhocHelper', listGenerator.name])
/** /**
* JobsControllerInit({ scope: $scope }); * JobsControllerInit({ scope: $scope });
@@ -62,7 +62,7 @@ export default
else if (job.type === 'project_update') { else if (job.type === 'project_update') {
typeId = job.project; typeId = job.project;
} }
else if (job.type === 'job' || job.type === "system_job") { else if (job.type === 'job' || job.type === "system_job" || job.type === 'ad_hoc_command') {
typeId = job.id; typeId = job.id;
} }
RelaunchJob({ scope: scope, id: typeId, type: job.type, name: job.name }); RelaunchJob({ scope: scope, id: typeId, type: job.type, name: job.name });
@@ -112,8 +112,8 @@ export default
} }
]) ])
.factory('RelaunchJob', ['RelaunchInventory', 'RelaunchPlaybook', 'RelaunchSCM', .factory('RelaunchJob', ['RelaunchInventory', 'RelaunchPlaybook', 'RelaunchSCM', 'RelaunchAdhoc',
function(RelaunchInventory, RelaunchPlaybook, RelaunchSCM) { function(RelaunchInventory, RelaunchPlaybook, RelaunchSCM, RelaunchAdhoc) {
return function(params) { return function(params) {
var scope = params.scope, var scope = params.scope,
id = params.id, id = params.id,
@@ -122,6 +122,9 @@ export default
if (type === 'inventory_update') { if (type === 'inventory_update') {
RelaunchInventory({ scope: scope, id: id}); RelaunchInventory({ scope: scope, id: id});
} }
else if (type === 'ad_hoc_command') {
RelaunchAdhoc({ scope: scope, id: id, name: name });
}
else if (type === 'job' || type === 'system_job') { else if (type === 'job' || type === 'system_job') {
RelaunchPlaybook({ scope: scope, id: id, name: name }); RelaunchPlaybook({ scope: scope, id: id, name: name });
} }
@@ -595,4 +598,12 @@ export default
id = params.id; id = params.id;
ProjectUpdate({ scope: scope, project_id: id }); ProjectUpdate({ scope: scope, project_id: id });
}; };
}])
.factory('RelaunchAdhoc', ['AdhocRun', function(AdhocRun) {
return function(params) {
var scope = params.scope,
id = params.id;
AdhocRun({ scope: scope, project_id: id, relaunch: true });
};
}]); }]);

View File

@@ -26,30 +26,52 @@ export default
scope.projectrequired = false; scope.projectrequired = false;
html = "<dl>\n" + html = "<dl>\n" +
"<dt>Read</dt>\n" + "<dt>Read</dt>\n" +
"<dd>Only allow the user or team to view the inventory.</dd>\n" + "<dd>Only allow the user or team to view the inventory."
+ "</dd>\n" +
"<dt>Write</dt>\n" + "<dt>Write</dt>\n" +
"<dd>Allow the user or team to modify hosts and groups contained in the inventory, add new hosts and groups, and perform inventory sync operations.\n" + "<dd>Allow the user or team to modify hosts and groups "
+ "contained in the inventory, add new hosts and groups"
+ ", and perform inventory sync operations.\n" +
"<dt>Admin</dt>\n" + "<dt>Admin</dt>\n" +
"<dd>Allow the user or team full access to the inventory. This includes reading, writing, deletion of the inventory and inventory sync operations.</dd>\n" + "<dd>Allow the user or team full access to the "
+ "inventory. This includes reading, writing, deletion "
+ "of the inventory, inventory sync operations, and "
+ "the ability to execute commands on the inventory."
+ "</dd>\n" +
"<dt>Execute commands</dt>\n" +
"<dd>Allow the user to execute commands on the "
+ "inventory.</dd>\n" +
"</dl>\n"; "</dl>\n";
scope.permissionTypeHelp = $sce.trustAsHtml(html); scope.permissionTypeHelp = $sce.trustAsHtml(html);
} else { } else {
scope.projectrequired = true; scope.projectrequired = true;
html = "<dl>\n" + html = "<dl>\n" +
"<dt>Create</dt>\n" + "<dt>Create</dt>\n" +
"<dd>Allow the user or team to create job templates. This implies that they have the Run and Check permissions.</dd>\n" + "<dd>Allow the user or team to create job templates. "
+ "This implies that they have the Run and Check "
+ "permissions.</dd>\n" +
"<dt>Run</dt>\n" + "<dt>Run</dt>\n" +
"<dd>Allow the user or team to run a job template from the project against the inventory. In Run mode modules will " + "<dd>Allow the user or team to run a job template from "
"be executed, and changes to the inventory will occur.</dd>\n" + + "the project against the inventory. In Run mode "
+ "modules will " +
"be executed, and changes to the inventory will occur."
+ "</dd>\n" +
"<dt>Check</dt>\n" + "<dt>Check</dt>\n" +
"<dd>Only allow the user or team to run the project against the inventory as a dry-run operation. In Check mode, module operations " + "<dd>Only allow the user or team to run the project "
"will only be simulated. No changes will occur.</dd>\n" + + "against the inventory as a dry-run operation. In "
+ "Check mode, module operations " +
"will only be simulated. No changes will occur."
+ "</dd>\n" +
"</dl>\n"; "</dl>\n";
scope.permissionTypeHelp = $sce.trustAsHtml(html); scope.permissionTypeHelp = $sce.trustAsHtml(html);
} }
if (reset) { if (reset) {
scope.permission_type = (scope.category === 'Inventory') ? 'read' : 'run'; //default to the first option if (scope.category === "Inventory") {
scope.permission_type = "read";
} else {
scope.permission_type = "run";
}
} }
}; };
} }

View File

@@ -16,6 +16,7 @@ export default
index: false, index: false,
hover: true, hover: true,
'class': 'table-no-border', 'class': 'table-no-border',
multiSelect: true,
fields: { fields: {
name: { name: {
@@ -78,6 +79,17 @@ export default
}, },
actions: { actions: {
launch: {
mode: 'all',
// TODO: ngShow permissions
ngClick: 'populateAdhocForm()',
awToolTip: "Run a command on this inventory"
// TODO: set up a tip watcher and change text based on when
// things are selected/not selected. This is started and
// commented out in the inventory controller within the watchers.
// awToolTip: "{{ adhocButtonTipContents }}",
// dataTipWatch: "adhocButtonTipContents"
},
create: { create: {
mode: 'all', mode: 'all',
ngClick: "createGroup()", ngClick: "createGroup()",

View File

@@ -41,7 +41,8 @@ export default
ngBind: 'permission.summary_fields.project.name' ngBind: 'permission.summary_fields.project.name'
}, },
permission_type: { permission_type: {
label: 'Permission' label: 'Permission',
ngBind: 'getPermissionText()'
} }
}, },

View File

@@ -609,6 +609,7 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
params.content = collapse_array[i].content; params.content = collapse_array[i].content;
params.idx = this.accordion_count++; params.idx = this.accordion_count++;
params.show = (collapse_array[i].show) ? collapse_array[i].show : null; params.show = (collapse_array[i].show) ? collapse_array[i].show : null;
params.ngHide = (collapse_array[i].ngHide) ? collapse_array[i].ngHide : null;
params.bind = (collapse_array[i].ngBind) ? collapse_array[i].ngBind : null; params.bind = (collapse_array[i].ngBind) ? collapse_array[i].ngBind : null;
html += HelpCollapse(params); html += HelpCollapse(params);
} }
@@ -676,6 +677,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += Attr(field, 'type'); html += Attr(field, 'type');
html += "ng-model=\"" + fld + '" '; html += "ng-model=\"" + fld + '" ';
html += "name=\"" + fld + '" '; html += "name=\"" + fld + '" ';
if (form.name === "permission") {
html += "ng-disabled='permission_type === \"admin\"'";
html += "ng-checked='permission_type === \"admin\"'";
}
html += (field.ngChange) ? Attr(field, 'ngChange') : ""; html += (field.ngChange) ? Attr(field, 'ngChange') : "";
html += "id=\"" + form.name + "_" + fld + "_chbox\" "; html += "id=\"" + form.name + "_" + fld + "_chbox\" ";
html += (idx !== undefined) ? "_" + idx : ""; html += (idx !== undefined) ? "_" + idx : "";
@@ -752,7 +757,9 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
} }
if ((!field.readonly) || (field.readonly && options.mode === 'edit')) { if ((!field.readonly) || (field.readonly && options.mode === 'edit')) {
html += "<div class='form-group' "; html += "<div class='form-group ";
html += (field['class']) ? (field['class']) : "";
html += "'";
html += (field.ngShow) ? this.attr(field, 'ngShow') : ""; html += (field.ngShow) ? this.attr(field, 'ngShow') : "";
html += (field.ngHide) ? this.attr(field, 'ngHide') : ""; html += (field.ngHide) ? this.attr(field, 'ngHide') : "";
html += ">\n"; html += ">\n";
@@ -1246,6 +1253,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
if (horizontal) { if (horizontal) {
html += "</div>\n"; html += "</div>\n";
} }
html += (field.helpCollapse) ? this.buildHelpCollapse(field.helpCollapse) : '';
} }
//radio group //radio group
@@ -1642,6 +1651,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
button.label = 'Reset'; button.label = 'Reset';
button['class'] = 'btn-default'; button['class'] = 'btn-default';
} }
if (btn === 'launch') {
button.label = 'Launch';
button['class'] = 'btn-primary';
}
// Build button HTML // Build button HTML
html += "<button type=\"button\" "; html += "<button type=\"button\" ";

View File

@@ -130,6 +130,9 @@ angular.module('GeneratorHelpers', [systemStatus.name])
case 'submit': case 'submit':
icon = 'fa-rocket'; icon = 'fa-rocket';
break; break;
case 'launch':
icon = 'fa-rocket';
break;
case 'stream': case 'stream':
icon = 'fa-clock-o'; icon = 'fa-clock-o';
break; break;
@@ -652,12 +655,14 @@ angular.module('GeneratorHelpers', [systemStatus.name])
var hdr = params.hdr, var hdr = params.hdr,
content = params.content, content = params.content,
show = params.show, show = params.show,
ngHide = params.ngHide,
idx = params.idx, idx = params.idx,
bind = params.bind, bind = params.bind,
html = ''; html = '';
html += "<div class=\"panel-group collapsible-help\" "; html += "<div class=\"panel-group collapsible-help\" ";
html += (show) ? "ng-show=\"" + show + "\"" : ""; html += (show) ? "ng-show=\"" + show + "\" " : "";
html += (ngHide) ? "ng-hide=\"" + ngHide + "\" " : "";
html += ">\n"; html += ">\n";
html += "<div class=\"panel panel-default\">\n"; html += "<div class=\"panel panel-default\">\n";
html += "<div class=\"panel-heading\" ng-click=\"accordionToggle('#accordion" + idx + "')\">\n"; html += "<div class=\"panel-heading\" ng-click=\"accordionToggle('#accordion" + idx + "')\">\n";

View File

@@ -1,9 +1,12 @@
<span ng-repeat="(name, options) in list.actions"> <span ng-repeat="(name, options) in list.actions">
<!-- TODO: Unfortunately, the data-tip-watch attribute is not loaded for
some reason -->
<button <button
toolbar-button toolbar-button
mode="options.mode" mode="options.mode"
icon-name="{{name}}" icon-name="{{name}}"
aw-tool-tip="{{options.awToolTip}}" aw-tool-tip="{{options.awToolTip}}"
data-tip-watch="{{options.dataTipWatch}}"
data-placement="{{options.dataPlacement}}" data-placement="{{options.dataPlacement}}"
data-container="{{options.dataContainer}}" data-container="{{options.dataContainer}}"
class="options.class" class="options.class"

View File

@@ -1535,9 +1535,9 @@ input[type="checkbox"].checkbox-no-label {
} }
} }
// Inventory edit dialog, source form, ec2 // ad hoc permission checkbox
#source_form.squeeze .form-group { .squeeze.form-group {
margin-bottom: 10px; margin-bottom: 10px;
} }
.disabled { .disabled {

View File

@@ -0,0 +1,4 @@
<div class="tab-pane" id="credentials">
<div ng-cloak id="htmlTemplate">
</div>
</div>

View File

@@ -0,0 +1,50 @@
<div class="tab-pane" id="jobs-stdout">
<div ng-cloak id="htmlTemplate">
<div class="row">
<div id="breadcrumb-container" class="col-md-6"
style="position: relative;">
<ul class="ansible-breadcrumb" id="breadcrumb-list">
<li><a href="/#/jobs">Jobs</a></li>
<li class="active">
<a href="/#/ad_hoc_commands/{{ job.id }}">
{{ job.id }} - {{ job.name }} standard out
</a>
</li>
</ul>
</div>
<div id="home-list-actions"
class="list-actions pull-right col-md-6">
<button type="button" class="btn btn-xs btn-primary ng-hide"
ng-click="refresh()" id="refresh_btn"
aw-tool-tip="Refresh the page"
data-placement="top" ng-show="socketStatus == 'error'"
data-original-title="" title="">
<i class="fa fa-refresh fa-lg"></i>
</button>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div id="job-status">
<label>Job Status</label>
<i class="fa icon-job-{{ job.status }}"></i> {{ job.status }}
</div>
<div class="scroll-spinner" id="stdoutMoreRowsTop">
<i class="fa fa-cog fa-spin"></i>
</div>
<div id="pre-container" class="body_background
body_foreground pre mono-space"
lr-infinite-scroll="stdOutScrollToTop"
scroll-threshold="300" data-direction="up" time-threshold="500">
<div id="pre-container-content"></div>
</div>
</div>
<div class="scroll-spinner" id="stdoutMoreRowsBottom">
<i class="fa fa-cog fa-spin"></i>
</div>
</div>
</div>
</div>