Merge pull request #125 from jlmitch5/adhoc_pr

UI Support for adhoc commands
This commit is contained in:
jlmitch5 2015-04-07 10:19:45 -04:00
commit 4b1322932f
30 changed files with 728 additions and 65 deletions

View File

@ -227,7 +227,7 @@ class BaseSerializer(serializers.ModelSerializer):
def get_type_choices(self):
type_name_map = {
'job': 'Playbook Run',
'ad_hoc_command': 'Ad Hoc Command',
'ad_hoc_command': 'Command',
'project_update': 'SCM Update',
'inventory_update': 'Inventory Sync',
'system_job': 'Management Job',

View File

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

View File

@ -0,0 +1,172 @@
/*************************************************
* 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 AdhocCtrl($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";
$scope.providedHostPatterns = $scope.limit;
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.
});
};
// Remove all data input into the form
$scope.formReset = function () {
generator.reset();
for (var fld in master) {
$scope[fld] = master[fld];
}
$scope.limit = $scope.providedHostPatterns;
KindChange({ scope: $scope, form: form, reset: false });
OwnerChange({ scope: $scope });
LoginMethodChange({ scope: $scope });
};
}
AdhocCtrl.$inject = ['$scope', '$rootScope', '$location', '$routeParams',
'AdhocForm', 'GenerateForm', 'Rest', 'ProcessErrors', 'ClearScope',
'GetBasePath', 'GetChoices', 'KindChange', 'LookUpInit', 'CredentialList',
'Empty', 'OwnerChange', 'LoginMethodChange', 'Wait'];

View File

@ -15,12 +15,12 @@
import 'tower/job-templates/main';
export function InventoriesList($scope, $rootScope, $location, $log, $routeParams, $compile, $filter, Rest, Alert, InventoryList, generateList,
LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller, ClearScope, ProcessErrors, GetBasePath, Wait, Stream,
export function InventoriesList($scope, $rootScope, $location, $log,
$routeParams, $compile, $filter, Rest, Alert, InventoryList, generateList,
LoadBreadCrumbs, Prompt, SearchInit, PaginateInit, ReturnToCaller,
ClearScope, ProcessErrors, GetBasePath, Wait, Stream,
EditInventoryProperties, Find, Empty, LogViewer) {
//ClearScope();
var list = InventoryList,
defaultUrl = GetBasePath('inventory'),
view = generateList,
@ -51,7 +51,10 @@ export function InventoriesList($scope, $rootScope, $location, $log, $routeParam
// close any lingering tool tipss
$(this).hide();
});
elem.attr({ "aw-pop-over": html, "data-popover-title": title, "data-placement": "right" });
elem.attr({
"aw-pop-over": html,
"data-popover-title": title,
"data-placement": "right" });
$compile(elem)($scope);
elem.on('shown.bs.popover', function() {
$('.popover').each(function() {
@ -837,12 +840,16 @@ InventoriesEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$lo
export function InventoriesManage ($log, $scope, $location, $routeParams, $compile, generateList, ClearScope, Empty, Wait, Rest, Alert, LoadBreadCrumbs, GetBasePath, ProcessErrors,
Breadcrumbs, InventoryGroups, InjectHosts, Find, HostsReload, SearchInit, PaginateInit, GetSyncStatusMsg, GetHostsStatusMsg, GroupsEdit, InventoryUpdate,
GroupsCancelUpdate, ViewUpdateStatus, GroupsDelete, Store, HostsEdit, HostsDelete, EditInventoryProperties, ToggleHostEnabled, Stream, ShowJobSummary,
InventoryGroupsHelp, HelpDialog, ViewJob, WatchInventoryWindowResize, GetHostContainerRows, GetGroupContainerRows, GetGroupContainerHeight,
GroupsCopy, HostsCopy, Socket)
{
export function InventoriesManage ($log, $scope, $rootScope, $location,
$routeParams, $compile, generateList, ClearScope, Empty, Wait, Rest, Alert,
LoadBreadCrumbs, GetBasePath, ProcessErrors, Breadcrumbs, InventoryGroups,
InjectHosts, Find, HostsReload, SearchInit, PaginateInit, GetSyncStatusMsg,
GetHostsStatusMsg, GroupsEdit, InventoryUpdate, GroupsCancelUpdate,
ViewUpdateStatus, GroupsDelete, Store, HostsEdit, HostsDelete,
EditInventoryProperties, ToggleHostEnabled, Stream, ShowJobSummary,
InventoryGroupsHelp, HelpDialog, ViewJob, WatchInventoryWindowResize,
GetHostContainerRows, GetGroupContainerRows, GetGroupContainerHeight,
GroupsCopy, HostsCopy, Socket) {
var PreviousSearchParams,
url,
@ -863,6 +870,53 @@ export function InventoriesManage ($log, $scope, $location, $routeParams, $compi
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.selected_group_id = null;
@ -1365,7 +1419,8 @@ export function InventoriesManage ($log, $scope, $location, $routeParams, $compi
hostScope.show_failures = show_failures;
$scope.groupSelect(group_id);
hostScope.hosts = [];
$scope.show_failures = show_failures; // turn on failed hosts filter in hosts view
$scope.show_failures = show_failures; // turn on failed hosts
// filter in hosts view
} else {
Wait('stop');
}
@ -1374,15 +1429,24 @@ export function InventoriesManage ($log, $scope, $location, $routeParams, $compi
if ($scope.removeGroupDeleteCompleted) {
$scope.removeGroupDeleteCompleted();
}
$scope.removeGroupDeleteCompleted = $scope.$on('GroupDeleteCompleted', function() {
$scope.refreshGroups();
});
$scope.removeGroupDeleteCompleted = $scope.$on('GroupDeleteCompleted',
function() {
$scope.refreshGroups();
}
);
}
InventoriesManage.$inject = ['$log', '$scope', '$location', '$routeParams', '$compile', 'generateList', 'ClearScope', 'Empty', 'Wait', 'Rest', 'Alert', 'LoadBreadCrumbs',
'GetBasePath', 'ProcessErrors', 'Breadcrumbs', 'InventoryGroups', 'InjectHosts', 'Find', 'HostsReload', 'SearchInit', 'PaginateInit', 'GetSyncStatusMsg',
'GetHostsStatusMsg', 'GroupsEdit', 'InventoryUpdate', 'GroupsCancelUpdate', 'ViewUpdateStatus', 'GroupsDelete', 'Store', 'HostsEdit', 'HostsDelete',
'EditInventoryProperties', 'ToggleHostEnabled', 'Stream', 'ShowJobSummary', 'InventoryGroupsHelp', 'HelpDialog', 'ViewJob', 'WatchInventoryWindowResize',
'GetHostContainerRows', 'GetGroupContainerRows', 'GetGroupContainerHeight', 'GroupsCopy', 'HostsCopy', 'Socket'
];
InventoriesManage.$inject = ['$log', '$scope', '$rootScope', '$location',
'$routeParams', '$compile', 'generateList', 'ClearScope', 'Empty', 'Wait',
'Rest', 'Alert', 'LoadBreadCrumbs', 'GetBasePath', 'ProcessErrors',
'Breadcrumbs', 'InventoryGroups', 'InjectHosts', 'Find', 'HostsReload',
'SearchInit', 'PaginateInit', 'GetSyncStatusMsg', 'GetHostsStatusMsg',
'GroupsEdit', 'InventoryUpdate', 'GroupsCancelUpdate', 'ViewUpdateStatus',
'GroupsDelete', 'Store', 'HostsEdit', 'HostsDelete',
'EditInventoryProperties', 'ToggleHostEnabled', 'Stream', 'ShowJobSummary',
'InventoryGroupsHelp', 'HelpDialog', 'ViewJob',
'WatchInventoryWindowResize', 'GetHostContainerRows',
'GetGroupContainerRows', 'GetGroupContainerHeight', 'GroupsCopy',
'HostsCopy', 'Socket'
];

View File

@ -1285,6 +1285,7 @@ export function JobDetailController ($location, $rootScope, $scope, $compile, $r
status: status
});
};
scope.refresh = function(){
$scope.$emit('LoadJob');
};

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();
@ -152,7 +152,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()
.success(function(data) {
$scope.job = data;
@ -252,5 +254,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' ];

View File

@ -1145,11 +1145,11 @@ export function JobTemplatesEdit($scope, $rootScope, $compile, $location, $log,
action: action
});
});
if($scope.survey_enabled === true && $scope.survey_exists!==true){
$scope.$emit("PromptForSurvey");
}
else {
PlaybookRun({
scope: $scope,
id: id

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 + " + run commands";
} else {
return this.permission.permission_type;
}
};
$scope.editPermission = function (id) {
$location.path($location.path() + '/' + id);
};
@ -156,6 +165,11 @@ export function PermissionsAdd($scope, $rootScope, $compile, $location, $log, $r
for (fld in form.fields) {
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/';
Rest.setUrl(url);
Rest.post(data)
@ -305,6 +319,11 @@ export function PermissionsEdit($scope, $rootScope, $compile, $location, $log, $
for (fld in form.fields) {
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);
if($scope.category === "Inventory"){
delete data.project;
@ -332,7 +351,8 @@ export function PermissionsEdit($scope, $rootScope, $compile, $location, $log, $
}
PermissionsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location', '$log', '$routeParams', 'PermissionsForm',
'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'Prompt', 'GetBasePath',
'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess', 'Wait', 'PermissionCategoryChange'
];
PermissionsEdit.$inject = ['$scope', '$rootScope', '$compile', '$location',
'$log', '$routeParams', 'PermissionsForm', 'GenerateForm', 'Rest', 'Alert',
'ProcessErrors', 'LoadBreadCrumbs', 'ReturnToCaller', 'ClearScope', 'Prompt',
'GetBasePath', 'InventoryList', 'ProjectList', 'LookUpInit', 'CheckAccess',
'Wait', 'PermissionCategoryChange'];

View File

@ -294,6 +294,15 @@ export function UsersEdit($scope, $rootScope, $compile, $location, $log, $routeP
$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 + " + run commands";
} else {
return this.permission.permission_type;
}
};
// Save changes to the parent
$scope.formSave = function () {
var data = {}, fld;

View File

@ -1,5 +1,6 @@
import ActivityDetail from "tower/forms/ActivityDetail";
import Credentials from "tower/forms/Credentials";
import Adhoc from "tower/forms/Adhoc";
import CustomInventory from "tower/forms/CustomInventory";
import EventsViewer from "tower/forms/EventsViewer";
import Groups from "tower/forms/Groups";
@ -30,6 +31,7 @@ import Users from "tower/forms/Users";
export
{ ActivityDetail,
Credentials,
Adhoc,
CustomInventory,
EventsViewer,
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',
labelClass: 'prepend-asterisk',
type: 'radio_group',
class: 'squeeze',
options: [{
label: 'Read',
value: 'read',
@ -121,11 +122,26 @@ export default
value: 'check',
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: [{
hdr: 'Permission',
ngBind: 'permissionTypeHelp'
}]
}
}],
ngShow: "category == 'Inventory'",
associated: 'permission_type'
},
},
buttons: {

View File

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

View File

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

View File

@ -0,0 +1,145 @@
/*********************************************
* 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,
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) {
new_job_id = data.id;
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

@ -17,7 +17,6 @@ angular.module('CredentialsHelper', ['Utilities'])
.factory('KindChange', ['Empty',
function (Empty) {
return function (params) {
var scope = params.scope,
reset = params.reset,
collapse, id;

View File

@ -299,7 +299,6 @@ function($compile, Rest, GetBasePath, TextareaResize,CreateDialog, GenerateForm,
html += "<div class=\"alert alert-info\">Launching this job requires the passwords listed below. Enter and confirm each password before continuing.</div>\n";
// html += "<form name=\"password_form\" novalidate>\n";
scope.passwords.forEach(function(password) {
// Prompt for password
field = form.fields[password];

View File

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

View File

@ -16,7 +16,7 @@ import listGenerator from 'tower/shared/list-generator/main';
export default
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 });
@ -62,7 +62,7 @@ export default
else if (job.type === 'project_update') {
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;
}
RelaunchJob({ scope: scope, id: typeId, type: job.type, name: job.name });
@ -112,8 +112,8 @@ export default
}
])
.factory('RelaunchJob', ['RelaunchInventory', 'RelaunchPlaybook', 'RelaunchSCM',
function(RelaunchInventory, RelaunchPlaybook, RelaunchSCM) {
.factory('RelaunchJob', ['RelaunchInventory', 'RelaunchPlaybook', 'RelaunchSCM', 'RelaunchAdhoc',
function(RelaunchInventory, RelaunchPlaybook, RelaunchSCM, RelaunchAdhoc) {
return function(params) {
var scope = params.scope,
id = params.id,
@ -122,6 +122,9 @@ export default
if (type === 'inventory_update') {
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') {
RelaunchPlaybook({ scope: scope, id: id, name: name });
}
@ -595,4 +598,12 @@ export default
id = params.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;
html = "<dl>\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" +
"<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" +
"<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";
scope.permissionTypeHelp = $sce.trustAsHtml(html);
} else {
scope.projectrequired = true;
html = "<dl>\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" +
"<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" +
"<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 " +
"be executed, and changes to the inventory will occur.</dd>\n" +
"<dd>Allow the user or team to run a job template from " +
"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" +
"<dd>Only allow the user or team to run the project against the inventory as a dry-run operation. In Check mode, module operations " +
"will only be simulated. No changes will occur.</dd>\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 " +
"will only be simulated. No changes will occur." +
"</dd>\n" +
"</dl>\n";
scope.permissionTypeHelp = $sce.trustAsHtml(html);
}
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

@ -18,7 +18,6 @@ export default
'InventoryHelper', 'InventoryFormDefinition', 'ParseHelper', 'SearchHelper', 'VariablesHelper',
])
.factory('GetGroupContainerHeight', [ function() {
return function() {
@ -101,7 +100,6 @@ export default
};
}])
.factory('WatchInventoryWindowResize', ['ApplyEllipsis', 'SetContainerHeights',
function (ApplyEllipsis, SetContainerHeights) {
return function (params) {
@ -185,7 +183,6 @@ export default
}
])
.factory('EditInventoryProperties', ['InventoryForm', 'GenerateForm', 'Rest', 'Alert', 'ProcessErrors', 'LookUpInit', 'OrganizationList',
'GetBasePath', 'ParseTypeChange', 'SaveInventory', 'Wait', 'Store', 'SearchInit', 'ParseVariableString', 'CreateDialog', 'TextareaResize',
function (InventoryForm, GenerateForm, Rest, Alert, ProcessErrors, LookUpInit, OrganizationList, GetBasePath, ParseTypeChange, SaveInventory,

View File

@ -16,6 +16,7 @@ export default
index: false,
hover: true,
'class': 'table-no-border',
multiSelect: true,
fields: {
name: {
@ -78,6 +79,17 @@ export default
},
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: {
mode: 'all',
ngClick: "createGroup()",
@ -134,7 +146,8 @@ export default
ngClick: 'updateGroup(group.id)',
awToolTip: "{{ group.launch_tooltip }}",
dataTipWatch: "group.launch_tooltip",
ngShow: "group.status !== 'running' && group.status !== 'pending' && group.status !== 'updating'",
ngShow: "group.status !== 'running' && group.status " +
"!== 'pending' && group.status !== 'updating'",
ngClass: "group.launch_class",
dataPlacement: "top"
},
@ -144,7 +157,8 @@ export default
ngClick: "cancelUpdate(group.id)",
awToolTip: "Cancel sync process",
'class': 'red-txt',
ngShow: "group.status == 'running' || group.status == 'pending' || group.status == 'updating'",
ngShow: "group.status == 'running' || group.status == 'pending' " +
"|| group.status == 'updating'",
dataPlacement: "top"
},
edit: {

View File

@ -41,7 +41,8 @@ export default
ngBind: 'permission.summary_fields.project.name'
},
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.idx = this.accordion_count++;
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;
html += HelpCollapse(params);
}
@ -676,6 +677,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
html += Attr(field, 'type');
html += "ng-model=\"" + 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 += "id=\"" + form.name + "_" + fld + "_chbox\" ";
html += (idx !== undefined) ? "_" + idx : "";
@ -752,7 +757,9 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
}
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.ngHide) ? this.attr(field, 'ngHide') : "";
html += ">\n";
@ -1246,6 +1253,8 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
if (horizontal) {
html += "</div>\n";
}
html += (field.helpCollapse) ? this.buildHelpCollapse(field.helpCollapse) : '';
}
//radio group
@ -1642,6 +1651,10 @@ angular.module('FormGenerator', [GeneratorHelpers.name, 'Utilities', listGenerat
button.label = 'Reset';
button['class'] = 'btn-default';
}
if (btn === 'launch') {
button.label = 'Launch';
button['class'] = 'btn-primary';
}
// Build button HTML
html += "<button type=\"button\" ";

View File

@ -130,6 +130,9 @@ angular.module('GeneratorHelpers', [systemStatus.name])
case 'submit':
icon = 'fa-rocket';
break;
case 'launch':
icon = 'fa-rocket';
break;
case 'stream':
icon = 'fa-clock-o';
break;
@ -646,12 +649,14 @@ angular.module('GeneratorHelpers', [systemStatus.name])
var hdr = params.hdr,
content = params.content,
show = params.show,
ngHide = params.ngHide,
idx = params.idx,
bind = params.bind,
html = '';
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 += "<div class=\"panel panel-default\">\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">
<!-- TODO: Unfortunately, the data-tip-watch attribute is not loaded for
some reason -->
<button
toolbar-button
mode="options.mode"
icon-name="{{name}}"
aw-tool-tip="{{options.awToolTip}}"
data-tip-watch="{{options.dataTipWatch}}"
data-placement="{{options.dataPlacement}}"
data-container="{{options.dataContainer}}"
class="options.class"

View File

@ -229,7 +229,6 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
selection.deselectedItems.forEach(function(item) {
item.isSelected = false;
});
}.bind(this));
this.scope.$on('$destroy', function() {
@ -396,7 +395,7 @@ export default ['$location', '$compile', '$rootScope', 'SearchWidget', 'Paginate
});
}
html += "</div><!-- list-acitons -->\n";
html += "</div><!-- list-actions -->\n";
html += "</div><!-- list-actions-column -->\n";
} else {

View File

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