Merge pull request #3174 from jbradberry/org_hosts_limit

[WIP] Org hosts limit

Reviewed-by: https://github.com/softwarefactory-project-zuul[bot]
This commit is contained in:
softwarefactory-project-zuul[bot]
2019-02-28 21:27:50 +00:00
committed by GitHub
20 changed files with 414 additions and 106 deletions

View File

@@ -236,6 +236,18 @@ function getLicenseErrorDetails () {
return { label, value };
}
function getHostLimitErrorDetails () {
if (!resource.model.has('org_host_limit_error')) {
return null;
}
const label = strings.get('labels.HOST_LIMIT_ERROR');
const tooltip = strings.get('tooltips.HOST_LIMIT');
const value = resource.model.get('org_host_limit_error');
return { tooltip, label, value };
}
function getLaunchedByDetails () {
const createdBy = resource.model.get('summary_fields.created_by');
const jobTemplate = resource.model.get('summary_fields.job_template');
@@ -804,6 +816,7 @@ function JobDetailsController (
vm.overwrite = getOverwriteDetails();
vm.overwriteVars = getOverwriteVarsDetails();
vm.licenseError = getLicenseErrorDetails();
vm.hostLimitError = getHostLimitErrorDetails();
// Relaunch and Delete Components
vm.job = angular.copy(_.get(resource.model, 'model.GET', {}));

View File

@@ -81,6 +81,26 @@
</div>
</div>
<!-- HOST LIMIT ERROR DETAIL -->
<div class="JobResults-resultRow" ng-show="vm.hostLimitError">
<label class="JobResults-resultRowLabel">
{{ vm.hostLimitError.label }}
<a id="awp-hostLimitError"
href=""
aw-pop-over="{{ vm.hostLimitError.tooltip }}"
data-placement="top"
data-container="body"
class="help-link"
title="{{ vm.hostLimitError.label }}"
tabindex="-1">
<i class="fa fa-question-circle"></i>
</a>
</label>
<div class="JobResults-resultRowText">
{{ vm.hostLimitError.value }}
</div>
</div>
<!-- START TIME DETAIL -->
<div class="JobResults-resultRow" ng-if="vm.started">
<label class="JobResults-resultRowLabel">{{ vm.started.label }}</label>

View File

@@ -22,6 +22,7 @@ function OutputStrings (BaseString) {
CREDENTIAL: t.s('View the Credential'),
EXPAND_OUTPUT: t.s('Expand Output'),
EXTRA_VARS: t.s('Read-only view of extra variables added to the job template'),
HOST_LIMIT: t.s('When this field is true, the job\'s inventory belongs to an organization that has exceeded it\'s limit of hosts as defined by the system administrator.'),
INVENTORY: t.s('View the Inventory'),
INVENTORY_SCM: t.s('View the Project'),
INVENTORY_SCM_JOB: t.s('View Project checkout results'),
@@ -57,6 +58,7 @@ function OutputStrings (BaseString) {
EXTRA_VARS: t.s('Extra Variables'),
FINISHED: t.s('Finished'),
FORKS: t.s('Forks'),
HOST_LIMIT_ERROR: t.s('Host Limit Error'),
INSTANCE_GROUP: t.s('Instance Group'),
INVENTORY: t.s('Inventory'),
INVENTORY_SCM: t.s('Source Project'),

View File

@@ -39,93 +39,110 @@ function atLaunchTemplateCtrl (
if (vm.template.type === 'job_template') {
const selectedJobTemplate = jobTemplate.create();
const preLaunchPromises = [
selectedJobTemplate.getLaunch(vm.template.id),
selectedJobTemplate.optionsLaunch(vm.template.id),
selectedJobTemplate.getLaunch(vm.template.id)
.catch(createErrorHandler(`/api/v2/job_templates/${vm.template.id}/launch`, 'GET')),
selectedJobTemplate.optionsLaunch(vm.template.id)
.catch(createErrorHandler(`/api/v2/job_templates/${vm.template.id}/launch`, 'OPTIONS'))
];
Promise.all(preLaunchPromises)
.then(([launchData, launchOptions]) => {
if (selectedJobTemplate.canLaunchWithoutPrompt()) {
selectedJobTemplate
.postLaunch({ id: vm.template.id })
.then(({ data }) => {
/* Slice Jobs: Redirect to WF Details page if returned
job type is a WF job */
if (data.type === 'workflow_job' && data.workflow_job !== null) {
$state.go('workflowResults', { id: data.workflow_job }, { reload: true });
} else {
$state.go('output', { id: data.job, type: 'playbook' }, { reload: true });
}
});
} else {
const promptData = {
launchConf: launchData.data,
launchOptions: launchOptions.data,
template: vm.template.id,
templateType: vm.template.type,
prompts: PromptService.processPromptValues({
launchConf: launchData.data,
launchOptions: launchOptions.data
}),
triggerModalOpen: true
};
if (launchData.data.survey_enabled) {
selectedJobTemplate.getSurveyQuestions(vm.template.id)
// If we don't get both of these things then one of the
// promises was rejected
if (launchData && launchOptions) {
if (selectedJobTemplate.canLaunchWithoutPrompt()) {
selectedJobTemplate
.postLaunch({ id: vm.template.id })
.then(({ data }) => {
const processed = PromptService.processSurveyQuestions({
surveyQuestions: data.spec
});
promptData.surveyQuestions = processed.surveyQuestions;
vm.promptData = promptData;
});
/* Slice Jobs: Redirect to WF Details page if returned
job type is a WF job */
if (data.type === 'workflow_job' && data.workflow_job !== null) {
$state.go('workflowResults', { id: data.workflow_job }, { reload: true });
} else {
$state.go('output', { id: data.job, type: 'playbook' }, { reload: true });
}
})
.catch(createErrorHandler(`/api/v2/job_templates/${vm.template.id}/launch`, 'POST'));
} else {
vm.promptData = promptData;
const promptData = {
launchConf: launchData.data,
launchOptions: launchOptions.data,
template: vm.template.id,
templateType: vm.template.type,
prompts: PromptService.processPromptValues({
launchConf: launchData.data,
launchOptions: launchOptions.data
}),
triggerModalOpen: true
};
if (launchData.data.survey_enabled) {
selectedJobTemplate.getSurveyQuestions(vm.template.id)
.then(({ data }) => {
const processed = PromptService.processSurveyQuestions({
surveyQuestions: data.spec
});
promptData.surveyQuestions = processed.surveyQuestions;
vm.promptData = promptData;
})
.catch(createErrorHandler(`/api/v2/job_templates/${vm.template.id}/survey_spec`, 'GET'));
} else {
vm.promptData = promptData;
}
}
}
});
} else if (vm.template.type === 'workflow_job_template') {
const selectedWorkflowJobTemplate = workflowTemplate.create();
const preLaunchPromises = [
selectedWorkflowJobTemplate.request('get', vm.template.id),
selectedWorkflowJobTemplate.getLaunch(vm.template.id),
selectedWorkflowJobTemplate.optionsLaunch(vm.template.id),
selectedWorkflowJobTemplate.request('get', vm.template.id)
.catch(createErrorHandler(`/api/v2/workflow_job_templates/${vm.template.id}`, 'GET')),
selectedWorkflowJobTemplate.getLaunch(vm.template.id)
.catch(createErrorHandler(`/api/v2/workflow_job_templates/${vm.template.id}/launch`, 'GET')),
selectedWorkflowJobTemplate.optionsLaunch(vm.template.id)
.catch(createErrorHandler(`/api/v2/workflow_job_templates/${vm.template.id}/launch`, 'OPTIONS')),
];
Promise.all(preLaunchPromises)
.then(([wfjtData, launchData, launchOptions]) => {
if (selectedWorkflowJobTemplate.canLaunchWithoutPrompt()) {
selectedWorkflowJobTemplate
.postLaunch({ id: vm.template.id })
.then(({ data }) => {
$state.go('workflowResults', { id: data.workflow_job }, { reload: true });
});
} else {
launchData.data.defaults.extra_vars = wfjtData.data.extra_vars;
const promptData = {
launchConf: selectedWorkflowJobTemplate.getLaunchConf(),
launchOptions: launchOptions.data,
template: vm.template.id,
templateType: vm.template.type,
prompts: PromptService.processPromptValues({
launchConf: selectedWorkflowJobTemplate.getLaunchConf(),
launchOptions: launchOptions.data
}),
triggerModalOpen: true,
};
if (launchData.data.survey_enabled) {
selectedWorkflowJobTemplate.getSurveyQuestions(vm.template.id)
// If we don't get all of these things then one of the
// promises was rejected
if (wfjtData && launchData && launchOptions) {
if (selectedWorkflowJobTemplate.canLaunchWithoutPrompt()) {
selectedWorkflowJobTemplate
.postLaunch({ id: vm.template.id })
.then(({ data }) => {
const processed = PromptService.processSurveyQuestions({
surveyQuestions: data.spec
});
promptData.surveyQuestions = processed.surveyQuestions;
vm.promptData = promptData;
});
$state.go('workflowResults', { id: data.workflow_job }, { reload: true });
})
.catch(createErrorHandler(`/api/v2/workflow_job_templates/${vm.template.id}/launch`, 'POST'));
} else {
vm.promptData = promptData;
launchData.data.defaults.extra_vars = wfjtData.data.extra_vars;
const promptData = {
launchConf: selectedWorkflowJobTemplate.getLaunchConf(),
launchOptions: launchOptions.data,
template: vm.template.id,
templateType: vm.template.type,
prompts: PromptService.processPromptValues({
launchConf: selectedWorkflowJobTemplate.getLaunchConf(),
launchOptions: launchOptions.data
}),
triggerModalOpen: true,
};
if (launchData.data.survey_enabled) {
selectedWorkflowJobTemplate.getSurveyQuestions(vm.template.id)
.then(({ data }) => {
const processed = PromptService.processSurveyQuestions({
surveyQuestions: data.spec
});
promptData.surveyQuestions = processed.surveyQuestions;
vm.promptData = promptData;
})
.catch(createErrorHandler(`/api/v2/workflow_job_templates/${vm.template.id}/survey_spec`, 'GET'));
} else {
vm.promptData = promptData;
}
}
}
});
@@ -163,14 +180,14 @@ function atLaunchTemplateCtrl (
} else {
$state.go('output', { id: launchRes.data.job, type: 'playbook' }, { reload: true });
}
}).catch(createErrorHandler('launch job template', 'POST'));
}).catch(createErrorHandler(`/api/v2/job_templates/${vm.template.id}/launch`, 'POST'));
} else if (vm.promptData.templateType === 'workflow_job_template') {
workflowTemplate.create().postLaunch({
id: vm.promptData.template,
launchData: jobLaunchData
}).then((launchRes) => {
$state.go('workflowResults', { id: launchRes.data.workflow_job }, { reload: true });
}).catch(createErrorHandler('launch workflow job template', 'POST'));
}).catch(createErrorHandler(`/api/v2/workflow_job_templates/${vm.template.id}/launch`, 'POST'));
}
};
}

