993 lines
48 KiB
JavaScript
993 lines
48 KiB
JavaScript
/*
|
|
* Copyright 2013 Mirantis, Inc.
|
|
*
|
|
* Licensed 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.
|
|
**/
|
|
define(
|
|
[
|
|
'utils',
|
|
'models',
|
|
'views/dialogs',
|
|
'views/cluster_page_tabs/nodes_tab_screens/screen',
|
|
'text!templates/cluster/nodes_management_panel.html',
|
|
'text!templates/cluster/assign_roles_panel.html',
|
|
'text!templates/cluster/node_list.html',
|
|
'text!templates/cluster/node_group.html',
|
|
'text!templates/cluster/node.html',
|
|
'text!templates/cluster/node_roles.html'
|
|
],
|
|
function(utils, models, dialogViews, Screen, nodesManagementPanelTemplate, assignRolesPanelTemplate, nodeListTemplate, nodeGroupTemplate, nodeTemplate, nodeRoleTemplate) {
|
|
'use strict';
|
|
var NodeListScreen, NodesManagementPanel, AssignRolesPanel, NodeList, NodeGroup, Node;
|
|
|
|
NodeListScreen = Screen.extend({
|
|
constructorName: 'NodeListScreen',
|
|
updateInterval: 20000,
|
|
hasChanges: function() {
|
|
return this instanceof this.ClusterNodesScreen ? false : !_.isEqual(this.nodes.pluck('pending_roles'), this.initialNodes.pluck('pending_roles'));
|
|
},
|
|
scheduleUpdate: function() {
|
|
this.registerDeferred($.timeout(this.updateInterval).done(_.bind(this.update, this)));
|
|
},
|
|
update: function() {
|
|
this.nodes.fetch().always(_.bind(this.scheduleUpdate, this));
|
|
},
|
|
revertChanges: function() {
|
|
this.nodes.each(function(node) {
|
|
node.set({pending_roles: this.initialNodes.get(node.id).get('pending_roles')}, {silent: true});
|
|
}, this);
|
|
},
|
|
calculateApplyButtonState: function() {
|
|
this.applyChangesButton.set('disabled', !this.hasChanges());
|
|
},
|
|
updateBatchActionsButtons: function() {
|
|
var nodes = new models.Nodes(this.nodes.where({checked: true}));
|
|
var deployedNodes = nodes.where({'status': 'ready'});
|
|
this.configureDisksButton.set('disabled', !nodes.length || deployedNodes.length > 1);
|
|
this.configureInterfacesButton.set('disabled', !nodes.length || deployedNodes.length > 1);
|
|
this.deleteNodesButton.set('visible', !!nodes.where({pending_deletion: false}).length && !this.isLocked());
|
|
this.addNodesButton.set('visible', !nodes.length);
|
|
var notDeployedSelectedNodes = nodes.where({pending_addition: true});
|
|
this.editRolesButton.set('visible', !!notDeployedSelectedNodes.length && notDeployedSelectedNodes.length == nodes.length);
|
|
// check selected nodes for group configuration availability
|
|
var noDisksConflict = true;
|
|
nodes.each(function(node) {
|
|
var noRolesConflict = !_.difference(_.union(nodes.at(0).get('roles'), nodes.at(0).get('pending_roles')), _.union(node.get('roles'), node.get('pending_roles'))).length;
|
|
noDisksConflict = noDisksConflict && noRolesConflict && _.isEqual(nodes.at(0).resource('disks'), node.resource('disks'));
|
|
});
|
|
if (!deployedNodes.length) {
|
|
this.configureDisksButton.set('invalid', !noDisksConflict);
|
|
this.configureInterfacesButton.set('invalid', _.uniq(nodes.map(function(node) {return node.resource('interfaces');})).length > 1 || !!nodes.where({status: 'error'}).length);
|
|
}
|
|
},
|
|
setupButtonsBindings: function() {
|
|
var visibleBindings = {
|
|
observe: 'visible',
|
|
visible: true
|
|
};
|
|
var disabledBindings = {
|
|
attributes: [{
|
|
name: 'disabled',
|
|
observe: 'disabled'
|
|
}]
|
|
};
|
|
this.stickit(this.deleteNodesButton, {'.btn-delete-nodes': visibleBindings});
|
|
this.stickit(this.configureDisksButton, {'.btn-configure-disks' : {
|
|
attributes: _.union([], disabledBindings.attributes, this.getConfigureButtonsObject('btn btn-group-congiration btn-configure-disks'))
|
|
}});
|
|
this.stickit(this.configureInterfacesButton, {'.btn-configure-interfaces': {
|
|
attributes: _.union([], disabledBindings.attributes, this.getConfigureButtonsObject('btn btn-group-congiration btn-configure-interfaces'))
|
|
}});
|
|
this.stickit(this.addNodesButton, {'.btn-add-nodes': _.extend({}, visibleBindings, disabledBindings)});
|
|
this.stickit(this.editRolesButton, {'.btn-edit-nodes': _.extend({}, visibleBindings,disabledBindings)});
|
|
this.stickit(this.applyChangesButton, {'.btn-apply': disabledBindings});
|
|
},
|
|
getConfigureButtonsObject: function(className) {
|
|
return [
|
|
{
|
|
name: 'data-invalid',
|
|
observe: 'invalid'
|
|
},
|
|
{
|
|
name: 'class',
|
|
observe: 'invalid',
|
|
onGet: function(value) {
|
|
return value ? className + ' conflict' : className;
|
|
}
|
|
}
|
|
];
|
|
},
|
|
actualizePendingRoles: function(node, roles, options) {
|
|
if (!options.assign) {
|
|
node.set({pending_roles: node.previous('pending_roles')}, {assign: true});
|
|
}
|
|
},
|
|
actualizeFilteredNode: function(node, options) {
|
|
var filteredNode = this.nodeList.filteredNodes.get(node.id);
|
|
if (filteredNode) {
|
|
filteredNode.set(node.attributes);
|
|
}
|
|
},
|
|
initialize: function() {
|
|
this.ClusterNodesScreen = require('views/cluster_page_tabs/nodes_tab_screens/cluster_nodes_screen');
|
|
this.AddNodesScreen = require('views/cluster_page_tabs/nodes_tab_screens/add_nodes_screen');
|
|
this.EditNodesScreen = require('views/cluster_page_tabs/nodes_tab_screens/edit_nodes_screen');
|
|
this.nodes.on('resize', function() {
|
|
this.render();
|
|
this.nodeList.filterNodes(this.nodeFilter.get('value'));
|
|
}, this);
|
|
if (this instanceof this.AddNodesScreen || this instanceof this.EditNodesScreen) {
|
|
this.nodes.on('change:pending_roles', this.actualizePendingRoles, this);
|
|
this.model.on('change:status', function() {
|
|
app.navigate('#cluster/' + this.model.id + '/nodes', {trigger: true});
|
|
}, this);
|
|
}
|
|
this.nodes.on('change', this.actualizeFilteredNode, this);
|
|
this.scheduleUpdate();
|
|
var defaultButtonModelsData = {
|
|
visible: false,
|
|
disabled: true,
|
|
invalid: false
|
|
};
|
|
this.addNodesButton = new Backbone.Model(_.extend({}, defaultButtonModelsData, {visible: true, disabled: false}));
|
|
this.deleteNodesButton = new Backbone.Model(_.extend({}, defaultButtonModelsData, {disabled: false}));
|
|
this.editRolesButton = new Backbone.Model(_.extend({}, defaultButtonModelsData));
|
|
this.configureDisksButton = new Backbone.Model(_.extend({}, defaultButtonModelsData));
|
|
this.configureInterfacesButton = new Backbone.Model(_.extend({}, defaultButtonModelsData));
|
|
this.applyChangesButton = new Backbone.Model(_.extend({}, defaultButtonModelsData));
|
|
this.nodeFilter = new Backbone.Model({value: ''});
|
|
this.nodeFilter.on('change', _.debounce(function(filter) {
|
|
this.nodeList.filterNodes(filter.get('value'));
|
|
}, 300), this);
|
|
},
|
|
render: function() {
|
|
this.tearDownRegisteredSubViews();
|
|
this.$el.html('');
|
|
if (this instanceof this.EditNodesScreen) {
|
|
this.$el.append($('<div>').addClass('alert').text($.t('cluster_page.nodes_tab.disk_configuration_reset_warning')));
|
|
}
|
|
var options = {nodes: this.nodes, screen: this};
|
|
var managementPanel = new NodesManagementPanel(options);
|
|
this.registerSubView(managementPanel);
|
|
this.$el.append(managementPanel.render().el);
|
|
if (this instanceof this.AddNodesScreen || this instanceof this.EditNodesScreen) {
|
|
this.roles = new AssignRolesPanel(options);
|
|
this.registerSubView(this.roles);
|
|
this.$el.append(this.roles.render().el);
|
|
}
|
|
this.nodeList = new NodeList(options);
|
|
this.registerSubView(this.nodeList);
|
|
this.$el.append(this.nodeList.render().el);
|
|
this.nodeList.calculateSelectAllCheckedState();
|
|
this.nodeList.calculateSelectAllDisabledState();
|
|
this.setupButtonsBindings();
|
|
var bindings = {
|
|
'input[name=filter]': {
|
|
observe: 'value',
|
|
getVal: function($el) { return $.trim($el.val()).toLowerCase(); }
|
|
},
|
|
'.btn-clear-filter': {
|
|
observe: 'value',
|
|
visible: function(value, options) {
|
|
return !!value;
|
|
}
|
|
}
|
|
};
|
|
this.stickit(this.nodeFilter, bindings);
|
|
return this;
|
|
}
|
|
});
|
|
|
|
NodesManagementPanel = Backbone.View.extend({
|
|
className: 'nodes-management-panel',
|
|
template: _.template(nodesManagementPanelTemplate),
|
|
events: {
|
|
'change select[name=grouping]' : 'groupNodes',
|
|
'click .btn-delete-nodes:not(:disabled)' : 'showDeleteNodesDialog',
|
|
'click .btn-apply:not(:disabled)' : 'applyChanges',
|
|
'click .btn-group-congiration:not(.conflict):not(:disabled)' : 'goToConfigurationScreen',
|
|
'click .btn-group-congiration.conflict' : 'showUnavailableGroupConfigurationDialog',
|
|
'click .btn-add-nodes': 'goToAddNodesScreen',
|
|
'click .btn-edit-nodes': 'goToEditNodesRolesScreen',
|
|
'click .btn-cancel': 'goToNodesList',
|
|
'click .btn-clear-filter' : 'clearFilter'
|
|
},
|
|
initialize: function(options) {
|
|
_.defaults(this, options);
|
|
this.cluster = this.screen.tab.model;
|
|
},
|
|
groupNodes: function(e) {
|
|
var grouping = this.$(e.currentTarget).val();
|
|
if (!(this.screen instanceof this.screen.AddNodesScreen || this.screen instanceof this.screen.EditNodesScreen)) {
|
|
this.cluster.save({grouping: grouping}, {patch: true, wait: true});
|
|
}
|
|
this.screen.nodeList.groupNodes(grouping);
|
|
},
|
|
showDeleteNodesDialog: function() {
|
|
var nodes = new models.Nodes(this.screen.nodes.where({checked: true}));
|
|
nodes.cluster = this.nodes.cluster;
|
|
var dialog = new dialogViews.DeleteNodesDialog({nodes: nodes});
|
|
app.page.tab.registerSubView(dialog);
|
|
dialog.render();
|
|
},
|
|
applyChanges: function() {
|
|
this.$('.btn-apply').prop('disabled', true);
|
|
var nodes = new models.Nodes(_.invoke(this.screen.nodes.where({checked: true}), 'clone'));
|
|
nodes.each(function(node) {
|
|
if (!this.nodes.cluster) {
|
|
node.set({cluster_id: this.cluster.id, pending_addition: true});
|
|
}
|
|
if (!node.get('pending_roles').length && node.get('pending_addition')) {
|
|
node.set({cluster_id: null, pending_addition: false});
|
|
}
|
|
}, this);
|
|
nodes.toJSON = function(options) {
|
|
return this.map(function(node) {
|
|
return _.pick(node.attributes, 'id', 'cluster_id', 'pending_roles', 'pending_addition');
|
|
});
|
|
};
|
|
nodes.sync('update', nodes)
|
|
.done(_.bind(function() {
|
|
$.when(this.cluster.fetch(), this.cluster.fetchRelated('nodes')).always(_.bind(function() {
|
|
app.navigate('#cluster/' + this.cluster.id + '/nodes', {trigger: true});
|
|
app.navbar.refresh();
|
|
app.page.removeFinishedNetworkTasks();
|
|
app.page.deploymentControl.render();
|
|
}, this));
|
|
}, this))
|
|
.fail(_.bind(function() {
|
|
this.$('.btn-apply').prop('disabled', false);
|
|
utils.showErrorDialog({
|
|
title: $.t('cluster_page.nodes_tab.node_management_panel.node_management_error.title'),
|
|
message: $.t('cluster_page.nodes_tab.node_management_panel.node_management_error.saving_warning')
|
|
});
|
|
}, this));
|
|
},
|
|
goToConfigurationScreen: function(e) {
|
|
var selectedNodesIds = _.pluck(this.screen.nodes.where({checked: true}), 'id').join(',');
|
|
app.navigate('#cluster/' + this.cluster.id + '/nodes/' + $(e.currentTarget).data('action') + '/' + utils.serializeTabOptions({nodes: selectedNodesIds}), {trigger: true});
|
|
},
|
|
goToAddNodesScreen: function() {
|
|
app.navigate('#cluster/' + this.cluster.id + '/nodes/add', {trigger: true});
|
|
},
|
|
goToEditNodesRolesScreen: function() {
|
|
app.navigate('#cluster/' + this.cluster.id + '/nodes/edit/' + utils.serializeTabOptions({nodes: _.pluck(this.nodes.where({checked: true}), 'id')}), {trigger: true});
|
|
},
|
|
goToNodesList: function() {
|
|
this.screen.revertChanges();
|
|
app.navigate('#cluster/' + this.cluster.id + '/nodes', {trigger: true});
|
|
},
|
|
clearFilter: function() {
|
|
this.screen.nodeFilter.set('value', '');
|
|
},
|
|
showUnavailableGroupConfigurationDialog: function (e) {
|
|
var action = this.$(e.currentTarget).data('action');
|
|
utils.showErrorDialog({
|
|
title: $.t('cluster_page.nodes_tab.node_management_panel.node_management_error.title'),
|
|
message: $.t('cluster_page.nodes_tab.node_management_panel.node_management_error.' + action + '_configuration_warning'),
|
|
hideLogsLink: true
|
|
});
|
|
},
|
|
render: function() {
|
|
this.tearDownRegisteredSubViews();
|
|
this.$el.html(this.template({
|
|
nodes: this.nodes,
|
|
cluster: this.cluster,
|
|
edit: this.screen instanceof this.screen.EditNodesScreen
|
|
})).i18n();
|
|
var isDisabled = !!this.cluster.tasks({group: 'deployment', status: 'running'}).length;
|
|
this.screen.addNodesButton.set('disabled', isDisabled);
|
|
this.screen.editRolesButton.set('disabled', isDisabled);
|
|
return this;
|
|
}
|
|
});
|
|
|
|
AssignRolesPanel = Backbone.View.extend({
|
|
template: _.template(assignRolesPanelTemplate),
|
|
className: 'roles-panel',
|
|
handleChanges: function() {
|
|
this.nodes = new models.Nodes(this.screen.nodes.where({checked: true}));
|
|
this.assignRoles();
|
|
this.checkForConflicts();
|
|
this.screen.calculateApplyButtonState();
|
|
},
|
|
assignRoles: function() {
|
|
_.each(this.collection.where({indeterminate: false}), function(role) {
|
|
_.each(this.nodes.filter(function(node) {return !node.hasRole(role.get('name'), true);}), function(node) {
|
|
var pending_roles = role.get('checked') ? _.uniq(_.union(node.get('pending_roles'), role.get('name'))) : _.difference(node.get('pending_roles'), role.get('name'));
|
|
node.set({pending_roles: pending_roles}, {assign: true});
|
|
});
|
|
}, this);
|
|
},
|
|
isRoleSelected: function(roleName) {
|
|
return this.collection.filter(function(role) {return role.get('name') == roleName && (role.get('checked') || role.get('indeterminate'));}).length;
|
|
},
|
|
isControllerSelectable: function(role) {
|
|
var allocatedController = this.cluster.get('nodes').filter(function(node) {return !node.get('pending_deletion') && node.hasRole('controller') && !_.contains(this.nodes.pluck('id'), node.id);}, this);
|
|
return role.get('name') != 'controller' || this.cluster.get('mode') != 'multinode' || ((this.isRoleSelected('controller') || this.screen.nodes.where({checked: true}).length <= 1) && !allocatedController.length);
|
|
},
|
|
isMongoSelectable: function(role) {
|
|
var deployedNodes = this.cluster.get('nodes').filter(function(node) {
|
|
return node.hasRole('mongo', true) && !node.get('pending_deletion');
|
|
});
|
|
return role.get('name') != 'mongo' || !deployedNodes.length;
|
|
},
|
|
isZabbixSelectable: function(role) {
|
|
var allocatedZabbix = this.cluster.get('nodes').filter(function(node) {return !node.get('pending_deletion') && node.hasRole('zabbix-server') && !_.contains(this.nodes.pluck('id'), node.id);}, this);
|
|
return role.get('name') != 'zabbix-server' || ((this.isRoleSelected('zabbix-server') || this.screen.nodes.where({checked: true}).length <= 1) && !allocatedZabbix.length);
|
|
},
|
|
getListOfIncompatibleRoles: function(roles) {
|
|
var forbiddenRoles = [];
|
|
_.each(roles, function(role) {
|
|
forbiddenRoles = _.union(forbiddenRoles, this.conflictingRoles[role.get('name')]);
|
|
}, this);
|
|
return _.uniq(forbiddenRoles);
|
|
},
|
|
checkForConflicts: function(e) {
|
|
this.collection.each(function(role) {
|
|
var conflict = '';
|
|
var disabled = !this.screen.nodes.length || this.loading.state() == 'pending';
|
|
// checking if role is unavailable
|
|
if (!disabled && role.get('unavailable')) {
|
|
disabled = true;
|
|
conflict = role.get('unavailabityReason');
|
|
}
|
|
// checking if role conflict with another role
|
|
if (!disabled) {
|
|
var selectedRoles = this.collection.filter(function(role) {return role.get('checked') || role.get('indeterminate');});
|
|
var roleConflictsWithAnotherRole = _.contains(this.getListOfIncompatibleRoles(selectedRoles), role.get('name'));
|
|
if (roleConflictsWithAnotherRole) {
|
|
disabled = true;
|
|
conflict = $.t('cluster_page.nodes_tab.incompatible_roles_warning');
|
|
}
|
|
}
|
|
// checking controller role conditions
|
|
if (!disabled && !this.isControllerSelectable(role)) {
|
|
disabled = true;
|
|
conflict = $.t('cluster_page.nodes_tab.one_controller_restriction');
|
|
}
|
|
// checking mongo role restriction
|
|
if (!disabled && !this.isMongoSelectable(role)) {
|
|
disabled = true;
|
|
conflict = $.t('cluster_page.nodes_tab.mongo_restriction');
|
|
}
|
|
// checking zabbix role conditions
|
|
if (!disabled && !this.isZabbixSelectable(role)) {
|
|
disabled = true;
|
|
conflict = $.t('cluster_page.nodes_tab.one_zabbix_restriction');
|
|
}
|
|
role.set({disabled: disabled, conflict: conflict});
|
|
}, this);
|
|
if (this.screen.nodeList) {
|
|
var controllerNode = this.nodes.filter(function(node) {return node.hasRole('controller');})[0];
|
|
var zabbixNode = this.nodes.filter(function(node) {return node.hasRole('zabbix-server');})[0];
|
|
_.each(this.screen.nodes.where({checked: false}), function(node) {
|
|
var isControllerAssigned = this.cluster.get('mode') == 'multinode' && this.isRoleSelected('controller') && controllerNode && controllerNode.id != node.id;
|
|
var isZabbixAssigned = this.isRoleSelected('zabbix-server') && zabbixNode && zabbixNode.id != node.id;
|
|
var disabled = isControllerAssigned || isZabbixAssigned || !node.isSelectable() || this.screen instanceof this.screen.EditNodesScreen || this.screen.isLocked();
|
|
node.set('disabled', disabled);
|
|
var filteredNode = this.screen.nodeList.filteredNodes.get(node.id);
|
|
if (filteredNode) {
|
|
filteredNode.set('disabled', disabled);
|
|
}
|
|
}, this);
|
|
this.screen.nodeList.calculateSelectAllDisabledState();
|
|
_.invoke(this.screen.nodeList.subViews, 'calculateSelectAllDisabledState', this);
|
|
}
|
|
},
|
|
getRoleData: function(role) {
|
|
return this.cluster.get('release').get('roles_metadata')[role];
|
|
},
|
|
checkRolesAvailability: function() {
|
|
this.collection.each(function(role) {
|
|
var unavailable = false;
|
|
var visible = true;
|
|
var unavailabityReasons = [];
|
|
var dependencies = this.getRoleData(role.get('name')).depends;
|
|
if (dependencies) {
|
|
var configModels = {
|
|
cluster: this.cluster,
|
|
settings: this.settings,
|
|
version: app.version,
|
|
default: this.settings
|
|
};
|
|
_.each(_.map(dependencies, utils.expandRestriction), function(dependency) {
|
|
if (!utils.evaluateExpression(dependency.condition, configModels).value) {
|
|
unavailable = true;
|
|
unavailabityReasons.push(dependency.warning);
|
|
if (dependency.action == 'hide') {
|
|
visible = false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
// FIXME(vk): hack for vCenter, do not allow ceph and controllers
|
|
// has to be removed when we describe it in role metadata
|
|
if (this.settings.get('common.libvirt_type.value') == 'vcenter') {
|
|
if (role.get('name') == 'compute') {
|
|
unavailable = true;
|
|
unavailabityReasons.push('Computes cannot be used with vCenter');
|
|
} else if (role.get('name') == 'ceph-osd') {
|
|
unavailable = true;
|
|
unavailabityReasons.push('Ceph cannot be used with vCenter');
|
|
}
|
|
}
|
|
|
|
if (unavailable) {
|
|
role.set({unavailable: true, unavailabityReason: unavailabityReasons.join(' ')});
|
|
}
|
|
role.set({visible: visible});
|
|
}, this);
|
|
},
|
|
initialize: function(options) {
|
|
_.defaults(this, options);
|
|
this.cluster = this.screen.tab.model;
|
|
this.collection = new Backbone.Collection(_.map(this.cluster.get('release').get('roles'), function(role) {
|
|
var roleData = this.getRoleData(role);
|
|
var nodesWithRole = this.nodes.filter(function(node) {return node.hasRole(role);});
|
|
return {
|
|
name: role,
|
|
label: roleData.name,
|
|
description: roleData.description,
|
|
disabled: false,
|
|
unavailable: false,
|
|
visible: true,
|
|
conflict: '',
|
|
checked: !!nodesWithRole.length && nodesWithRole.length == this.nodes.length,
|
|
indeterminate: !!nodesWithRole.length && nodesWithRole.length != this.nodes.length
|
|
};
|
|
}, this));
|
|
this.collection.on('change:checked', this.handleChanges, this);
|
|
this.settings = this.cluster.get('settings');
|
|
(this.loading = this.settings.fetch({cache: true})).done(_.bind(function() {
|
|
this.processConflictingRoles();
|
|
this.checkRolesAvailability();
|
|
this.checkForConflicts();
|
|
}, this));
|
|
},
|
|
processConflictingRoles: function() {
|
|
var rolesMetadata = this.cluster.get('release').get('roles_metadata');
|
|
this.conflictingRoles = {};
|
|
_.each(rolesMetadata, function(roleData, roleName) {
|
|
var conflicts = roleData.conflicts;
|
|
if (conflicts) {
|
|
this.conflictingRoles[roleName] = _.uniq(_.union(this.conflictingRoles[roleName], conflicts));
|
|
_.each(conflicts, function(conflict) {
|
|
this.conflictingRoles[conflict] = this.conflictingRoles[conflict] || [];
|
|
this.conflictingRoles[conflict].push(roleName);
|
|
}, this);
|
|
}
|
|
}, this);
|
|
},
|
|
stickitRole: function (role) {
|
|
var bindings = {};
|
|
bindings['input[name=' + role.get('name') + ']'] = {
|
|
observe: 'checked',
|
|
visible: function() {
|
|
return role.get('visible');
|
|
},
|
|
visibleFn: function($el, isVisible) {
|
|
$el.parents('.role-container').toggle(isVisible);
|
|
},
|
|
onSet: function(value) {
|
|
role.set('indeterminate', false);
|
|
return value;
|
|
},
|
|
attributes: [{
|
|
name: 'disabled',
|
|
observe: 'disabled'
|
|
},{
|
|
name: 'indeterminate',
|
|
observe: 'indeterminate'
|
|
}]
|
|
};
|
|
bindings['.role-conflict.' + role.get('name')] = 'conflict';
|
|
return this.stickit(role, bindings);
|
|
},
|
|
render: function() {
|
|
this.$el.html(this.template({roles: this.collection})).i18n();
|
|
this.collection.each(this.stickitRole, this);
|
|
this.checkForConflicts();
|
|
return this;
|
|
}
|
|
});
|
|
|
|
NodeList = Backbone.View.extend({
|
|
className: 'node-list',
|
|
template: _.template(nodeListTemplate),
|
|
events: {
|
|
'click .btn-cluster-details': 'toggleSummaryPanel'
|
|
},
|
|
selectAllBindings: {
|
|
'input[name=select-nodes-common]': {
|
|
observe: 'checked',
|
|
onSet: 'selectNodes',
|
|
attributes: [{
|
|
name: 'disabled',
|
|
observe: 'disabled'
|
|
}]
|
|
}
|
|
},
|
|
selectNodes: function(value) {
|
|
_.each(this.subViews, function(nodeGroup) {
|
|
if (!nodeGroup.selectAllCheckbox.get('disabled')) {
|
|
nodeGroup.selectAllCheckbox.set('checked', value);
|
|
}
|
|
});
|
|
_.invoke(this.filteredNodes.where({disabled: false}), 'set', {checked: value});
|
|
return value;
|
|
},
|
|
hideSummaryPanel: function(e) {
|
|
if (!(e && $(e.target).closest(this.$('.node-list-name')).length)) {
|
|
this.$('.cluster-details').hide();
|
|
}
|
|
},
|
|
toggleSummaryPanel: function() {
|
|
this.$('.cluster-details').toggle();
|
|
},
|
|
calculateSelectAllCheckedState: function() {
|
|
var availableNodes = this.filteredNodes.filter(function(node) {return node.isSelectable();});
|
|
this.selectAllCheckbox.set('checked', availableNodes.length && this.filteredNodes.where({checked: true}).length == availableNodes.length);
|
|
},
|
|
calculateSelectAllDisabledState: function() {
|
|
var availableNodes = this.filteredNodes.filter(function(node) {return node.isSelectable();});
|
|
var roleAmountRestrictions = this.screen.roles && (this.screen.roles.isRoleSelected('controller') || this.screen.roles.isRoleSelected('zabbix-server')) && availableNodes.length > 1;
|
|
var disabled = !this.filteredNodes.where({disabled: false}).length || roleAmountRestrictions || this.screen instanceof this.screen.EditNodesScreen;
|
|
this.selectAllCheckbox.set('disabled', disabled);
|
|
},
|
|
groupNodes: function(grouping) {
|
|
if (_.isUndefined(grouping)) {
|
|
grouping = this.screen instanceof this.screen.AddNodesScreen ? 'hardware' : this.screen.tab.model.get('grouping');
|
|
}
|
|
var nodeGroups = _.pairs(this.filteredNodes.groupByAttribute(grouping));
|
|
// sort node groups
|
|
if (grouping != 'hardware') {
|
|
var preferredOrder = this.screen.tab.model.get('release').get('roles');
|
|
nodeGroups.sort(function(firstGroup, secondGroup) {
|
|
var firstGroupRoles = firstGroup[1][0].sortedRoles();
|
|
var secondGroupRoles = secondGroup[1][0].sortedRoles();
|
|
var order;
|
|
while (!order && firstGroupRoles.length && secondGroupRoles.length) {
|
|
order = _.indexOf(preferredOrder, firstGroupRoles.shift()) - _.indexOf(preferredOrder, secondGroupRoles.shift());
|
|
}
|
|
return order || firstGroupRoles.length - secondGroupRoles.length;
|
|
});
|
|
} else {
|
|
nodeGroups = _.sortBy(nodeGroups, function(group){ return group[0];});
|
|
}
|
|
this.renderNodeGroups(nodeGroups);
|
|
this.screen.updateBatchActionsButtons();
|
|
},
|
|
filterNodes: function(filterValue) {
|
|
this.filteredNodes.reset(_.invoke(this.nodes.filter(function(node) {
|
|
return _.contains(node.get('name').toLowerCase(), filterValue) || _.contains(node.get('mac').toLowerCase(), filterValue);
|
|
}), 'clone'));
|
|
},
|
|
initialize: function(options) {
|
|
_.defaults(this, options);
|
|
this.screen.initialNodes = new models.Nodes(this.nodes.invoke('clone'));
|
|
this.filteredNodes = new models.Nodes(this.nodes.invoke('clone'));
|
|
this.filteredNodes.cluster = this.screen.nodes.cluster;
|
|
this.filteredNodes.deferred = this.screen.nodes.deferred;
|
|
this.filteredNodes.on('change:checked', this.calculateSelectAllCheckedState, this);
|
|
this.filteredNodes.on('reset', this.render, this);
|
|
this.eventNamespace = 'click.click-summary-panel';
|
|
this.selectAllCheckbox = new Backbone.Model({
|
|
checked: false,
|
|
disabled: false
|
|
});
|
|
},
|
|
renderNodeGroups: function(nodeGroups) {
|
|
this.$('.nodes').html('');
|
|
_.each(nodeGroups, function(group) {
|
|
var nodeGroupView = new NodeGroup({
|
|
groupLabel: group[0],
|
|
nodes: new models.Nodes(group[1]),
|
|
nodeList: this
|
|
});
|
|
this.registerSubView(nodeGroupView);
|
|
this.$('.nodes').append(nodeGroupView.render().el);
|
|
}, this);
|
|
},
|
|
beforeTearDown: function() {
|
|
$('html').off(this.eventNamespace);
|
|
Backbone.history.off('route', this.hideSummaryPanel, this);
|
|
},
|
|
render: function() {
|
|
this.tearDownRegisteredSubViews();
|
|
this.$el.html(this.template({
|
|
nodes: this.filteredNodes,
|
|
totalNodesAmount: this.screen.nodes.length,
|
|
edit: this.screen instanceof this.screen.EditNodesScreen
|
|
})).i18n();
|
|
this.groupNodes();
|
|
$('html').on(this.eventNamespace, _.bind(this.hideSummaryPanel, this));
|
|
Backbone.history.on('route', this.hideSummaryPanel, this);
|
|
this.stickit(this.selectAllCheckbox, this.selectAllBindings);
|
|
return this;
|
|
}
|
|
});
|
|
|
|
NodeGroup = Backbone.View.extend({
|
|
className: 'node-group',
|
|
template: _.template(nodeGroupTemplate),
|
|
selectAllBindings: {
|
|
'input[name=select-node-group]': {
|
|
observe: 'checked',
|
|
onSet: 'selectNodes',
|
|
attributes: [{
|
|
name: 'disabled',
|
|
observe: 'disabled'
|
|
}]
|
|
}
|
|
},
|
|
selectNodes: function(value) {
|
|
_.each(this.nodes.where({disabled: false}), function(node) {
|
|
node.set('checked', value);
|
|
});
|
|
this.nodeList.calculateSelectAllCheckedState();
|
|
return value;
|
|
},
|
|
calculateSelectAllCheckedState: function() {
|
|
var availableNodes = this.nodes.filter(function(node) {return node.isSelectable();});
|
|
this.selectAllCheckbox.set('checked', availableNodes.length && this.nodes.where({checked: true}).length == availableNodes.length);
|
|
},
|
|
calculateSelectAllDisabledState: function() {
|
|
var availableNodes = this.nodes.where({disabled: false});
|
|
var roleAmountRestrictions = this.nodeList.screen.roles && (this.nodeList.screen.roles.isRoleSelected('controller') || this.nodeList.screen.roles.isRoleSelected('zabbix-server')) && availableNodes.length > 1;
|
|
var disabled = !availableNodes.length || roleAmountRestrictions || this.nodeList.screen instanceof this.nodeList.screen.EditNodesScreen;
|
|
this.selectAllCheckbox.set('disabled', disabled);
|
|
},
|
|
initialize: function(options) {
|
|
_.defaults(this, options);
|
|
this.selectAllCheckbox = new Backbone.Model({
|
|
checked: false,
|
|
disabled: false
|
|
});
|
|
this.selectAllCheckbox.on('change:disabled', this.nodeList.calculateSelectAllDisabledState, this.nodeList);
|
|
this.nodes.on('change:checked', this.calculateSelectAllCheckedState, this);
|
|
},
|
|
renderNode: function(node) {
|
|
var nodeView = new Node({
|
|
node: node,
|
|
group: this
|
|
});
|
|
this.registerSubView(nodeView);
|
|
this.$('.nodes-group').append(nodeView.render().el);
|
|
},
|
|
render: function() {
|
|
this.tearDownRegisteredSubViews();
|
|
this.$el.html(this.template({
|
|
groupLabel: this.groupLabel,
|
|
nodes: this.nodes
|
|
})).i18n();
|
|
this.nodes.each(this.renderNode, this);
|
|
this.stickit(this.selectAllCheckbox, this.selectAllBindings);
|
|
this.calculateSelectAllCheckedState();
|
|
this.calculateSelectAllDisabledState();
|
|
return this;
|
|
}
|
|
});
|
|
|
|
Node = Backbone.View.extend({
|
|
template: _.template(nodeTemplate),
|
|
roleTemplate: _.template(nodeRoleTemplate),
|
|
templateHelpers: _.pick(utils, 'showDiskSize', 'showMemorySize'),
|
|
renaming: false,
|
|
events: {
|
|
'click .node-renameable': 'startNodeRenaming',
|
|
'keydown .name input': 'onNodeNameInputKeydown',
|
|
'click .node-details': 'showNodeDetails',
|
|
'click .btn-discard-role-changes': 'discardRoleChanges',
|
|
'click .btn-discard-addition': 'discardAddition',
|
|
'click .btn-discard-deletion': 'discardDeletion',
|
|
'click .btn-view-logs': 'showNodeLogs'
|
|
},
|
|
bindings: {
|
|
'.role-list': {
|
|
observe: ['roles', 'pending_roles'],
|
|
update: function($el) {
|
|
return $el.html(this.roleTemplate({
|
|
deployedRoles: this.sortRoles(this.node.get('roles')),
|
|
pendingRoles: this.sortRoles(this.node.get('pending_roles'))
|
|
}));
|
|
}
|
|
},
|
|
'.node': {
|
|
attributes: [{
|
|
name: 'class',
|
|
observe: 'checked',
|
|
onGet: function(value, options) {
|
|
return value ? 'node checked' : 'node';
|
|
}
|
|
}]
|
|
},
|
|
'.node-checkbox input': {
|
|
observe: 'checked',
|
|
attributes: [{
|
|
name: 'disabled',
|
|
observe: 'disabled'
|
|
}]
|
|
},
|
|
'.node-status-label': {
|
|
observe: ['status', 'online', 'pending_addition', 'pending_deletion'],
|
|
onGet: 'formatStatusLabel'
|
|
},
|
|
'.node-box': {
|
|
attributes: [{
|
|
name: 'class',
|
|
observe: ['status', 'online', 'pending_addition', 'pending_deletion', 'disabled'],
|
|
onGet: 'formatNodePanelClass'
|
|
}]
|
|
},
|
|
'.node-status': {
|
|
attributes: [{
|
|
name: 'class',
|
|
observe: ['status', 'online', 'pending_addition', 'pending_deletion'],
|
|
onGet: 'formatStatusBlockClass'
|
|
}]
|
|
},
|
|
'.progress': {
|
|
attributes: [{
|
|
name: 'class',
|
|
observe: 'status',
|
|
onGet: function(value, options) {
|
|
var progressBarClass = value == 'deploying' ? 'progress-success' : value == 'provisioning' ? '' : 'hide';
|
|
return 'progress ' + progressBarClass;
|
|
}
|
|
}]
|
|
},
|
|
'.bar': {
|
|
observe: 'progress',
|
|
update: function($el, value) {
|
|
value = _.max([value, 3]);
|
|
$el.css('width', value + '%');
|
|
}
|
|
},
|
|
'.node-status i': {
|
|
observe: 'status',
|
|
visible: function(value) {
|
|
return !_.contains(['provisioning', 'deploying'], value);
|
|
},
|
|
attributes: [{
|
|
name: 'class',
|
|
observe: ['status', 'online', 'pending_addition', 'pending_deletion'],
|
|
onGet: 'formatStatusIconClass'
|
|
}]
|
|
},
|
|
'.node-button button': {
|
|
observe: 'cluster',
|
|
visible: function(value) {
|
|
return !_.isUndefined(value) && value != '';
|
|
},
|
|
attributes: [{
|
|
name: 'class',
|
|
observe: ['pending_addition', 'pending_deletion', 'pending_roles'],
|
|
onGet: 'formatNodeButtonClass'
|
|
},{
|
|
name: 'title',
|
|
observe: ['pending_addition', 'pending_deletion', 'pending_roles'],
|
|
onGet: 'formatNodeButtonTitle'
|
|
}]
|
|
},
|
|
'.node-button i': {
|
|
attributes: [{
|
|
name: 'class',
|
|
observe: ['pending_addition', 'pending_deletion', 'pending_roles'],
|
|
onGet: 'formatNodeButtonIcon'
|
|
}]
|
|
},
|
|
'.name p': {
|
|
observe: ['name', 'mac'],
|
|
onGet: function(values) {
|
|
return values[0] || values[1];
|
|
}
|
|
}
|
|
},
|
|
sortRoles: function(roles) {
|
|
roles = roles || [];
|
|
var preferredOrder = this.screen.tab.model.get('release').get('roles');
|
|
return roles.sort(function(a, b) {
|
|
return _.indexOf(preferredOrder, a) - _.indexOf(preferredOrder, b);
|
|
});
|
|
},
|
|
defineNodeViewStatus: function() {
|
|
return !this.node.get('online') ? 'offline' : this.node.get('pending_addition') ? 'pending_addition' : this.node.get('pending_deletion') ? 'pending_deletion' : this.node.get('status');
|
|
},
|
|
formatNodePanelClass: function(value, options) {
|
|
var nodeClass = this.node.get('pending_deletion') ? 'node-delete' : this.node.get('pending_addition') ? 'node-new' : this.node.get('online') ? this.node.get('status') : 'node-offline';
|
|
return 'node-box ' + nodeClass + (this.node.get('disabled') ? ' disabled' : '');
|
|
},
|
|
formatStatusIconClass: function(value, options) {
|
|
var icons = {
|
|
offline: 'icon-block',
|
|
pending_addition: 'icon-ok-circle-empty',
|
|
pending_deletion: 'icon-cancel-circle',
|
|
ready: 'icon-ok',
|
|
provisioned: 'icon-install',
|
|
error: 'icon-attention',
|
|
discover: 'icon-ok-circle-empty'
|
|
};
|
|
return icons[this.defineNodeViewStatus()] || '';
|
|
},
|
|
formatStatusBlockClass: function(value, options) {
|
|
var classes = {
|
|
offline: 'msg-offline',
|
|
pending_addition: 'msg-ok',
|
|
pending_deletion: 'msg-warning',
|
|
ready: 'msg-ok',
|
|
provisioning: 'provisioning',
|
|
provisioned: 'msg-provisioned',
|
|
deploying: 'deploying',
|
|
error: 'msg-error',
|
|
discover: 'msg-discover'
|
|
};
|
|
return 'node-status ' + classes[this.defineNodeViewStatus()];
|
|
},
|
|
formatStatusLabel: function(value) {
|
|
var operatingSystem;
|
|
try {
|
|
operatingSystem = this.node.collection.cluster.get('release').get('operating_system');
|
|
} catch (ignore) {}
|
|
operatingSystem = operatingSystem || 'OS';
|
|
var labels = {
|
|
offline: $.t('cluster_page.nodes_tab.node.status.offline'),
|
|
pending_addition: $.t('cluster_page.nodes_tab.node.status.pending_addition'),
|
|
pending_deletion: $.t('cluster_page.nodes_tab.node.status.pending_deletion'),
|
|
ready: $.t('cluster_page.nodes_tab.node.status.ready'),
|
|
provisioning: $.t('cluster_page.nodes_tab.node.status.installing_os', {os: operatingSystem}),
|
|
provisioned: $.t('cluster_page.nodes_tab.node.status.os_is_installed', {os: operatingSystem}),
|
|
deploying: $.t('cluster_page.nodes_tab.node.status.installing_openstack'),
|
|
error: $.t('cluster_page.nodes_tab.node.status.error'),
|
|
discover: $.t('cluster_page.nodes_tab.node.status.discovered')
|
|
};
|
|
return labels[this.defineNodeViewStatus()] || '';
|
|
},
|
|
hasChanges: function() {
|
|
return this.node.get('pending_addition') || this.node.get('pending_deletion') || (this.node.get('pending_roles') && this.node.get('pending_roles').length);
|
|
},
|
|
formatNodeButtonClass: function(value, options) {
|
|
var btnClass = this.node.get('pending_addition') ? 'addition' : this.node.get('pending_deletion') ? 'deletion' : 'role-changes';
|
|
return this.hasChanges() && !(this.screen instanceof this.screen.EditNodesScreen) ? 'btn btn-link btn-discard-node-changes btn-discard-' + btnClass : 'btn btn-link btn-view-logs';
|
|
},
|
|
formatNodeButtonTitle: function(value, options) {
|
|
var title = this.node.get('pending_addition') ? $.t('cluster_page.nodes_tab.node.status.discard_addition') : this.node.get('pending_deletion') ? $.t('cluster_page.nodes_tab.node.status.discard_deletion') : $.t('cluster_page.nodes_tab.node.status.discard_role_changes');
|
|
return this.hasChanges() && !(this.screen instanceof this.screen.EditNodesScreen) ? title : $.t('cluster_page.nodes_tab.node.status.view_logs');
|
|
},
|
|
formatNodeButtonIcon: function(value, options) {
|
|
return this.hasChanges() && !(this.screen instanceof this.screen.EditNodesScreen) ? 'icon-back-in-time' : 'icon-logs';
|
|
},
|
|
calculateNodeState: function() {
|
|
this.node.set('disabled', !this.node.isSelectable() || this.screen instanceof this.screen.EditNodesScreen || this.screen.isLocked());
|
|
if (this.screen.isLocked()) {
|
|
this.node.set('checked', false);
|
|
}
|
|
},
|
|
startNodeRenaming: function() {
|
|
$('html').on(this.eventNamespace, _.bind(function(e) {
|
|
// FIXME: 'node-renameable' class usage should be revised
|
|
if ($(e.target).hasClass('node-name') || $(e.target).hasClass('node-renameable')) {
|
|
e.preventDefault();
|
|
} else {
|
|
this.endNodeRenaming();
|
|
}
|
|
}, this));
|
|
this.renaming = true;
|
|
this.render();
|
|
this.$('.node-name').focus();
|
|
},
|
|
endNodeRenaming: function() {
|
|
$('html').off(this.eventNamespace);
|
|
this.renaming = false;
|
|
this.render();
|
|
},
|
|
applyNewNodeName: function() {
|
|
var name = $.trim(this.$('.node-name').val());
|
|
if (name && name != this.node.get('name')) {
|
|
this.$('.node-name').attr('disabled', true);
|
|
this.screen.nodes.get(this.node.id).save({name: name}, {patch: true, wait: true}).always(_.bind(this.endNodeRenaming, this));
|
|
} else {
|
|
this.endNodeRenaming();
|
|
}
|
|
},
|
|
onNodeNameInputKeydown: function(e) {
|
|
if (e.which == 13) {
|
|
this.applyNewNodeName();
|
|
} else if (e.which == 27) {
|
|
this.endNodeRenaming();
|
|
}
|
|
},
|
|
showNodeDetails: function(e) {
|
|
e.preventDefault();
|
|
var dialog = new dialogViews.ShowNodeInfoDialog({node: this.node});
|
|
app.page.tab.registerSubView(dialog);
|
|
dialog.render();
|
|
},
|
|
updateNode: function(data) {
|
|
this.node.save(data, {patch: true, wait: true})
|
|
.done(_.bind(function() {
|
|
this.screen.tab.model.fetch();
|
|
this.screen.tab.model.fetchRelated('nodes');
|
|
app.navbar.refresh();
|
|
app.page.removeFinishedNetworkTasks();
|
|
}, this))
|
|
.fail(function() {utils.showErrorDialog({title: $.t('dialog.discard_changes.cant_discard')});});
|
|
},
|
|
discardRoleChanges: function(e) {
|
|
e.preventDefault();
|
|
var data = {pending_roles: []};
|
|
if (this.node.get('pending_addition')) {
|
|
data.cluster = null;
|
|
data.pending_addition = false;
|
|
}
|
|
this.updateNode(data);
|
|
},
|
|
discardAddition: function(e) {
|
|
e.preventDefault();
|
|
this.updateNode({
|
|
cluster_id: null,
|
|
pending_addition: false,
|
|
pending_roles: []
|
|
});
|
|
},
|
|
discardDeletion: function(e) {
|
|
e.preventDefault();
|
|
this.updateNode({pending_deletion: false});
|
|
},
|
|
showNodeLogs: function() {
|
|
var status = this.node.get('status');
|
|
var error = this.node.get('error_type');
|
|
var options = {type: 'remote', node: this.node.id};
|
|
if (status == 'discover') {
|
|
options.source = 'bootstrap/messages';
|
|
} else if (status == 'provisioning' || status == 'provisioned' || (status == 'error' && error == 'provision')) {
|
|
options.source = 'install/anaconda';
|
|
} else if (status == 'deploying' || status == 'ready' || (status == 'error' && error == 'deploy')) {
|
|
options.source = 'install/puppet';
|
|
}
|
|
app.navigate('#cluster/' + this.screen.tab.model.id + '/logs/' + utils.serializeTabOptions(options), {trigger: true});
|
|
},
|
|
beforeTearDown: function() {
|
|
$('html').off(this.eventNamespace);
|
|
},
|
|
initialize: function(options) {
|
|
_.defaults(this, options);
|
|
this.screen = this.group.nodeList.screen;
|
|
this.eventNamespace = 'click.editnodename' + this.node.id;
|
|
this.node.on('change:checked', function(node, checked, options) {
|
|
this.screen.nodes.get(node.id).set('checked', checked);
|
|
}, this);
|
|
this.node.set('checked', this.screen instanceof this.screen.EditNodesScreen || this.screen.nodes.get(this.node.id).get('checked') || false);
|
|
this.node.on('change:status', this.calculateNodeState, this);
|
|
this.node.on('change:disabled', this.group.calculateSelectAllDisabledState, this.group);
|
|
if (!(this.screen instanceof this.screen.ClusterNodesScreen)) {
|
|
this.node.on('change:checked', this.screen.roles.handleChanges, this.screen.roles);
|
|
}
|
|
},
|
|
render: function() {
|
|
this.tearDownRegisteredSubViews();
|
|
this.$el.html(this.template(_.extend({
|
|
node: this.node,
|
|
renaming: this.renaming,
|
|
edit: this.screen instanceof this.screen.EditNodesScreen,
|
|
locked: this.screen.isLocked()
|
|
}, this.templateHelpers))).i18n();
|
|
this.stickit(this.node);
|
|
this.calculateNodeState();
|
|
return this;
|
|
}
|
|
});
|
|
|
|
return NodeListScreen;
|
|
});
|