/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var App = require('app');
var db = require('utils/db');
var stringUtils = require('utils/string_utils');
var blueprintUtils = require('utils/blueprint');
var validationUtils = require('utils/validator');
/**
* By Step 6, we have the following information stored in App.db and set on this
* controller by the router:
*
* hosts: App.db.hosts (list of all hosts the user selected in Step 3)
* selectedServiceNames: App.db.selectedServiceNames (the services that the user selected in Step 4)
* masterComponentHosts: App.db.masterComponentHosts (master-components-to-hosts mapping the user selected in Step 5)
*
* Step 6 will set the following information in App.db:
* slaveComponentHosts: App.db.slaveComponentHosts (slave-components-to-hosts mapping the user selected in Step 6)
*
*/
App.WizardStep6Controller = Em.Controller.extend(App.BlueprintMixin, {
/**
* List of hosts
* @type {object[]}
*/
hosts: [],
/**
* List of components info about selecting/deselecting status for components.
*
* @type {Array}
* @item {Em.Object}
* @property name {String} - component name
* @property label {String} - component display name
* @property allChecked {bool} - all checkboxes are checked
* @property noChecked {bool} - no checkboxes checked
*/
headers: [],
/**
* @type {bool}
*/
isLoaded: false,
/**
* Indication if user has chosen hosts to install clients
* @type {bool}
*/
isClientsSet: false,
/**
* Define state for submit button
* @type {bool}
*/
submitDisabled: false,
/**
* Check if addHostWizard
used
* @type {bool}
*/
isAddHostWizard: function () {
return this.get('content.controllerName') === 'addHostController';
}.property('content.controllerName'),
/**
* Check if installerWizard
used
* @type {bool}
*/
isInstallerWizard: function () {
return this.get('content.controllerName') === 'installerController';
}.property('content.controllerName'),
/**
* Check if addServiceWizard
used
* @type {bool}
*/
isAddServiceWizard: function () {
return this.get('content.controllerName') === 'addServiceController';
}.property('content.controllerName'),
installedServiceNames: function () {
return this.get('content.services').filterProperty('isInstalled').mapProperty('serviceName');
}.property('content.services').cacheable(),
/**
* Validation error messages which don't related with any master
*/
generalErrorMessages: [],
/**
* Validation warning messages which don't related with any master
*/
generalWarningMessages: [],
/**
* true if validation has any general (which is not related with concrete host) error message
*/
anyGeneralErrors: function() {
var messages = this.get('generalErrorMessages');
return this.get('errorMessage') || (messages && messages.length > 0);
}.property('generalErrorMessages', 'generalErrorMessages.@each', 'errorMessage'),
/**
* true if validation has any general (which is not related with concrete host) warning message
*/
anyGeneralWarnings: function() {
var messages = this.get('generalWarningMessages');
return messages && messages.length > 0;
}.property('generalWarningMessages', 'generalWarningMessages.@each'),
/**
* true if validation has any general (which is not related with concrete host) error or warning message
*/
anyGeneralIssues: function () {
return this.get('anyGeneralErrors') || this.get('anyGeneralWarnings');
}.property('anyGeneralErrors', 'anyGeneralWarnings'),
/**
* true if validation has any error message (general or host specific)
*/
anyErrors: function() {
return this.get('anyGeneralErrors') || this.get('hosts').some(function(h) { return h.get('errorMessages').length > 0; });
}.property('anyGeneralErrors', 'hosts.@each.errorMessages'),
/**
* true if validation has any warning message (general or host specific)
*/
anyWarnings: function() {
return this.get('anyGeneralWarnings') || this.get('hosts').some(function(h) { return h.get('warnMessages').length > 0; });
}.property('anyGeneralWarnings', 'hosts.@each.warnMessages'),
openSlavesAndClientsIssues: function () {
App.ModalPopup.show({
header: Em.I18n.t('installer.step6.validationSlavesAndClients.popup.header'),
bodyClass: Em.View.extend({
controller: this,
templateName: require('templates/wizard/step6/step6_issues_popup')
}),
secondary: null
});
},
/**
* Verify condition that at least one checkbox of each component was checked
* @method clearError
*/
clearError: function () {
var self = this;
var isError = false;
var err = false;
var hosts = this.get('hosts');
var headers = this.get('headers');
var headersMap = {};
headers.forEach(function (header) {
headersMap[header.name] = true;
});
hosts.forEach(function (host) {
host.get('checkboxes').forEach(function (checkbox) {
if (headersMap[checkbox.get('component')]) {
headersMap[checkbox.get('component')] = !checkbox.get('checked');
}
});
});
for (var i in headersMap) {
err |= headersMap[i];
}
if (!err) {
this.set('errorMessage', '');
}
if (this.get('isAddHostWizard')) {
hosts.forEach(function (host) {
isError = false;
headers.forEach(function (header) {
isError |= host.get('checkboxes').findProperty('title', header.get('label')).checked;
});
isError = !isError;
if (!isError) {
self.set('errorMessage', '');
}
});
}
},
/**
* Clear Step6 data like hosts
, headers
etc
* @method clearStep
*/
clearStep: function () {
this.set('hosts', []);
this.set('headers', []);
this.clearError();
this.set('isLoaded', false);
},
/**
* Enable some service for all hosts
* @param {object} event
* @method selectAllNodes
*/
selectAllNodes: function (event) {
var name = Em.get(event, 'context.name');
if (name) {
this.setAllNodes(name, true);
this.callValidation();
}
},
/**
* Disable some services for all hosts
* @param {object} event
* @method deselectAllNodes
*/
deselectAllNodes: function (event) {
var name = Em.get(event, 'context.name');
if (name) {
this.setAllNodes(name, false);
this.callValidation();
}
},
/**
* Enable/disable some service for all hosts
* @param {String} component - component name
* @param {bool} checked - true - enable, false - disable
* @method setAllNodes
*/
setAllNodes: function (component, checked) {
this.get('hosts').forEach(function (host) {
host.get('checkboxes').filterProperty('isInstalled', false).forEach(function (checkbox) {
if (checkbox.get('component') === component) {
checkbox.set('checked', checked);
}
});
});
this.checkCallback(component);
},
/**
* Checkbox check callback
* Verify if all/none checkboxes for current component are checked
* @param {String} component
* @method checkCallback
*/
checkCallback: function (component) {
var header = this.get('headers').findProperty('name', component);
if (header) {
var hosts = this.get('hosts');
var allTrue = true;
var allFalse = true;
hosts.forEach(function (host) {
host.get('checkboxes').forEach(function (checkbox) {
if (checkbox.get('component') === component && !checkbox.get('isInstalled')) {
allTrue = allTrue && checkbox.get('checked');
allFalse = allFalse && !checkbox.get('checked');
}
});
});
header.set('allChecked', allTrue);
header.set('noChecked', allFalse);
}
this.clearError();
},
/**
* Init step6 data
* @method loadStep
*/
loadStep: function () {
console.log("WizardStep6Controller: Loading step6: Assign Slaves");
this.clearStep();
var selectedServices = App.StackService.find().filterProperty('isSelected');
var installedServices = App.StackService.find().filterProperty('isInstalled');
var services;
if (this.get('isInstallerWizard')) services = selectedServices;
else if (this.get('isAddHostWizard')) services = installedServices;
else if (this.get('isAddServiceWizard')) services = installedServices.concat(selectedServices);
var headers = Em.A([]);
services.forEach(function (stackService) {
stackService.get('serviceComponents').forEach(function (serviceComponent) {
if (serviceComponent.get('isShownOnInstallerSlaveClientPage')) {
headers.pushObject(Em.Object.create({
name: serviceComponent.get('componentName'),
label: App.format.role(serviceComponent.get('componentName')),
allChecked: false,
isRequired: serviceComponent.get('isRequired'),
noChecked: true,
isDisabled: installedServices.someProperty('serviceName', stackService.get('serviceName')) && this.get('isAddServiceWizard')
}));
}
}, this);
}, this);
if (this.get('content.clients') && !!this.get('content.clients').length) {
headers.pushObject(Em.Object.create({
name: 'CLIENT',
label: App.format.role('CLIENT'),
allChecked: false,
noChecked: true,
isDisabled: false
}));
}
this.get('headers').pushObjects(headers);
this.render();
if (this.get('content.skipSlavesStep')) {
App.router.send('next');
} else {
this.callValidation();
}
},
/**
* Get active host names
* @return {string[]}
* @method getHostNames
*/
getHostNames: function () {
var hostInfo = this.get('content.hosts');
var hostNames = [];
//flag identify whether get all hosts or only uninstalled(newly added) hosts
var getUninstalledHosts = (this.get('content.controllerName') !== 'addServiceController');
for (var index in hostInfo) {
if (hostInfo.hasOwnProperty(index)) {
if (hostInfo[index].bootStatus === 'REGISTERED') {
if (!getUninstalledHosts || !hostInfo[index].isInstalled) {
hostNames.push(hostInfo[index].name);
}
}
}
}
return hostNames;
},
/**
* Load all data needed for this module. Then it automatically renders in template
* @method render
*/
render: function () {
var hostsObj = [],
masterHosts = [],
headers = this.get('headers'),
masterHostNames = this.get('content.masterComponentHosts').mapProperty('hostName').uniq();
this.getHostNames().forEach(function (_hostName) {
var hasMaster = masterHostNames.contains(_hostName);
var obj = Em.Object.create({
hostName: _hostName,
hasMaster: hasMaster,
checkboxes: []
});
headers.forEach(function (header) {
obj.checkboxes.pushObject(Em.Object.create({
component: header.name,
title: header.label,
checked: false,
isInstalled: false,
isDisabled: header.get('isDisabled')
}));
});
if (hasMaster) {
masterHosts.pushObject(obj)
} else {
hostsObj.pushObject(obj);
}
});
//hosts with master components should be in the beginning of list
hostsObj.unshift.apply(hostsObj, masterHosts);
hostsObj = this.renderSlaves(hostsObj);
this.set('hosts', hostsObj);
headers.forEach(function (header) {
this.checkCallback(header.get('name'));
}, this);
this.set('isLoaded', true);
},
/**
* Set checked values for slaves checkboxes
* @param {Array} hostsObj
* @return {Array}
* @method renderSlaves
*/
renderSlaves: function (hostsObj) {
var headers = this.get('headers');
var clientHeaders = headers.findProperty('name', 'CLIENT');
var slaveComponents = this.get('content.slaveComponentHosts');
if (!slaveComponents) { // we are at this page for the first time
var recommendations = this.get('content.recommendations');
// Get all host-component pairs from recommendations
var componentHostPairs = recommendations.blueprint.host_groups.map(function (group) {
return group.components.map(function (component) {
return recommendations.blueprint_cluster_binding.host_groups.findProperty('name', group.name).hosts.map(function (host) {
return { component: component.name, host: host.fqdn};
});
});
});
// Flatten results twice because of two map() call before
componentHostPairs = [].concat.apply([], componentHostPairs);
componentHostPairs = [].concat.apply([], componentHostPairs);
var clientComponents = App.get('components.clients');
hostsObj.forEach(function (host) {
var checkboxes = host.get('checkboxes');
checkboxes.forEach(function (checkbox) {
var recommended = componentHostPairs.some(function (pair) {
var componentMatch = pair.component === checkbox.component;
if (checkbox.component === 'CLIENT' && !componentMatch) {
componentMatch = clientComponents.contains(pair.component);
}
return pair.host === host.hostName && componentMatch;
});
checkbox.checked = recommended;
});
});
} else {
this.get('headers').forEach(function (header) {
var nodes = slaveComponents.findProperty('componentName', header.get('name'));
if (nodes) {
nodes.hosts.forEach(function (_node) {
var node = hostsObj.findProperty('hostName', _node.hostName);
if (node) {
node.get('checkboxes').findProperty('title', header.get('label')).set('checked', true);
node.get('checkboxes').findProperty('title', header.get('label')).set('isInstalled', _node.isInstalled);
}
});
}
});
}
this.selectClientHost(hostsObj);
return hostsObj;
},
/**
*
* @param hostsObj
*/
selectClientHost: function (hostsObj) {
if (!this.get('isClientsSet')) {
var nonMasterHost = hostsObj.findProperty('hasMaster', false);
var clientHost = !!nonMasterHost ? nonMasterHost : hostsObj[hostsObj.length - 1]; // last host
var clientCheckBox = clientHost.get('checkboxes').findProperty('component', 'CLIENT');
if (clientCheckBox) {
clientCheckBox.set('checked', true);
}
this.set('isClientsSet', true);
}
},
/**
* Select checkboxes which correspond to master components
*
* @param {Array} hostsObj
* @return {Array}
* @method selectMasterComponents
*/
selectMasterComponents: function (hostsObj) {
var masterComponentHosts = this.get('content.masterComponentHosts');
console.log('Master components selected on:', masterComponentHosts.mapProperty('hostName').uniq().join(", "));
if (masterComponentHosts) {
masterComponentHosts.forEach(function (item) {
var host = hostsObj.findProperty('hostName', item.hostName);
if (host) {
var checkbox = host.get('checkboxes').findProperty('component', item.component);
if (checkbox) {
checkbox.set('checked', true);
}
}
});
}
return hostsObj;
},
/**
* Return list of master components for specified hostname
* @param {string} hostName
* @return {string[]}
* @method getMasterComponentsForHost
*/
getMasterComponentsForHost: function (hostName) {
return this.get('content.masterComponentHosts').filterProperty('hostName', hostName).mapProperty('component');
},
callValidation: function (successCallback) {
this.callServerSideValidation(successCallback);
},
/**
* Update submit button status
* @metohd callServerSideValidation
*/
callServerSideValidation: function (successCallback) {
var self = this;
// We do not want to disable Next due to server validation issues - hence commented out line below
// self.set('submitDisabled', true);
var selectedServices = App.StackService.find().filterProperty('isSelected').mapProperty('serviceName');
var installedServices = App.StackService.find().filterProperty('isInstalled').mapProperty('serviceName');
var services = installedServices.concat(selectedServices).uniq();
var hostNames = self.get('hosts').mapProperty('hostName');
var slaveBlueprint = self.getCurrentBlueprint();
var masterBlueprint = null;
var invisibleSlaves = App.StackServiceComponent.find().filterProperty("isSlave").filterProperty("isShownOnInstallerSlaveClientPage", false).mapProperty("componentName");
if (this.get('isInstallerWizard') || this.get('isAddServiceWizard')) {
masterBlueprint = self.getCurrentMastersBlueprint();
var invisibleMasters = [];
if (this.get('isInstallerWizard')) {
invisibleMasters = App.StackServiceComponent.find().filterProperty("isMaster").filterProperty("isShownOnInstallerAssignMasterPage", false).mapProperty("componentName");
} else if (this.get('isAddServiceWizard')) {
invisibleMasters = App.StackServiceComponent.find().filterProperty("isMaster").filterProperty("isShownOnAddServiceAssignMasterPage", false).mapProperty("componentName");
}
var selectedClientComponents = self.get('content.clients').mapProperty('component_name');
var alreadyInstalledClients = App.get('components.clients').reject(function (c) {
return selectedClientComponents.contains(c);
});
var invisibleComponents = invisibleMasters.concat(invisibleSlaves).concat(alreadyInstalledClients);
var invisibleBlueprint = blueprintUtils.filterByComponents(this.get('content.recommendations'), invisibleComponents);
masterBlueprint = blueprintUtils.mergeBlueprints(masterBlueprint, invisibleBlueprint);
} else if (this.get('isAddHostWizard')) {
masterBlueprint = self.getCurrentMasterSlaveBlueprint();
hostNames = hostNames.concat(App.Host.find().mapProperty("hostName")).uniq();
slaveBlueprint = blueprintUtils.addComponentsToBlueprint(slaveBlueprint, invisibleSlaves);
}
var bluePrintsForValidation = blueprintUtils.mergeBlueprints(masterBlueprint, slaveBlueprint);
this.set('content.recommendationsHostGroups', bluePrintsForValidation);
App.ajax.send({
name: 'config.validations',
sender: self,
data: {
stackVersionUrl: App.get('stackVersionURL'),
hosts: hostNames,
services: services,
validate: 'host_groups',
recommendations: bluePrintsForValidation
},
success: 'updateValidationsSuccessCallback',
error: 'updateValidationsErrorCallback'
}).
then(function () {
if (!self.get('submitDisabled') && successCallback) {
successCallback();
}
}
);
},
/**
* Success-callback for validations request
* @param {object} data
* @method updateValidationsSuccessCallback
*/
updateValidationsSuccessCallback: function (data) {
var self = this;
//data = JSON.parse(data); // temporary fix
var clientComponents = App.get('components.clients');
this.set('generalErrorMessages', []);
this.set('generalWarningMessages', []);
this.get('hosts').setEach('warnMessages', []);
this.get('hosts').setEach('errorMessages', []);
this.get('hosts').setEach('anyMessage', false);
this.get('hosts').forEach(function (host) {
host.checkboxes.setEach('hasWarnMessage', false);
host.checkboxes.setEach('hasErrorMessage', false);
});
var anyErrors = false;
var anyGeneralClientErrors = false; // any error/warning for any client component (under "CLIENT" alias)
var validationData = validationUtils.filterNotInstalledComponents(data);
validationData.filterProperty('type', 'host-component').filter(function (i) {
return !(i['component-name'] && App.StackServiceComponent.find().findProperty('componentName', i['component-name']).get('isMaster'));
}).forEach(function (item) {
var checkboxWithIssue = null;
var isGeneralClientValidationItem = clientComponents.contains(item['component-name']); // it is an error/warning for any client component (under "CLIENT" alias)
var host = self.get('hosts').find(function (h) {
return h.hostName === item.host && h.checkboxes.some(function (checkbox) {
var isClientComponent = checkbox.component === "CLIENT" && isGeneralClientValidationItem;
if (checkbox.component === item['component-name'] || isClientComponent) {
checkboxWithIssue = checkbox;
return true;
} else {
return false;
}
});
});
if (host) {
host.set('anyMessage', true);
if (item.level === 'ERROR') {
anyErrors = true;
host.get('errorMessages').push(item.message);
checkboxWithIssue.set('hasErrorMessage', true);
} else if (item.level === 'WARN') {
host.get('warnMessages').push(item.message);
checkboxWithIssue.set('hasWarnMessage', true);
}
} else {
var component;
if (isGeneralClientValidationItem) {
if (!anyGeneralClientErrors) {
anyGeneralClientErrors = true;
component = "Client";
}
} else {
component = item['component-name'];
}
if (component || !item['component-name']) {
var details = "";
if (item.host) {
details += " (" + item.host + ")";
}
if (item.level === 'ERROR') {
anyErrors = true;
self.get('generalErrorMessages').push(item.message + details);
} else if (item.level === 'WARN') {
self.get('generalWarningMessages').push(item.message + details);
}
}
}
});
// use this.set('submitDisabled', anyErrors); is validation results should block next button
// It's because showValidationIssuesAcceptBox allow use accept validation issues and continue
// this.set('submitDisabled', false);
},
/**
* Error-callback for validations request
* @param {object} jqXHR
* @param {object} ajaxOptions
* @param {string} error
* @param {object} opt
* @method updateValidationsErrorCallback
*/
updateValidationsErrorCallback: function (jqXHR, ajaxOptions, error, opt) {
App.ajax.defaultErrorHandler(jqXHR, opt.url, opt.method, jqXHR.status);
console.log('Load validations failed');
},
/**
* Composes selected values of comboboxes into blueprint format
*/
getCurrentBlueprint: function () {
var self = this;
var res = {
blueprint: { host_groups: [] },
blueprint_cluster_binding: { host_groups: [] }
};
var clientComponents = self.get('content.clients').mapProperty('component_name');
var mapping = self.get('hosts');
mapping.forEach(function (item, i) {
var group_name = 'host-group-' + (i+1);
var host_group = {
name: group_name,
components: item.checkboxes.filterProperty('checked', true).map(function (checkbox) {
if (checkbox.component === "CLIENT") {
return clientComponents.map(function (client) {
return { name: client };
});
} else {
return { name: checkbox.component };
}
})
};
host_group.components = [].concat.apply([], host_group.components);
var binding = {
name: group_name,
hosts: [
{ fqdn: item.hostName }
]
};
res.blueprint.host_groups.push(host_group);
res.blueprint_cluster_binding.host_groups.push(binding);
});
return res;
},
/**
* Create blueprint from assigned master components to appropriate hosts
* @returns {Object}
* @method getCurrentMastersBlueprint
*/
getCurrentMastersBlueprint: function () {
var res = {
blueprint: { host_groups: [] },
blueprint_cluster_binding: { host_groups: [] }
};
var masters = this.get('content.masterComponentHosts');
var hosts = this.get('content.hosts');
Em.keys(hosts).forEach(function (host, i) {
var group_name = 'host-group-' + (i + 1);
var components = [];
masters.forEach(function (master) {
if (master.hostName === host) {
components.push({
name: master.component
});
}
});
res.blueprint.host_groups.push({
name: group_name,
components: components
});
res.blueprint_cluster_binding.host_groups.push({
name: group_name,
hosts: [
{
fqdn: host
}
]
});
}, this);
return blueprintUtils.mergeBlueprints(res, this.getCurrentSlaveBlueprint());
},
/**
* In case of any validation issues shows accept dialog box for user which allow cancel and fix issues or continue anyway
* @metohd submit
*/
showValidationIssuesAcceptBox: function(callback) {
var self = this;
if (self.get('anyWarnings') || self.get('anyErrors')) {
App.ModalPopup.show({
primary: Em.I18n.t('common.continueAnyway'),
header: Em.I18n.t('installer.step6.validationIssuesAttention.header'),
body: Em.I18n.t('installer.step6.validationIssuesAttention'),
onPrimary: function () {
this.hide();
callback();
}
});
} else {
callback();
}
}
});