View File

@@ -5,9 +5,9 @@
*************************************************/
export default ['$state', '$stateParams', '$scope', 'RelatedHostsFormDefinition', 'ParseTypeChange',
'GenerateForm', 'HostsService', 'rbacUiControlService', 'GetBasePath', 'ToJSON', 'canAdd',
'GenerateForm', 'HostsService', 'GetBasePath', 'ToJSON', 'canAdd',
function($state, $stateParams, $scope, RelatedHostsFormDefinition, ParseTypeChange,
GenerateForm, HostsService, rbacUiControlService, GetBasePath, ToJSON, canAdd) {
GenerateForm, HostsService, GetBasePath, ToJSON, canAdd) {
init();

View File

@@ -17,9 +17,9 @@
url: function(){
return '';
},
error: function(data, status) {
ProcessErrors($rootScope, data.data, status, null, { hdr: 'Error!',
msg: 'Call to ' + this.url + '. GET returned: ' + status });
error: function(data) {
ProcessErrors($rootScope, data.data, data.status, null, { hdr: 'Error!',
msg: 'Call to ' + this.url + '. GET returned: ' + data.status });
},
success: function(data){
return data;

View File

@@ -24,8 +24,6 @@ export default ['$scope', '$rootScope', '$location', '$stateParams',
init();
function init(){
// @issue What is this doing, why
$scope.$emit("HideOrgListHeader");
const virtualEnvs = ConfigData.custom_virtualenvs || [];
$scope.custom_virtualenvs_visible = virtualEnvs.length > 1;
$scope.custom_virtualenvs_options = virtualEnvs.filter(
@@ -43,15 +41,18 @@ export default ['$scope', '$rootScope', '$location', '$stateParams',
// Save
$scope.formSave = function() {
var fld, params = {};
Wait('start');
for (fld in form.fields) {
params[fld] = $scope[fld];
}
if (!params.max_hosts || params.max_hosts === '') {
params.max_hosts = 0;
}
var url = GetBasePath(base);
url += (base !== 'organizations') ? $stateParams.project_id + '/organizations/' : '';
Rest.setUrl(url);
Rest.post({
name: $scope.name,
description: $scope.description,
custom_virtualenv: $scope.custom_virtualenv
})
Rest.post(params)
.then(({data}) => {
const organization_id = data.id,
instance_group_url = data.related.instance_groups;
@@ -73,7 +74,7 @@ export default ['$scope', '$rootScope', '$location', '$stateParams',
let explanation = _.has(data, "name") ? data.name[0] : "";
ProcessErrors($scope, data, status, OrganizationForm, {
hdr: 'Error!',
msg: `Failed to save organization. PUT status: ${status}. ${explanation}`
msg: `Failed to save organization. POST status: ${status}. ${explanation}`
});
});
};

View File

@@ -38,7 +38,6 @@ export default ['$scope', '$location', '$stateParams', 'OrgAdminLookup',
}
});
$scope.$emit("HideOrgListHeader");
$scope.instance_groups = InstanceGroupsData;
const virtualEnvs = ConfigData.custom_virtualenvs || [];
$scope.custom_virtualenvs_visible = virtualEnvs.length > 1;
@@ -57,7 +56,7 @@ export default ['$scope', '$location', '$stateParams', 'OrgAdminLookup',
$scope.organization_name = data.name;
for (fld in form.fields) {
if (data[fld]) {
if (typeof data[fld] !== 'undefined') {
$scope[fld] = data[fld];
master[fld] = data[fld];
}
@@ -98,6 +97,9 @@ export default ['$scope', '$location', '$stateParams', 'OrgAdminLookup',
for (fld in form.fields) {
params[fld] = $scope[fld];
}
if (!params.max_hosts || params.max_hosts === '') {
params.max_hosts = 0;
}
Rest.setUrl(defaultUrl + id + '/');
Rest.put(params)
.then(() => {

View File

@@ -54,6 +54,21 @@ export default ['NotificationsList', 'i18n',
dataPlacement: 'right',
ngDisabled: '!(organization_obj.summary_fields.user_capabilities.edit || canAdd)',
ngShow: 'custom_virtualenvs_visible'
},
max_hosts: {
label: i18n._('Max Hosts'),
type: 'number',
integer: true,
min: 0,
max: 2147483647,
default: 0,
spinner: true,
dataTitle: i18n._('Max Hosts'),
dataPlacement: 'right',
dataContainer: 'body',
awPopOver: "<p>" + i18n._("The maximum number of hosts allowed to be managed by this organization. Value defaults to 0 which means no limit. Refer to the Ansible documentation for more details.") + "</p>",
ngDisabled: '!current_user.is_superuser',
ngShow: 'BRAND_NAME === "Tower"'
}
},