blob: 467cf81dd9841248ecc1acf8b32f341b27cbecbe [file] [log] [blame]
/**
* 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 blueprintUtils = require('utils/blueprint');
var numberUtils = require('utils/number_utils');
var validationUtils = require('utils/validator');
/**
* Mixin for assign master-to-host step in wizards
* Implements basic logic of assign masters page
* Should be used with controller linked with App.AssignMasterComponentsView
* @type {Ember.Mixin}
*/
App.AssignMasterComponents = Em.Mixin.create({
/**
* Array of master component names to show on the page
* By default is empty, this means that masters of all selected services should be shown
* @type {Array}
*/
mastersToShow: [],
/**
* Array of master component names to show on the service config assign master page
* @type {Array}
*/
mastersToCreate: [],
/**
* Array of master component names to add for install
* @type {Array}
*/
mastersToAdd: [],
/**
* Array of master component names, that are already installed, but should have ability to change host
* @type {Array}
*/
mastersToMove: [],
/**
* Array of master component names, that should be addable
* Are used in HA wizards to add components, that are not addable for other wizards
* @type {Array}
*/
mastersAddableInHA: [],
/**
* Array of master component names to show 'Current' prefix in label before component name
* Prefix will be shown only for installed instances
* @type {Array}
*/
showCurrentPrefix: [],
/**
* Array of master component names to show 'Additional' prefix in label before component name
* Prefix will be shown only for not installed instances
* @type {Array}
*/
showAdditionalPrefix: [],
/**
* Array of objects with label and host keys to show specific hosts on the page
* @type {Array}
* format:
* [
* {
* label: 'Current',
* host: 'c6401.ambari.apache.org'
* },
* {
* label: 'Additional',
* host: function () {
* return 'c6402.ambari.apache.org';
* }.property()
* }
* ]
*/
additionalHostsList: [],
/**
* Define whether show already installed masters first
* @type {Boolean}
*/
showInstalledMastersFirst: false,
/**
* Map of component name to list of hostnames for that component
* format:
* {
* NAMENODE: [
* 'c6401.ambari.apache.org'
* ],
* DATANODE: [
* 'c6402.ambari.apache.org',
* 'c6403.ambari.apache.org',
* ]
* }
* @type {Object}
*/
recommendedHostsForComponents: {},
recommendations: null,
markSavedComponentsAsInstalled: false,
/**
* @type {boolean}
* @default false
*/
validationInProgress: false,
/**
* run validation call which was skipped
* validation should be always ran after last change
* @type {boolean}
* @default false
*/
runQueuedValidation: false,
/**
* Array of <code>servicesMasters</code> objects, that will be shown on the page
* Are filtered using <code>mastersToShow</code>
* @type {Array}
*/
servicesMastersToShow: function () {
var mastersToShow = this.get('mastersToShow');
var servicesMasters = this.get('servicesMasters');
var result = [];
if (!mastersToShow.length) {
result = servicesMasters;
} else {
mastersToShow.forEach(function (master) {
result = result.concat(servicesMasters.filterProperty('component_name', master));
});
}
if (this.get('showInstalledMastersFirst')) {
result = this.sortMasterComponents(result);
}
return result;
}.property('servicesMasters.length', 'mastersToShow.length', 'showInstalledMastersFirst'),
/**
* Sort masters, installed masters will be first.
* @param masters
* @returns {Array}
*/
sortMasterComponents: function (masters) {
return [].concat(masters.filterProperty('isInstalled'), masters.filterProperty('isInstalled', false));
},
/**
* Check if <code>installerWizard</code> used
* @type {bool}
*/
isInstallerWizard: Em.computed.equal('content.controllerName', 'installerController'),
/**
* Master components which could be assigned to multiple hosts
* @type {string[]}
*/
multipleComponents: Em.computed.alias('App.components.multipleMasters'),
/**
* Master components which could be assigned to multiple hosts
* @type {string[]}
*/
addableComponents: function () {
return App.get('components.addableMasterInstallerWizard').concat(this.get('mastersAddableInHA')).uniq();
}.property('App.components.addableMasterInstallerWizard', 'mastersAddableInHA'),
/**
* Define state for submit button
* @type {bool}
*/
submitDisabled: false,
/**
* Is Submit-click processing now
* @type {bool}
*/
submitButtonClicked: false,
/**
* Either use or not use server validation in this controller
* @type {bool}
*/
useServerValidation: true,
/**
* Trigger for executing host names check for components
* Should de "triggered" when host changed for some component and when new multiple component is added/removed
* @type {bool}
*/
hostNameCheckTrigger: false,
/**
* List of hosts
* @type {Array}
*/
hosts: [],
/**
* Name of multiple component which host name was changed last
* @type {Object|null}
*/
componentToRebalance: null,
/**
* Name of component which host was changed last
* @type {string}
*/
lastChangedComponent: null,
/**
* Flag for rebalance multiple components
* @type {number}
*/
rebalanceComponentHostsCounter: 0,
/**
* @type {Ember.Enumerable}
*/
servicesMasters: [],
/**
* @type {Ember.Enumerable}
*/
selectedServicesMasters: [],
/**
* Is hosts data loaded
* @type {bool}
*/
isHostsLoaded: false,
/**
* Are recommendations loaded
* @type {bool}
*/
isRecommendationsLoaded: false,
/**
* Is data for current step loaded
* @type {bool}
*/
isLoaded: Em.computed.and('isHostsLoaded', 'isRecommendationsLoaded'),
/**
* Validation error messages which don't related with any master
*/
generalErrorMessages: [],
/**
* Validation warning messages which don't related with any master
*/
generalWarningMessages: [],
/**
* Is masters-hosts layout initial one
* @type {bool}
*/
isInitialLayout: true,
/**
* Is back from the next step
* @type {bool}
*/
backFromNextStep: false,
/**
* true if any error exists
*/
anyError: function() {
return this.get('servicesMasters').some(function(m) { return m.get('errorMessage'); }) || this.get('generalErrorMessages').some(function(m) { return m; });
}.property('servicesMasters.@each.errorMessage', 'generalErrorMessages'),
/**
* true if any warning exists
*/
anyWarning: function() {
return this.get('servicesMasters').some(function(m) { return m.get('warnMessage'); }) || this.get('generalWarningMessages').some(function(m) { return m; });
}.property('servicesMasters.@each.warnMessage', 'generalWarningMessages'),
/**
* Clear loaded recommendations
*/
clearRecommendations: function() {
if (this.get('content.recommendations')) {
this.set('content.recommendations', null);
}
if (this.get('recommendations')) {
this.set('recommendations', null);
}
},
/**
* List of host with assigned masters
* Format:
* <code>
* [
* {
* host_name: '',
* hostInfo: {},
* masterServices: [],
* masterServicesToDisplay: [] // used only in template
* },
* ....
* ]
* </code>
* @type {Ember.Enumerable}
*/
masterHostMapping: function () {
var mapping = [], mappingObject, mappedHosts, hostObj;
//get the unique assigned hosts and find the master services assigned to them
mappedHosts = this.get("selectedServicesMasters").mapProperty("selectedHost").uniq();
mappedHosts.forEach(function (item) {
hostObj = this.get("hosts").findProperty("host_name", item);
// User may input invalid host name (this is handled in hostname checker). Here we just skip it
if (!hostObj) return;
var masterServices = this.get("selectedServicesMasters").filterProperty("selectedHost", item),
masterServicesToDisplay = [];
masterServices.mapProperty('display_name').uniq().forEach(function (n) {
masterServicesToDisplay.pushObject(masterServices.findProperty('display_name', n));
});
mappingObject = Em.Object.create({
host_name: item,
hostInfo: hostObj.host_info,
masterServices: masterServices,
masterServicesToDisplay: masterServicesToDisplay
});
mapping.pushObject(mappingObject);
}, this);
return mapping.sortProperty('host_name');
}.property('selectedServicesMasters.@each.selectedHost', 'selectedServicesMasters.@each.isHostNameValid', 'isLoaded'),
/**
* Count of hosts without masters
* @type {number}
*/
remainingHosts: function () {
if (this.get('content.controllerName') === 'installerController') {
return 0;
} else {
return (this.get("hosts.length") - this.get("masterHostMapping.length"));
}
}.property('masterHostMapping.length', 'selectedServicesMasters.@each.selectedHost'),
/**
* Update submit button status
* @method updateIsSubmitDisabled
*/
updateIsSubmitDisabled: function () {
if (this.thereIsNoMasters()) {
return false;
}
var isSubmitDisabled = this.get('servicesMasters').someProperty('isHostNameValid', false);
if (this.get('useServerValidation')) {
this.set('submitDisabled', true);
if (this.get('servicesMasters').length === 0) {
return;
}
if (!isSubmitDisabled) {
if (!this.get('isInitialLayout')) {
this.clearRecommendations(); // reset previous recommendations
} else {
this.set('isInitialLayout', false);
}
this.recommendAndValidate();
}
} else {
isSubmitDisabled = isSubmitDisabled || !this.customClientSideValidation();
this.set('submitDisabled', isSubmitDisabled);
return isSubmitDisabled;
}
}.observes('servicesMasters.@each.selectedHost'),
/**
* Function to validate master-to-host assignments
* Should be defined in controller
* @returns {boolean}
*/
customClientSideValidation: function () {
return true;
},
/**
* Send AJAX request to validate current host layout
* @param blueprint - blueprint for validation (can be with/withour slave/client components)
*/
validate: function(blueprint, callback) {
var self = this;
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('host_name');
App.ajax.send({
name: 'config.validations',
sender: self,
data: {
stackVersionUrl: App.get('stackVersionURL'),
hosts: hostNames,
services: services,
validate: 'host_groups',
recommendations: blueprint
},
success: 'updateValidationsSuccessCallback',
error: 'updateValidationsErrorCallback'
}).then(function() {
if (callback) {
callback();
}
}
);
},
/**
* Success-callback for validations request
* @param {object} data
* @method updateValidationsSuccessCallback
*/
updateValidationsSuccessCallback: function (data) {
var self = this;
var generalErrorMessages = [];
var generalWarningMessages = [];
this.get('servicesMasters').setEach('warnMessage', null);
this.get('servicesMasters').setEach('errorMessage', null);
var anyErrors = false;
var validationData = validationUtils.filterNotInstalledComponents(data);
validationData.filterProperty('type', 'host-component').forEach(function(item) {
var master = self.get('servicesMasters').find(function(m) {
return m.component_name === item['component-name'] && m.selectedHost === item.host;
});
if (master) {
if (item.level === 'ERROR') {
anyErrors = true;
master.set('errorMessage', item.message);
} else if (item.level === 'WARN') {
master.set('warnMessage', item.message);
}
}
});
this.set('generalErrorMessages', generalErrorMessages);
this.set('generalWarningMessages', generalWarningMessages);
// 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); //this.set('submitDisabled', anyErrors);
},
/**
* 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) {
},
/**
* Composes selected values of comboboxes into master blueprint + merge it with currently installed slave blueprint
*/
getCurrentBlueprint: function() {
var self = this;
var res = {
blueprint: { host_groups: [] },
blueprint_cluster_binding: { host_groups: [] }
};
var mapping = self.get('masterHostMapping');
mapping.forEach(function(item, i) {
var group_name = 'host-group-' + (i+1);
var host_group = {
name: group_name,
components: item.masterServices.map(function(master) {
return { name: master.component_name };
})
};
var binding = {
name: group_name,
hosts: [ { fqdn: item.host_name } ]
};
res.blueprint.host_groups.push(host_group);
res.blueprint_cluster_binding.host_groups.push(binding);
});
return blueprintUtils.mergeBlueprints(res, self.getCurrentSlaveBlueprint());
},
/**
* Clear controller data (hosts, masters etc)
* @method clearStep
*/
clearStep: function () {
this.setProperties({
hosts: [],
isHostsLoaded: false,
isRecommendationsLoaded: false,
backFromNextStep: false,
selectedServicesMasters: [],
servicesMasters: []
});
App.StackServiceComponent.find().forEach(function (stackComponent) {
stackComponent.set('serviceComponentId', 1);
}, this);
},
clearStepOnExit: function () {
this.clearStep();
},
/**
* Load controller data (hosts, host components etc)
* @method loadStep
*/
loadStep: function () {
var self = this;
this.clearStep();
if (this._additionalClearSteps) {
this._additionalClearSteps();
}
this.renderHostInfo().done(function () {
//when returning from step Assign Slaves and Clients, recommendations are already available
//set the flag so that recommendations AJAX call is not made unnecessarily
if (self.get('recommendations')) {
self.set('backFromNextStep', true);
}
self.loadComponentsRecommendationsFromServer(self.loadStepCallback);
});
},
/**
* Callback after load controller data (hosts, host components etc)
* @method loadStepCallback
*/
loadStepCallback: function(components, self) {
self.renderComponents(components);
self.get('addableComponents').forEach(function (componentName) {
self.updateComponent(componentName);
}, self);
self.set('isRecommendationsLoaded', true);
if (self.thereIsNoMasters() && !self.get('mastersToCreate').length) {
App.router.send('next');
}
},
/**
* Returns true if there is no new master components which need assigment to host
*/
thereIsNoMasters: function() {
return !this.get("selectedServicesMasters").filterProperty('isInstalled', false).length;
},
/**
* Used to set showAddControl flag for installer wizard
* @method updateComponent
*/
updateComponent: function (componentName) {
var component = this.last(componentName);
if (!component) {
return;
}
var showControl = !App.StackServiceComponent.find().findProperty('componentName', componentName).get('stackService').get('isInstalled')
|| this.get('mastersAddableInHA').contains(componentName);
if (showControl) {
var mastersLength = this.get("selectedServicesMasters").filterProperty("component_name", componentName).length;
if (mastersLength < this.getMaxNumberOfMasters(componentName)) {
component.set('showAddControl', true);
} else {
component.set('showRemoveControl', mastersLength != 1);
}
}
},
/**
* Count max number of instances for masters <code>componentName</code>, according to their cardinality and number of hosts
* @param componentName
* @returns {Number}
*/
getMaxNumberOfMasters: function (componentName) {
var maxByCardinality = App.StackServiceComponent.find().findProperty('componentName', componentName).get('maxToInstall');
var hostsNumber = this.get("hosts.length");
return Math.min(maxByCardinality, hostsNumber);
},
/**
* Load active host list to <code>hosts</code> variable
* @method renderHostInfo
*/
renderHostInfo: function () {
var self = this;
var isInstaller = (this.get('wizardController.name') === 'installerController' || this.get('content.controllerName') === 'installerController');
return App.ajax.send({
name: isInstaller ? 'hosts.info.install' : 'hosts.high_availability.wizard',
sender: this,
data: {
hostNames: isInstaller ? this.getHosts().join() : null
}
}).success(function(data) {
self.loadWizardHostsSuccessCallback(data)
});
},
loadWizardHostsSuccessCallback: function (data) {
var hostInfo = this.get('content.hosts'),
result = [];
data.items.forEach(function (host) {
var hostName = host.Hosts.host_name,
_host = hostInfo[hostName],
cpu = host.Hosts.cpu_count,
memory = host.Hosts.total_mem.toFixed(2);
if (_host.bootStatus === 'REGISTERED') {
result.push(Em.Object.create({
host_name: hostName,
cpu: cpu,
memory: memory,
disk_info: host.Hosts.disk_info,
maintenance_state: host.Hosts.maintenance_state,
isInstalled: _host.isInstalled,
host_info: Em.I18n.t('installer.step5.hostInfo').fmt(hostName, numberUtils.bytesToSize(memory, 1, 'parseFloat', 1024), cpu)
}));
}
}, this);
this.set('hosts', result);
this.sortHosts(this.get('hosts'));
this.set('isHostsLoaded', true);
},
/**
* Sort list of host-objects by properties (memory - desc, cpu - desc, hostname - asc)
* @param {object[]} hosts
*/
sortHosts: function (hosts) {
hosts.sort(function (a, b) {
if (a.get('memory') == b.get('memory')) {
if (a.get('cpu') == b.get('cpu')) {
return a.get('host_name').localeCompare(b.get('host_name')); // hostname asc
}
return b.get('cpu') - a.get('cpu'); // cores desc
}
return b.get('memory') - a.get('memory'); // ram desc
});
},
/**
* Get recommendations info from API
* @param {function}callback
* @param {boolean} includeMasters
* @method loadComponentsRecommendationsFromServer
*/
loadComponentsRecommendationsFromServer: function(callback, includeMasters) {
var self = this;
//when returning from step Assign Slaves and Clients, backFromNextStep will be true
if (this.get('recommendations') && this.get('backFromNextStep')) {
// Don't do AJAX call if recommendations has been already received
// But if user returns to previous step (selecting services), stored recommendations will be cleared in routers' next handler and AJAX call will be made again
callback(self.createComponentInstallationObjects(), self);
}
else {
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.getHosts();
var data = {
stackVersionUrl: App.get('stackVersionURL'),
hosts: hostNames,
services: services,
recommend: 'host_groups'
};
if (includeMasters) {
// Made partial recommendation request for reflect in blueprint host-layout changes which were made by user in UI
data.recommendations = self.getCurrentBlueprint();
}
else
if (!self.get('isInstallerWizard')) {
data.recommendations = self.getCurrentMasterSlaveBlueprint();
}
return App.ajax.send({
name: 'wizard.loadrecommendations',
sender: self,
data: data,
success: 'loadRecommendationsSuccessCallback',
error: 'loadRecommendationsErrorCallback'
}).then(function () {
callback(self.createComponentInstallationObjects(), self);
});
}
},
/**
* Create components for displaying component-host comboboxes in UI assign dialog
* expects content.recommendations will be filled with recommendations API call result
* @return {Object[]}
*/
createComponentInstallationObjects: function() {
var stackMasterComponentsMap = {},
masterHosts = this.get('content.masterComponentHosts') || this.get('masterComponentHosts'), //saved to local storage info
servicesToAdd = (this.get('content.services')|| []).filterProperty('isSelected').filterProperty('isInstalled', false).mapProperty('serviceName'),
recommendations = this.get('recommendations'),
resultComponents = [],
multipleComponentHasBeenAdded = {},
hostGroupsMap = {};
App.StackServiceComponent.find().forEach(function(component) {
var isMasterCreateOnConfig = this.get('mastersToCreate').contains(component.get('componentName'));
if (this.get('isInstallerWizard') && (component.get('isShownOnInstallerAssignMasterPage') || isMasterCreateOnConfig) ) {
stackMasterComponentsMap[component.get('componentName')] = component;
} else if (component.get('isShownOnAddServiceAssignMasterPage') || this.get('mastersToShow').contains(component.get('componentName')) || isMasterCreateOnConfig) {
stackMasterComponentsMap[component.get('componentName')] = component;
}
}, this);
recommendations.blueprint_cluster_binding.host_groups.forEach(function(group) {
hostGroupsMap[group.name] = group;
});
recommendations.blueprint.host_groups.forEach(function(host_group) {
var hosts = hostGroupsMap[host_group.name] ? hostGroupsMap[host_group.name].hosts : [];
hosts.forEach(function(host) {
host_group.components.forEach(function(component) {
var willBeDisplayed = true;
var stackMasterComponent = stackMasterComponentsMap[component.name];
if (stackMasterComponent) {
var isMasterCreateOnConfig = this.get('mastersToCreate').contains(component.name);
// If service is already installed and not being added as a new service then render on UI only those master components
// that have already installed hostComponents.
// NOTE: On upgrade there might be a prior installed service with non-installed newly introduced serviceComponent
if (!servicesToAdd.contains(stackMasterComponent.get('serviceName')) && !isMasterCreateOnConfig) {
willBeDisplayed = masterHosts.someProperty('component', component.name);
}
if (willBeDisplayed) {
var savedComponents = masterHosts.filterProperty('component', component.name);
if (this.get('multipleComponents').contains(component.name) && savedComponents.length > 0) {
if (!multipleComponentHasBeenAdded[component.name]) {
multipleComponentHasBeenAdded[component.name] = true;
savedComponents.forEach(function(saved) {
resultComponents.push(this.createComponentInstallationObject(stackMasterComponent, host.fqdn.toLowerCase(), saved));
}, this);
}
} else {
var savedComponent = masterHosts.findProperty('component', component.name);
resultComponents.push(this.createComponentInstallationObject(stackMasterComponent, host.fqdn.toLowerCase(), savedComponent));
}
}
}
}, this);
}, this);
}, this);
return resultComponents;
},
/**
* Create component for displaying component-host comboboxes in UI assign dialog
* @param fullComponent - full component description
* @param hostName - host fqdn where component will be installed
* @param savedComponent - the same object which function returns but created before
* @return {Object}
*/
createComponentInstallationObject: function(fullComponent, hostName, savedComponent) {
var componentName = fullComponent.get('componentName');
var componentObj = {};
componentObj.component_name = componentName;
componentObj.display_name = App.format.role(fullComponent.get('componentName'), false);
componentObj.serviceId = fullComponent.get('serviceName');
componentObj.isServiceCoHost = App.StackServiceComponent.find().findProperty('componentName', componentName).get('isCoHostedComponent') && !this.get('mastersToMove').contains(componentName);
componentObj.selectedHost = savedComponent ? savedComponent.hostName : hostName;
componentObj.isInstalled = savedComponent ? savedComponent.isInstalled || (this.get('markSavedComponentsAsInstalled') && !this.get('mastersToCreate').contains(fullComponent.get('componentName'))) : false;
return componentObj;
},
/**
* Success-callback for recommendations request
* @param {object} data
* @method loadRecommendationsSuccessCallback
*/
loadRecommendationsSuccessCallback: function (data) {
var recommendations = data.resources[0].recommendations;
this.set('recommendations', recommendations);
if (this.get('content.controllerName')) {
this.set('content.recommendations', recommendations);
}
var recommendedHostsForComponent = {};
var hostsForHostGroup = {};
recommendations.blueprint_cluster_binding.host_groups.forEach(function(hostGroup) {
hostsForHostGroup[hostGroup.name] = hostGroup.hosts.mapProperty('fqdn');
});
recommendations.blueprint.host_groups.forEach(function (hostGroup) {
var components = hostGroup.components.mapProperty('name');
components.forEach(function (componentName) {
var hostList = recommendedHostsForComponent[componentName] || [];
var hostNames = hostsForHostGroup[hostGroup.name] || [];
hostList.pushObjects(hostNames);
recommendedHostsForComponent[componentName] = hostList;
});
});
this.set('recommendedHostsForComponents', recommendedHostsForComponent);
if (this.get('content.controllerName')) {
this.set('content.recommendedHostsForComponents', recommendedHostsForComponent);
}
},
/**
* Error-callback for recommendations request
* @param {object} jqXHR
* @param {object} ajaxOptions
* @param {string} error
* @param {object} opt
* @method loadRecommendationsErrorCallback
*/
loadRecommendationsErrorCallback: function (jqXHR, ajaxOptions, error, opt) {
App.ajax.defaultErrorHandler(jqXHR, opt.url, opt.type, jqXHR.status);
},
/**
* Put master components to <code>selectedServicesMasters</code>, which will be automatically rendered in template
* @param {Ember.Enumerable} masterComponents
* @method renderComponents
*/
renderComponents: function (masterComponents) {
var installedServices = App.StackService.find().filterProperty('isSelected').filterProperty('isInstalled', false).mapProperty('serviceName'); //list of shown services
var result = [];
var serviceComponentId, previousComponentName;
this.addNewMasters(masterComponents);
masterComponents.forEach(function (item) {
var masterComponent = App.StackServiceComponent.find().findProperty('componentName', item.component_name);
var componentObj = Em.Object.create(item);
var showRemoveControl;
if (masterComponent.get('isMasterWithMultipleInstances')) {
showRemoveControl = installedServices.contains(masterComponent.get('stackService.serviceName')) &&
(masterComponents.filterProperty('component_name', item.component_name).length > 1);
previousComponentName = item.component_name;
componentObj.set('serviceComponentId', result.filterProperty('component_name', item.component_name).length + 1);
componentObj.set("showRemoveControl", showRemoveControl);
}
componentObj.set('isHostNameValid', true);
componentObj.set('showCurrentPrefix', this.get('showCurrentPrefix').contains(item.component_name) && item.isInstalled);
componentObj.set('showAdditionalPrefix', this.get('showAdditionalPrefix').contains(item.component_name) && !item.isInstalled);
if (this.get('mastersToMove').contains(item.component_name)) {
componentObj.set('isInstalled', false);
}
result.push(componentObj);
}, this);
result = this.sortComponentsByServiceName(result);
this.set("selectedServicesMasters", result);
this.set('servicesMasters', result);
},
/**
* Add new master components from <code>mastersToAdd</code> list
* @param masterComponents
* @returns {masterComponents[]}
*/
addNewMasters: function (masterComponents) {
this.get('mastersToAdd').forEach(function (masterName, index, mastersToAdd) {
var toBeAddedNumber = mastersToAdd.filter(function (name) {
return name === masterName;
}).length,
alreadyAddedNumber = masterComponents.filterProperty('component_name', masterName).rejectProperty('isInstalled').length;
if (toBeAddedNumber > alreadyAddedNumber) {
var hostName = this.getHostForMaster(masterName, masterComponents),
serviceName = this.getServiceByMaster(masterName);
masterComponents.push(this.createComponentInstallationObject(
Em.Object.create({
componentName: masterName,
serviceName: serviceName
}),
hostName
));
}
}, this);
return masterComponents;
},
/**
* Find available host for master and return it
* If there is no available hosts returns false
* @param master
* @param allMasters
* @returns {*}
*/
getHostForMaster: function (master, allMasters) {
var masterHostList = [];
allMasters.forEach(function (component) {
if (component.component_name === master) {
masterHostList.push(component.selectedHost);
}
});
var recommendedHostsForMaster = this.get('recommendedHostsForComponents')[master] || [];
for (var k = 0; k < recommendedHostsForMaster.length; k++) {
if(!masterHostList.contains(recommendedHostsForMaster[k])) {
return recommendedHostsForMaster[k];
}
}
var usedHosts = allMasters.filterProperty('component_name', master).mapProperty('selectedHost');
var allHosts = this.get('hosts');
for (var i = 0; i < allHosts.length; i++) {
if (!usedHosts.contains(allHosts[i].get('host_name'))) {
return allHosts[i].get('host_name');
}
}
return false;
},
/**
* Find serviceName for master by it's componentName
* @param master
* @returns {*}
*/
getServiceByMaster: function (master) {
return App.StackServiceComponent.find().findProperty('componentName', master).get('serviceName');
},
/**
* Sort components by their service (using <code>App.StackService.displayOrder</code>)
* Services not in App.StackService.displayOrder are moved to the end of the list
*
* @param components
* @returns {*}
*/
sortComponentsByServiceName: function(components) {
var displayOrder = App.StackService.displayOrder;
var componentsOrderForService = App.StackService.componentsOrderForService;
var indexForUnordered = Math.max(displayOrder.length, components.length);
return components.sort(function (a, b) {
if(a.serviceId === b.serviceId && a.serviceId in componentsOrderForService)
return componentsOrderForService[a.serviceId].indexOf(a.component_name) - componentsOrderForService[b.serviceId].indexOf(b.component_name);
var aValue = displayOrder.indexOf(a.serviceId) != -1 ? displayOrder.indexOf(a.serviceId) : indexForUnordered;
var bValue = displayOrder.indexOf(b.serviceId) != -1 ? displayOrder.indexOf(b.serviceId) : indexForUnordered;
return aValue - bValue;
});
},
/**
* Update dependent co-hosted components according to the change in the component host
* @method updateCoHosts
*/
updateCoHosts: function () {
var components = App.StackServiceComponent.find().filterProperty('isOtherComponentCoHosted');
var selectedServicesMasters = this.get('selectedServicesMasters');
components.forEach(function (component) {
var componentName = component.get('componentName');
var hostComponent = selectedServicesMasters.findProperty('component_name', componentName);
var dependentCoHosts = component.get('coHostedComponents');
dependentCoHosts.forEach(function (coHostedComponent) {
var dependentHostComponent = selectedServicesMasters.findProperty('component_name', coHostedComponent);
if (!this.get('mastersToMove').contains(coHostedComponent) && hostComponent && dependentHostComponent) dependentHostComponent.set('selectedHost', hostComponent.get('selectedHost'));
}, this);
}, this);
}.observes('selectedServicesMasters.@each.selectedHost'),
/**
* On change callback for inputs
* @param {string} componentName
* @param {string} selectedHost
* @param {number} serviceComponentId
* @method assignHostToMaster
*/
assignHostToMaster: function (componentName, selectedHost, serviceComponentId) {
var flag = this.isHostNameValid(componentName, selectedHost);
var component;
this.updateIsHostNameValidFlag(componentName, serviceComponentId, flag);
if (serviceComponentId) {
component = this.get('selectedServicesMasters').filterProperty('component_name', componentName).findProperty("serviceComponentId", serviceComponentId);
if (component) component.set("selectedHost", selectedHost);
}
else {
this.get('selectedServicesMasters').findProperty("component_name", componentName).set("selectedHost", selectedHost);
}
},
/**
* Determines if hostName is valid for component:
* <ul>
* <li>host name shouldn't be empty</li>
* <li>host should exist</li>
* <li>if host installed maintenance state should be 'OFF'</li>
* <li>host should have only one component with <code>componentName</code></li>
* </ul>
* @param {string} componentName
* @param {string} selectedHost
* @returns {boolean} true - valid, false - invalid
* @method isHostNameValid
*/
isHostNameValid: function (componentName, selectedHost) {
return (selectedHost.trim() !== '') &&
(this.get('hosts').filter(function(host) {
return host.host_name === selectedHost && (!host.isInstalled || host.maintenance_state === 'OFF');
}).length > 0) &&
(this.get('selectedServicesMasters').
filterProperty('component_name', componentName).
mapProperty('selectedHost').
filter(function (h) {
return h === selectedHost;
}).length <= 1);
},
/**
* Update <code>isHostNameValid</code> property with <code>flag</code> value
* for component with name <code>componentName</code> and
* <code>serviceComponentId</code>-property equal to <code>serviceComponentId</code>-parameter value
* @param {string} componentName
* @param {number} serviceComponentId
* @param {bool} flag
* @method updateIsHostNameValidFlag
*/
updateIsHostNameValidFlag: function (componentName, serviceComponentId, flag) {
var component;
if (componentName) {
if (serviceComponentId) {
component = this.get('selectedServicesMasters').filterProperty('component_name', componentName).findProperty("serviceComponentId", serviceComponentId);
} else {
component = this.get('selectedServicesMasters').findProperty("component_name", componentName);
}
if (component) {
component.set("isHostNameValid", flag);
component.set("errorMessage", flag ? null : Em.I18n.t('installer.step5.error.host.invalid'));
}
}
},
/**
* Returns last component of selected type
* @param {string} componentName
* @return {Em.Object|null}
* @method last
*/
last: function (componentName) {
return this.get("selectedServicesMasters").filterProperty("component_name", componentName).get("lastObject");
},
/**
* Add new component to ZooKeeper Server and Hbase master
* @param {string} componentName
* @return {bool} true - added, false - not added
* @method addComponent
*/
addComponent: function (componentName) {
/*
* Logic: If ZooKeeper or Hbase service is selected then there can be
* minimum 1 ZooKeeper or Hbase master in total, and
* maximum 1 ZooKeeper or Hbase on every host
*/
var maxNumMasters = this.getMaxNumberOfMasters(componentName),
currentMasters = this.get("selectedServicesMasters").filterProperty("component_name", componentName).sortProperty('serviceComponentId'),
newMaster = null,
masterHosts = null,
suggestedHost = null,
i = 0,
lastMaster = null;
if (!currentMasters.length) {
return false;
}
if (currentMasters.get("length") < maxNumMasters) {
currentMasters.set("lastObject.showAddControl", false);
currentMasters.set("lastObject.showRemoveControl", true);
//create a new master component host based on an existing one
newMaster = Em.Object.create({});
lastMaster = currentMasters.get("lastObject");
newMaster.set("display_name", lastMaster.get("display_name"));
newMaster.set("component_name", lastMaster.get("component_name"));
newMaster.set("selectedHost", lastMaster.get("selectedHost"));
newMaster.set("serviceId", lastMaster.get("serviceId"));
newMaster.set("isInstalled", false);
newMaster.set('showAdditionalPrefix', this.get('showAdditionalPrefix').contains(lastMaster.get("component_name")));
if (currentMasters.get("length") === (maxNumMasters - 1)) {
newMaster.set("showAddControl", false);
} else {
newMaster.set("showAddControl", true);
}
newMaster.set("showRemoveControl", true);
//get recommended host for the new Zookeeper server
masterHosts = currentMasters.mapProperty("selectedHost").uniq();
for (i = 0; i < this.get("hosts.length"); i++) {
if (!(masterHosts.contains(this.get("hosts")[i].get("host_name")))) {
suggestedHost = this.get("hosts")[i].get("host_name");
break;
}
}
newMaster.set("selectedHost", suggestedHost);
newMaster.set("serviceComponentId", (currentMasters.get("lastObject.serviceComponentId") + 1));
this.get("selectedServicesMasters").insertAt(this.get("selectedServicesMasters").indexOf(lastMaster) + 1, newMaster);
this.setProperties({
componentToRebalance: componentName,
lastChangedComponent: componentName
});
this.incrementProperty('rebalanceComponentHostsCounter');
this.toggleProperty('hostNameCheckTrigger');
return true;
}
return false;//if no more zookeepers can be added
},
/**
* Remove component from ZooKeeper server or Hbase Master
* @param {string} componentName
* @param {number} serviceComponentId
* @return {bool} true - removed, false - no
* @method removeComponent
*/
removeComponent: function (componentName, serviceComponentId) {
var currentMasters = this.get("selectedServicesMasters").filterProperty("component_name", componentName);
//work only if the multiple master service is selected in previous step
if (currentMasters.length <= 1) {
return false;
}
this.get("selectedServicesMasters").removeAt(this.get("selectedServicesMasters").indexOf(currentMasters.findProperty("serviceComponentId", serviceComponentId)));
currentMasters = this.get("selectedServicesMasters").filterProperty("component_name", componentName);
if (currentMasters.get("length") < this.getMaxNumberOfMasters(componentName)) {
currentMasters.set("lastObject.showAddControl", true);
}
if (currentMasters.filterProperty('isInstalled', false).get("length") === 1) {
currentMasters.set("lastObject.showRemoveControl", false);
}
this.setProperties({
componentToRebalance: componentName,
lastChangedComponent: componentName
});
this.incrementProperty('rebalanceComponentHostsCounter');
this.toggleProperty('hostNameCheckTrigger');
return true;
},
recommendAndValidate: function(callback) {
var self = this;
if (this.get('validationInProgress')) {
this.set('runQueuedValidation', true);
return;
}
this.set('validationInProgress', true);
// load recommendations with partial request
self.loadComponentsRecommendationsFromServer(function() {
// For validation use latest received recommendations because it contains current master layout and recommended slave/client layout
self.validate(self.get('recommendations'), function() {
if (callback) {
callback();
}
self.set('validationInProgress', false);
if (self.get('runQueuedValidation')) {
self.set('runQueuedValidation', false);
self.recommendAndValidate(callback);
}
});
}, true);
},
_goNextStepIfValid: function () {
if (!this.get('submitDisabled')) {
App.router.send('next');
}else{
App.set('router.nextBtnClickInProgress', false);
}
},
nextButtonDisabled: Em.computed.or('App.router.btnClickInProgress', 'submitDisabled', 'validationInProgress', '!isLoaded'),
/**
* Submit button click handler
* Disable 'Next' button while it is already under process. (using Router's property 'nextBtnClickInProgress')
* @method submit
*/
submit: function () {
var self = this;
if (this.get('submitDisabled')) {
return;
}
if (!this.get('submitButtonClicked') && !App.get('router.nextBtnClickInProgress')) {
this.set('submitButtonClicked', true);
App.router.set('nextBtnClickInProgress', true);
if (this.get('useServerValidation')) {
self.recommendAndValidate(function () {
self.showValidationIssuesAcceptBox(self._goNextStepIfValid.bind(self));
});
}
else {
this.updateIsSubmitDisabled();
this._goNextStepIfValid();
this.set('submitButtonClicked', false);
}
}
},
/**
* In case of any validation issues shows accept dialog box for user which allow cancel and fix issues or continue anyway
* @method showValidationIssuesAcceptBox
*/
showValidationIssuesAcceptBox: function(callback) {
var self = this;
// If there are no warnings and no errors, return
if (!self.get('anyWarning') && !self.get('anyError')) {
callback();
self.set('submitButtonClicked', false);
return;
}
App.ModalPopup.show({
primary: Em.I18n.t('common.continueAnyway'),
header: Em.I18n.t('installer.step5.validationIssuesAttention.header'),
body: Em.I18n.t('installer.step5.validationIssuesAttention'),
onPrimary: function () {
this._super();
callback();
self.set('submitButtonClicked', false);
},
onSecondary: function () {
this._super();
App.router.set('nextBtnClickInProgress', false);
self.set('submitButtonClicked', false);
},
onClose: function () {
this._super();
self.set('submitButtonClicked', false);
}
});
},
getHosts: function () {
return Em.keys(this.get('content.hosts'));
}
});