Merge "[React] Edit node interfaces screen"

This commit is contained in:
Jenkins 2015-02-12 12:15:28 +00:00 committed by Gerrit Code Review
commit 436e82b212
7 changed files with 637 additions and 424 deletions

View File

@ -2743,8 +2743,8 @@ hr.slim {
.physical-network-box {
display: block;
&.nodrag {
opacity: 0.7;
.network-box-item {
opacity: 0.7;
border-color: @fuel-dark-red;
background-color: #f2dede;
}
@ -2757,6 +2757,9 @@ hr.slim {
padding: 8px 15px;
label {
margin: 0;
.label-wrapper {
margin-top: 5px;
}
}
}
@ -2844,6 +2847,11 @@ hr.slim {
> b {
color: @font-color-base;
}
.parameter-name .label-wrapper {
min-width: 60px;
float: left;
margin-top: 0px;
}
select {
width: 160px;
margin-top: -4px;
@ -3833,3 +3841,7 @@ i.btn-cluster-details {
p { margin-bottom: 10px; }
ul { margin-bottom: 20px; }
}
.ui-sortable-placeholder {
display: none;
}

View File

@ -643,10 +643,10 @@ define([
return _.contains(slaveInterfaceNames, slaveInterface.get('name'));
});
},
validate: function() {
validate: function(attrs) {
var errors = [];
var networks = new models.Networks(this.get('assigned_networks').invoke('getFullNetwork'));
var untaggedNetworks = networks.filter(function(network) {return _.isNull(network.getVlanRange());});
var networks = new models.Networks(this.get('assigned_networks').invoke('getFullNetwork', attrs.networks));
var untaggedNetworks = networks.filter(function(network) { return _.isNull(network.getVlanRange(attrs.networkingParameters)); });
// public and floating networks are allowed to be assigned to the same interface
var maxUntaggedNetworksCount = networks.where({name: 'public'}).length && networks.where({name: 'floating'}).length ? 2 : 1;
if (untaggedNetworks.length > maxUntaggedNetworksCount) {
@ -674,7 +674,10 @@ define([
});
models.InterfaceNetwork = BaseModel.extend({
constructorName: 'InterfaceNetwork'
constructorName: 'InterfaceNetwork',
getFullNetwork: function(networks) {
return networks.findWhere({name: this.get('name')});
}
});
models.InterfaceNetworks = BaseCollection.extend({
@ -687,7 +690,15 @@ define([
});
models.Network = BaseModel.extend({
constructorName: 'Network'
constructorName: 'Network',
getVlanRange: function(networkingParameters) {
if (!this.get('meta').neutron_vlan_range) {
var externalNetworkData = this.get('meta').ext_net_data;
var vlanStart = externalNetworkData ? networkingParameters.get(externalNetworkData[0]) : this.get('vlan_start');
return _.isNull(vlanStart) ? vlanStart : [vlanStart, externalNetworkData ? vlanStart + networkingParameters.get(externalNetworkData[1]) - 1 : vlanStart];
}
return networkingParameters.get('vlan_range');
}
});
models.Networks = BaseCollection.extend({

View File

@ -23,7 +23,7 @@ define(
'jsx!views/cluster_page_tabs/nodes_tab_screens/add_nodes_screen',
'jsx!views/cluster_page_tabs/nodes_tab_screens/edit_nodes_screen',
'views/cluster_page_tabs/nodes_tab_screens/edit_node_disks_screen',
'views/cluster_page_tabs/nodes_tab_screens/edit_node_interfaces_screen'
'jsx!views/cluster_page_tabs/nodes_tab_screens/edit_node_interfaces_screen'
],
function($, _, React, BackboneViewWrapper, ClusterNodesScreen, AddNodesScreen, EditNodesScreen, EditNodeDisksScreen, EditNodeInterfacesScreen) {
'use strict';
@ -49,7 +49,7 @@ function($, _, React, BackboneViewWrapper, ClusterNodesScreen, AddNodesScreen, E
add: AddNodesScreen,
edit: EditNodesScreen,
disks: BackboneViewWrapper(EditNodeDisksScreen),
interfaces: BackboneViewWrapper(EditNodeInterfacesScreen)
interfaces: EditNodeInterfacesScreen
};
},
checkScreen: function(newScreen) {
@ -58,20 +58,22 @@ function($, _, React, BackboneViewWrapper, ClusterNodesScreen, AddNodesScreen, E
}
},
changeScreen: function(newScreen, screenOptions) {
var NewscreenComponent = this.getAvailableScreens()[newScreen];
if (!NewscreenComponent) return;
var NewScreenComponent = this.getAvailableScreens()[newScreen];
if (!NewScreenComponent) return;
var options = {cluster: this.props.cluster, screenOptions: screenOptions};
return (NewscreenComponent.fetchData ? NewscreenComponent.fetchData(options) : $.Deferred().resolve())
return (NewScreenComponent.fetchData ? NewScreenComponent.fetchData(options) : $.Deferred().resolve())
.done(_.bind(function(data) {
this.setState({
screen: newScreen,
screenOptions: screenOptions,
screenData: data
screenData: data || {}
});
}, this));
},
componentWillMount: function() {
this.checkScreen(this.props.tabOptions[0]);
var newScreen = this.props.tabOptions[0] || 'list';
this.checkScreen(newScreen);
this.changeScreen(newScreen, this.props.tabOptions.slice(1));
},
componentWillReceiveProps: function(newProps) {
var newScreen = newProps.tabOptions[0] || 'list';
@ -80,7 +82,7 @@ function($, _, React, BackboneViewWrapper, ClusterNodesScreen, AddNodesScreen, E
},
render: function() {
var Screen = this.getAvailableScreens()[this.state.screen];
if (!Screen) return null;
if (!Screen || !_.isObject(this.state.screenData)) return null;
return (
<ReactTransitionGroup component='div' className='wrapper' transitionName='screen'>
<ScreenTransitionWrapper key={this.state.screen}>

View File

@ -1,374 +0,0 @@
/*
* 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(
[
'jquery',
'underscore',
'i18n',
'backbone',
'utils',
'models',
'views/cluster_page_tabs/nodes_tab_screens/edit_node_screen',
'text!templates/cluster/edit_node_interfaces.html',
'text!templates/cluster/node_interface.html',
'jquery-ui'
],
function($, _, i18n, Backbone, utils, models, EditNodeScreen, editNodeInterfacesScreenTemplate, nodeInterfaceTemplate) {
'use strict';
var EditNodeInterfacesScreen, NodeInterface;
EditNodeInterfacesScreen = EditNodeScreen.extend({
className: 'edit-node-networks-screen',
constructorName: 'EditNodeInterfacesScreen',
template: _.template(editNodeInterfacesScreenTemplate),
events: {
'click .btn-bond:not(:disabled)': 'bondInterfaces',
'click .btn-unbond:not(:disabled)': 'unbondInterfaces',
'click .btn-defaults': 'loadDefaults',
'click .btn-revert-changes:not(:disabled)': 'revertChanges',
'click .btn-apply:not(:disabled)': 'applyChanges',
'click .btn-return:not(:disabled)': 'returnToNodeList'
},
initButtons: function() {
this.constructor.__super__.initButtons.apply(this);
this.bondInterfacesButton = new Backbone.Model({disabled: true});
this.unbondInterfacesButton = new Backbone.Model({disabled: true});
},
updateButtonsState: function(state) {
this.constructor.__super__.updateButtonsState.apply(this, arguments);
this.bondInterfacesButton.set('disabled', state);
this.unbondInterfacesButton.set('disabled', state);
},
setupButtonsBindings: function() {
this.constructor.__super__.setupButtonsBindings.apply(this);
var bindings = {attributes: [{name: 'disabled', observe: 'disabled'}]};
this.stickit(this.bondInterfacesButton, {'.btn-bond': bindings});
this.stickit(this.unbondInterfacesButton, {'.btn-unbond': bindings});
},
isLocked: function() {
var nodesAvailableForChanges = this.nodes.filter(function(node) {
return node.get('pending_addition') || node.get('status') == 'error';
});
return !nodesAvailableForChanges.length || this.constructor.__super__.isLocked.apply(this);
},
hasChanges: function() {
function getInterfaceConfiguration(interfaces) {
return _.map(interfaces.toJSON(), function(ifc) {
return _.pick(ifc, ['assigned_networks', 'type', 'mode', 'slaves']);
});
}
var currentConfiguration = getInterfaceConfiguration(this.interfaces);
return !this.nodes.reduce(function(result, node) {
return result && _.isEqual(currentConfiguration, getInterfaceConfiguration(node.interfaces));
}, true);
},
checkForChanges: function() {
this.updateButtonsState(this.isLocked() || !this.hasChanges());
this.loadDefaultsButton.set('disabled', this.isLocked());
this.updateBondingControlsState();
},
validateInterfacesSpeedsForBonding: function(interfaces) {
var slaveInterfaces = _.flatten(_.invoke(interfaces, 'getSlaveInterfaces'), true);
var speeds = _.invoke(slaveInterfaces, 'get', 'current_speed');
// warn if not all speeds are the same or there are interfaces with unknown speed
return _.uniq(speeds).length > 1 || !_.compact(speeds).length;
},
bondingAvailable: function() {
var isExperimental = _.contains(app.version.get('feature_groups'), 'experimental');
var iserDisabled = this.cluster.get('settings').get('storage.iser.value') != true;
var mellanoxSriovDisabled = this.cluster.get('settings').get('neutron_mellanox.plugin.value') != 'ethernet';
return !this.isLocked() && isExperimental && this.cluster.get('net_provider') == 'neutron' && iserDisabled && mellanoxSriovDisabled;
},
updateBondingControlsState: function() {
var checkedInterfaces = this.interfaces.filter(function(ifc) {return ifc.get('checked') && !ifc.isBond();});
var checkedBonds = this.interfaces.filter(function(ifc) {return ifc.get('checked') && ifc.isBond();});
var creatingNewBond = checkedInterfaces.length >= 2 && !checkedBonds.length;
var addingInterfacesToExistingBond = !!checkedInterfaces.length && checkedBonds.length == 1;
var bondingPossible = creatingNewBond || addingInterfacesToExistingBond;
var newBondWillHaveInvalidSpeeds = bondingPossible && this.validateInterfacesSpeedsForBonding(checkedBonds.concat(checkedInterfaces));
var existingBondHasInvalidSpeeds = !!this.interfaces.find(function(ifc) {
return ifc.isBond() && this.validateInterfacesSpeedsForBonding(ifc.getSlaveInterfaces());
}, this);
this.bondInterfacesButton.set('disabled', !bondingPossible);
this.unbondInterfacesButton.set('disabled', checkedInterfaces.length || !checkedBonds.length);
this.$('.bond-speed-warning').toggle(newBondWillHaveInvalidSpeeds || existingBondHasInvalidSpeeds);
},
bondInterfaces: function() {
var interfaces = this.interfaces.filter(function(ifc) {return ifc.get('checked') && !ifc.isBond();});
var bond = this.interfaces.find(function(ifc) {return ifc.get('checked') && ifc.isBond();});
if (!bond) {
// if no bond selected - create new one
bond = new models.Interface({
type: 'bond',
name: this.interfaces.generateBondName(),
mode: models.Interface.prototype.bondingModes[0],
assigned_networks: new models.InterfaceNetworks(),
slaves: _.invoke(interfaces, 'pick', 'name')
});
} else {
// adding interfaces to existing bond
bond.set({slaves: bond.get('slaves').concat(_.invoke(interfaces, 'pick', 'name'))});
// remove the bond to add it later and trigger re-rendering
this.interfaces.remove(bond, {silent: true});
}
_.each(interfaces, function(ifc) {
bond.get('assigned_networks').add(ifc.get('assigned_networks').models);
ifc.get('assigned_networks').reset();
ifc.set({checked: false});
});
this.interfaces.add(bond);
},
unbondInterfaces: function() {
_.each(this.interfaces.where({checked: true}), function(bond) {
// assign all networks from the bond to the first slave interface
var ifc = this.interfaces.findWhere({name: bond.get('slaves')[0].name});
ifc.get('assigned_networks').add(bond.get('assigned_networks').models);
bond.get('assigned_networks').reset();
bond.set({checked: false});
this.interfaces.remove(bond);
}, this);
},
loadDefaults: function() {
this.disableControls(true);
this.interfaces.fetch({url: _.result(this.nodes.at(0), 'url') + '/interfaces/default_assignment', reset: true})
.fail(_.bind(function() {
utils.showErrorDialog({
title: i18n('cluster_page.nodes_tab.configure_interfaces.configuration_error.title'),
message: i18n('cluster_page.nodes_tab.configure_interfaces.configuration_error.load_defaults_warning')
});
}, this));
},
revertChanges: function() {
this.interfaces.reset(_.cloneDeep(this.nodes.at(0).interfaces.toJSON()), {parse: true});
},
applyChanges: function() {
this.disableControls(true);
var bonds = this.interfaces.filter(function(ifc) {return ifc.isBond();});
// bonding map contains indexes of slave interfaces
// it is needed to build the same configuration for all the nodes
// as interface names might be different, so we use indexes
var bondingMap = _.map(bonds, function(bond) {
return _.map(bond.get('slaves'), function(slave) {
return this.interfaces.indexOf(this.interfaces.findWhere(slave));
}, this);
}, this);
return $.when.apply($, this.nodes.map(function(node) {
// removing previously configured bonds
var oldNodeBonds = node.interfaces.filter(function(ifc) {return ifc.isBond();});
node.interfaces.remove(oldNodeBonds);
// creating node-specific bonds without slaves
var nodeBonds = _.map(bonds, function(bond) {
return new models.Interface(_.omit(bond.toJSON(), 'slaves'), {parse: true});
}, this);
node.interfaces.add(nodeBonds);
// determining slaves using bonding map
_.each(nodeBonds, function(bond, bondIndex) {
var slaveIndexes = bondingMap[bondIndex];
var slaveInterfaces = _.map(slaveIndexes, node.interfaces.at, node.interfaces);
bond.set({slaves: _.invoke(slaveInterfaces, 'pick', 'name')});
});
// assigning networks according to user choice
node.interfaces.each(function(ifc, index) {
ifc.set({assigned_networks: new models.InterfaceNetworks(this.interfaces.at(index).get('assigned_networks').toJSON())});
}, this);
return Backbone.sync('update', node.interfaces, {url: _.result(node, 'url') + '/interfaces'});
}, this))
.done(function() {
app.page.removeFinishedNetworkTasks();
})
.always(_.bind(this.checkForChanges, this))
.fail(function() {
utils.showErrorDialog({
title: i18n('cluster_page.nodes_tab.configure_interfaces.configuration_error.title'),
message: i18n('cluster_page.nodes_tab.configure_interfaces.configuration_error.saving_warning')
});
});
},
initialize: function() {
this.constructor.__super__.initialize.apply(this, arguments);
if (this.nodes.length) {
this.cluster.on('change:status', function() {
this.revertChanges();
this.render();
this.checkForChanges();
}, this);
this.networkConfiguration = this.cluster.get('networkConfiguration');
this.loading = $.when.apply($, this.nodes.map(function(node) {
node.interfaces = new models.Interfaces();
node.interfaces.url = _.result(node, 'url') + '/interfaces';
return node.interfaces.fetch();
}, this).concat(this.networkConfiguration.fetch({cache: true})))
.done(_.bind(function() {
this.interfaces = new models.Interfaces(this.nodes.at(0).interfaces.toJSON(), {parse: true});
this.interfaces.on('reset add remove change:slaves', this.render, this);
this.interfaces.on('sync change:mode', this.checkForChanges, this);
this.interfaces.on('change:checked reset', this.updateBondingControlsState, this);
// FIXME: modifying prototype to easily access NetworkConfiguration cluster
// should be reimplemented in a less hacky way
var networks = this.networkConfiguration.get('networks');
models.InterfaceNetwork.prototype.getFullNetwork = function() {
return networks.findWhere({name: this.get('name')});
};
var networkingParameters = this.networkConfiguration.get('networking_parameters');
models.Network.prototype.getVlanRange = function() {
if (!this.get('meta').neutron_vlan_range) {
var externalNetworkData = this.get('meta').ext_net_data;
var vlanStart = externalNetworkData ? networkingParameters.get(externalNetworkData[0]) : this.get('vlan_start');
return _.isNull(vlanStart) ? vlanStart : [vlanStart, externalNetworkData ? vlanStart + networkingParameters.get(externalNetworkData[1]) - 1 : vlanStart];
}
return networkingParameters.get('vlan_range');
};
this.render();
}, this))
.fail(_.bind(this.goToNodeList, this));
} else {
this.goToNodeList();
}
this.initButtons();
},
beforeTearDown: function() {
delete models.InterfaceNetwork.prototype.getFullNetwork;
delete models.Network.prototype.getVlanRange;
},
renderInterfaces: function() {
this.tearDownRegisteredSubViews();
this.$('.node-networks').html('');
var slaveInterfaceNames = _.pluck(_.flatten(_.filter(this.interfaces.pluck('slaves'))), 'name');
this.interfaces.each(_.bind(function(ifc) {
if (!_.contains(slaveInterfaceNames, ifc.get('name'))) {
var nodeInterface = new NodeInterface({cluster: ifc, screen: this});
this.registerSubView(nodeInterface);
this.$('.node-networks').append(nodeInterface.render().el);
}
}, this));
// if any errors found disable apply button
_.each(this.interfaces.invoke('validate'), _.bind(function(interfaceValidationResult) {
if (!_.isEmpty(interfaceValidationResult)) {
this.applyChangesButton.set('disabled', true);
}
}, this));
},
render: function() {
this.$el.html(this.template({
nodes: this.nodes,
locked: this.isLocked(),
bondingAvailable: this.bondingAvailable()
})).i18n();
if (this.loading && this.loading.state() != 'pending') {
this.renderInterfaces();
this.checkForChanges();
}
this.setupButtonsBindings();
return this;
}
});
NodeInterface = Backbone.View.extend({
template: _.template(nodeInterfaceTemplate),
templateHelpers: _.pick(utils, 'showBandwidth'),
events: {
'sortremove .logical-network-box': 'dragStart',
'sortstart .logical-network-box': 'dragStart',
'sortreceive .logical-network-box': 'dragStop',
'sortstop .logical-network-box': 'dragStop',
'sortactivate .logical-network-box': 'dragActivate',
'sortdeactivate .logical-network-box': 'dragDeactivate',
'sortover .logical-network-box': 'updateDropTarget',
'click .btn-remove-interface': 'removeInterface'
},
bindings: {
'input[type=checkbox]': {
observe: 'checked'
},
'select[name=mode]': {
observe: 'mode',
selectOptions: {
collection: function() {
return _.map(models.Interface.prototype.bondingModes, function(mode) {
return {value: mode, label: i18n('cluster_page.nodes_tab.configure_interfaces.bonding_modes.' + mode, {defaultValue: mode})};
});
}
}
}
},
removeInterface: function(e) {
var slaveInterfaceId = parseInt($(e.currentTarget).data('interface-id'), 10);
var slaveInterface = this.screen.interfaces.get(slaveInterfaceId);
this.cluster.set('slaves', _.reject(this.cluster.get('slaves'), {name: slaveInterface.get('name')}));
},
dragStart: function(event, ui) {
var networkNames = $(ui.item).find('.logical-network-item').map(function(index, el) {
return $(el).data('name');
}).get();
this.screen.draggedNetworks = this.cluster.get('assigned_networks').filter(function(network) {
return _.contains(networkNames, network.get('name'));
});
if (event.type == 'sortstart') {
this.updateDropTarget();
} else if (event.type == 'sortremove') {
this.cluster.get('assigned_networks').remove(this.screen.draggedNetworks);
}
},
dragStop: function(event) {
if (event.type == 'sortreceive') {
this.cluster.get('assigned_networks').add(this.screen.draggedNetworks);
}
this.render();
this.screen.draggedNetworks = null;
},
updateDropTarget: function() {
this.screen.dropTarget = this;
},
checkIfEmpty: function() {
this.$('.network-help-message').toggle(!this.cluster.get('assigned_networks').length && !this.screen.isLocked());
},
initialize: function(options) {
_.defaults(this, options);
this.cluster.get('assigned_networks').on('add remove', this.checkIfEmpty, this);
this.cluster.get('assigned_networks').on('add remove', this.screen.checkForChanges, this.screen);
},
handleValidationErrors: function() {
var validationResult = this.cluster.validate();
if (validationResult.length) {
this.screen.applyChangesButton.set('disabled', true);
_.each(validationResult, _.bind(function(error) {
this.$('.physical-network-box[data-name=' + this.cluster.get('name') + ']')
.addClass('nodrag')
.next('.network-box-error-message').text(error);
}, this));
}
},
render: function() {
this.$el.html(this.template(_.extend({
ifc: this.cluster,
locked: this.screen.isLocked(),
bondingAvailable: this.screen.bondingAvailable()
}, this.templateHelpers))).i18n();
this.checkIfEmpty();
this.$('.logical-network-box').sortable({
connectWith: '.logical-network-box',
items: '.logical-network-group:not(.disabled)',
containment: this.screen.$('.node-networks'),
disabled: this.screen.isLocked()
}).disableSelection();
this.handleValidationErrors();
this.stickit(this.cluster);
return this;
}
});
return EditNodeInterfacesScreen;
});

View File

@ -0,0 +1,592 @@
/*
* Copyright 2015 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(
[
'jquery',
'underscore',
'backbone',
'react',
'i18n',
'utils',
'models',
'jsx!views/dialogs',
'jsx!views/controls',
'jsx!component_mixins',
'jquery-ui'
],
function($, _, Backbone, React, i18n, utils, models, dialogs, controls, ComponentMixins) {
'use strict';
var cx = React.addons.classSet,
ScreenMixin, EditNodeInterfacesScreen, NodeInterface;
ScreenMixin = {
goToNodeList: function() {
app.navigate('#cluster/' + this.props.cluster.get('id') + '/nodes', {trigger: true});
},
isLockedScreen: function() {
return this.props.cluster && !!this.props.cluster.tasks({group: 'deployment', status: 'running'}).length;
},
returnToNodeList: function() {
if (this.hasChanges()) {
utils.showDialog(dialogs.DiscardSettingsChangesDialog, {cb: _.bind(this.goToNodeList, this)});
} else {
this.goToNodeList();
}
}
};
EditNodeInterfacesScreen = React.createClass({
mixins: [
ScreenMixin,
ComponentMixins.backboneMixin('interfaces', 'change:checked change:slaves reset sync'),
ComponentMixins.backboneMixin('cluster', 'change:status change:networkConfiguration change:nodes sync'),
ComponentMixins.backboneMixin('nodes', 'change sync')
],
statics: {
fetchData: function(options) {
var cluster = options.cluster,
nodeIds = utils.deserializeTabOptions(options.screenOptions[0]).nodes.split(',').map(function(id) {return parseInt(id, 10);}),
nodeLoadingErrorNS = 'cluster_page.nodes_tab.configure_interfaces.node_loading_error.',
nodes,
networkConfiguration;
networkConfiguration = cluster.get('networkConfiguration');
nodes = new models.Nodes(cluster.get('nodes').getByIds(nodeIds));
if (nodes.length != nodeIds.length) {
utils.showErrorDialog({
title: i18n(nodeLoadingErrorNS + 'title'),
message: i18n(nodeLoadingErrorNS + 'load_error')
});
ScreenMixin.goToNodeList();
return;
}
return $.when.apply($, nodes.map(function(node) {
node.interfaces = new models.Interfaces();
return node.interfaces.fetch({
url: _.result(node, 'url') + '/interfaces',
reset: true
}, this);
}, this).concat([networkConfiguration.fetch({cache: true})]))
.then(_.bind(function() {
var interfaces = new models.Interfaces();
interfaces.set(_.cloneDeep(nodes.at(0).interfaces.toJSON()), {parse: true});
return {
interfaces: interfaces,
nodes: nodes
};
}, this));
}
},
getInitialState: function() {
return {
actionInProgress: false,
interfaceErrors: {}
};
},
componentWillMount: function() {
this.setState({initialInterfaces: this.interfacesToJSON(this.props.interfaces)});
},
componentDidMount: function() {
this.validate();
},
getDraggedNetworks: function() {
return this.draggedNetworks || null;
},
setDraggedNetworks: function(networks) {
this.draggedNetworks = networks;
},
interfacesPickFromJSON: function(json) {
// Pick certain interface fields that have influence on hasChanges.
return _.pick(json, ['assigned_networks', 'mode', 'type', 'slaves']);
},
interfacesToJSON: function(interfaces, remainingNodesMode) {
// Sometimes 'state' is sent from the API and sometimes not
// It's better to just unify all inputs to the one without state.
var picker = remainingNodesMode ? this.interfacesPickFromJSON : function(json) { return _.omit(json, 'state'); };
return interfaces.map(function(ifc) {
return picker(ifc.toJSON());
});
},
hasChangesInRemainingNodes: function() {
var initialInterfacesOmitted = _.map(this.state.initialInterfaces, this.interfacesPickFromJSON);
return _.any(this.props.nodes.slice(1), _.bind(function(node) {
return !_.isEqual(initialInterfacesOmitted, this.interfacesToJSON(node.interfaces, true));
}, this));
},
hasChanges: function() {
return !_.isEqual(this.state.initialInterfaces, this.interfacesToJSON(this.props.interfaces)) ||
this.hasChangesInRemainingNodes();
},
loadDefaults: function() {
this.setState({actionInProgress: true});
$.when(this.props.interfaces.fetch({
url: _.result(this.props.nodes.at(0), 'url') + '/interfaces/default_assignment', reset: true
}, this)).done(_.bind(function() {
this.setState({actionInProgress: false});
}, this)).fail(_.bind(function() {
var errorNS = 'cluster_page.nodes_tab.configure_interfaces.configuration_error.';
utils.showErrorDialog({
title: i18n(errorNS + 'title'),
message: i18n(errorNS + 'load_defaults_warning')
});
this.goToNodeList();
}, this));
},
revertChanges: function() {
this.props.interfaces.reset(_.cloneDeep(this.state.initialInterfaces), {parse: true});
},
applyChanges: function() {
var nodes = this.props.nodes,
interfaces = this.props.interfaces,
bonds = interfaces.filter(function(ifc) {return ifc.isBond();});
// bonding map contains indexes of slave interfaces
// it is needed to build the same configuration for all the nodes
// as interface names might be different, so we use indexes
var bondingMap = _.map(bonds, function(bond) {
return _.map(bond.get('slaves'), function(slave) {
return interfaces.indexOf(interfaces.findWhere(slave));
});
});
this.setState({actionInProgress: true});
return $.when.apply($, nodes.map(function(node) {
var oldNodeBonds, nodeBonds;
// removing previously configured bonds
oldNodeBonds = node.interfaces.filter(function(ifc) {return ifc.isBond();});
node.interfaces.remove(oldNodeBonds);
// creating node-specific bonds without slaves
nodeBonds = _.map(bonds, function(bond) {
return new models.Interface(_.omit(bond.toJSON(), 'slaves'), {parse: true});
}, this);
node.interfaces.add(nodeBonds);
// determining slaves using bonding map
_.each(nodeBonds, function(bond, bondIndex) {
var slaveIndexes = bondingMap[bondIndex],
slaveInterfaces = _.map(slaveIndexes, node.interfaces.at, node.interfaces);
bond.set({slaves: _.invoke(slaveInterfaces, 'pick', 'name')});
});
// Assigning networks according to user choice
node.interfaces.each(function(ifc, index) {
ifc.set({assigned_networks: new models.InterfaceNetworks(interfaces.at(index).get('assigned_networks').toJSON())});
}, this);
return Backbone.sync('update', node.interfaces, {url: _.result(node, 'url') + '/interfaces'});
}, this))
.done(_.bind(function() {
this.setState({initialInterfaces: this.interfacesToJSON(this.props.interfaces)});
app.page.removeFinishedNetworkTasks();
}, this))
.fail(function() {
var errorNS = 'cluster_page.nodes_tab.configure_interfaces.configuration_error.';
utils.showErrorDialog({
title: i18n(errorNS + 'title'),
message: i18n(errorNS + 'saving_warning')
});
}).always(_.bind(function() {
this.setState({actionInProgress: false});
}, this));
},
isLocked: function() {
var hasLockedNodes = this.props.nodes.any(function(node) {
return !(node.get('pending_addition') ||
node.get('status') == 'ready' ||
node.get('status') == 'error');
});
return hasLockedNodes || this.isLockedScreen();
},
bondingAvailable: function() {
var cluster = this.props.cluster,
isExperimental = _.contains(app.version.get('feature_groups'), 'experimental'),
iserDisabled = !cluster.get('settings').get('storage.iser.value'),
mellanoxSriovDisabled = cluster.get('settings').get('neutron_mellanox.plugin.value') != 'ethernet';
return !this.isLocked() && isExperimental && cluster.get('net_provider') == 'neutron' && iserDisabled && mellanoxSriovDisabled;
},
bondInterfaces: function() {
this.setState({actionInProgress: true});
var interfaces = this.props.interfaces.filter(function(ifc) {return ifc.get('checked') && !ifc.isBond();}),
bonds = this.props.interfaces.find(function(ifc) {return ifc.get('checked') && ifc.isBond();});
if (!bonds) {
// if no bond selected - create new one
bonds = new models.Interface({
type: 'bond',
name: this.props.interfaces.generateBondName(),
mode: models.Interface.prototype.bondingModes[0],
assigned_networks: new models.InterfaceNetworks(),
slaves: _.invoke(interfaces, 'pick', 'name')
});
} else {
// adding interfaces to existing bond
bonds.set({slaves: bonds.get('slaves').concat(_.invoke(interfaces, 'pick', 'name'))});
// remove the bond to add it later and trigger re-rendering
this.props.interfaces.remove(bonds, {silent: true});
}
_.each(interfaces, function(ifc) {
bonds.get('assigned_networks').add(ifc.get('assigned_networks').models);
ifc.get('assigned_networks').reset();
ifc.set({checked: false});
});
this.props.interfaces.add(bonds);
this.setState({actionInProgress: false});
},
unbondInterfaces: function() {
this.setState({actionInProgress: true});
_.each(this.props.interfaces.where({checked: true}), function(bond) {
// assign all networks from the bond to the first slave interface
var ifc = this.props.interfaces.findWhere({name: bond.get('slaves')[0].name});
ifc.get('assigned_networks').add(bond.get('assigned_networks').models);
bond.get('assigned_networks').reset();
bond.set({checked: false});
this.props.interfaces.remove(bond);
}, this);
this.setState({actionInProgress: false});
},
validate: function() {
var interfaceErrors = {},
validationResult,
networkConfiguration = this.props.cluster.get('networkConfiguration'),
networkingParameters = networkConfiguration.get('networking_parameters'),
networks = networkConfiguration.get('networks');
if (!this.props.interfaces) {
return;
}
this.props.interfaces.each(_.bind(function(ifc) {
validationResult = ifc.validate({
networkingParameters: networkingParameters,
networks: networks
});
if (validationResult.length) {
interfaceErrors[ifc.get('name')] = validationResult.join(' ');
}
}), this);
if (!_.isEqual(this.state.interfaceErrors, interfaceErrors)) {
this.setState({interfaceErrors: interfaceErrors});
}
},
refresh: function() {
this.forceUpdate();
},
render: function() {
var configureInterfacesTransNS = 'cluster_page.nodes_tab.configure_interfaces.',
nodes = this.props.nodes,
nodeNames = nodes.pluck('name'),
interfaces = this.props.interfaces,
locked = this.isLocked(),
bondingAvailable = this.bondingAvailable(),
checkedInterfaces = interfaces.filter(function(ifc) {return ifc.get('checked') && !ifc.isBond();}),
checkedBonds = interfaces.filter(function(ifc) {return ifc.get('checked') && ifc.isBond();}),
creatingNewBond = checkedInterfaces.length >= 2 && !checkedBonds.length,
addingInterfacesToExistingBond = !!checkedInterfaces.length && checkedBonds.length == 1,
bondingPossible = creatingNewBond || addingInterfacesToExistingBond,
unbondingPossible = !checkedInterfaces.length && !!checkedBonds.length,
hasChanges = this.hasChanges(),
hasErrors = _.chain(this.state.interfaceErrors).values().some().value(),
slaveInterfaceNames = _.pluck(_.flatten(_.filter(interfaces.pluck('slaves'))), 'name'),
returnEnabled = !this.state.actionInProgress,
loadDefaultsEnabled = !this.state.actionInProgress && !locked,
revertChangesEnabled = !this.state.actionInProgress && hasChanges,
applyEnabled = !hasErrors && !this.state.actionInProgress && hasChanges;
return (
<div className='edit-node-networks-screen' style={{display: 'block'}} ref='nodeNetworksScreen'>
<div className={cx({'edit-node-interfaces': true, 'changes-locked': locked})}>
<h3>
{i18n(configureInterfacesTransNS + 'title', {count: nodes.length, name: nodeNames.join(', ')})}
</h3>
</div>
<div className='row'>
<div className='page-control-box'>
<div className='page-control-button-placeholder'>
<button className='btn btn-bond' disabled={!bondingAvailable || !bondingPossible} onClick={this.bondInterfaces}>{i18n(configureInterfacesTransNS + 'bond_button')}</button>
<button className='btn btn-unbond' disabled={!bondingAvailable || !unbondingPossible} onClick={this.unbondInterfaces}>{i18n(configureInterfacesTransNS + 'unbond_button')}</button>
</div>
</div>
{bondingAvailable &&
<div className='bond-speed-warning alert hide'>{i18n(configureInterfacesTransNS + 'bond_speed_warning')}</div>
}
<div className='node-networks'>
{
interfaces.map(_.bind(function(ifc) {
if (!_.contains(slaveInterfaceNames, ifc.get('name'))) {
return <NodeInterface {...this.props}
key={'interface-' + ifc.get('name')}
interface={ifc}
locked={locked}
bondingAvailable={bondingAvailable}
getDraggedNetworks={this.getDraggedNetworks}
setDraggedNetworks={this.setDraggedNetworks}
errors={this.state.interfaceErrors[ifc.get('name')]}
validate={this.validate}
refresh={this.refresh}
/>;
}
}, this))
}
</div>
<div className='page-control-box'>
<div className='back-button pull-left'>
<button className='btn btn-return' onClick={this.returnToNodeList} disabled={!returnEnabled}>{i18n('cluster_page.nodes_tab.back_to_nodes_button')}</button>
</div>
<div className='page-control-button-placeholder'>
<button className='btn btn-defaults' onClick={this.loadDefaults} disabled={!loadDefaultsEnabled}>{i18n('common.load_defaults_button')}</button>
<button className='btn btn-revert-changes' onClick={this.revertChanges} disabled={!revertChangesEnabled}>{i18n('common.cancel_changes_button')}</button>
<button className='btn btn-success btn-apply' onClick={this.applyChanges} disabled={!applyEnabled}>{i18n('common.apply_button')}</button>
</div>
</div>
</div>
</div>
);
}
});
NodeInterface = React.createClass({
mixins: [
ComponentMixins.backboneMixin('cluster', 'change:status'),
ComponentMixins.backboneMixin('interface', 'change:checked change:mode'),
ComponentMixins.backboneMixin({
modelOrCollection: function(props) {
return props.interface.get('assigned_networks');
},
renderOn: 'add change remove'
})
],
propTypes: {
bondingAvailable: React.PropTypes.bool,
locked: React.PropTypes.bool,
refresh: React.PropTypes.func
},
onModelChange: function() {
this.props.refresh();
},
componentDidMount: function() {
$(this.refs.logicalNetworkBox.getDOMNode()).sortable({
connectWith: '.logical-network-box',
items: '.logical-network-group:not(.disabled)',
containment: $('.node-networks'),
disabled: this.props.locked,
receive: this.dragStop,
remove: this.dragStart,
start: this.dragStart,
stop: this.dragStop
}).disableSelection();
},
componentDidUpdate: function() {
this.props.validate();
},
dragStart: function(e, ui) {
var networkNames = $(ui.item).find('.logical-network-item').map(function(index, el) {
// NOTE(pkaminski): .data('name') returns an incorrect result here.
// This is probably caused by jQuery .data cache (attr reads directly from DOM).
// http://api.jquery.com/data/#data-html5
return $(el).attr('data-name');
});
if (e.type == 'sortstart') {
// NOTE(pkaminski): Save initial networks state -- this is used for blocking
// of dragging within one interface -- see also this.dragStop
this.initialNetworks = this.props.interface.get('assigned_networks').pluck('name');
}
if (e.type == 'sortremove') {
$(this.refs.logicalNetworkBox.getDOMNode()).sortable('cancel');
this.props.interface.get('assigned_networks').remove(this.props.getDraggedNetworks());
} else {
this.props.setDraggedNetworks(this.props.interface.get('assigned_networks').filter(function(network) {
return _.contains(networkNames, network.get('name'));
})[0]
);
}
},
dragStop: function(e) {
var networks;
if (e.type == 'sortreceive') {
this.props.interface.get('assigned_networks').add(this.props.getDraggedNetworks());
} else if (e.type == 'sortstop') {
// Block dragging within an interface
networks = this.props.interface.get('assigned_networks').pluck('name');
if (!_.xor(networks, this.initialNetworks).length) {
$(this.refs.logicalNetworkBox.getDOMNode()).sortable('cancel');
}
this.initialNetworks = [];
}
this.props.setDraggedNetworks(null);
},
bondingChanged: function(name, value) {
this.props.interface.set({checked: value});
},
bondingModeChanged: function(name, value) {
this.props.interface.set({mode: value});
},
bondingRemoveInterface: function(slaveName) {
var slaves = _.reject(this.props.interface.get('slaves'), {name: slaveName});
this.props.interface.set('slaves', slaves);
},
render: function() {
var configureInterfacesTransNS = 'cluster_page.nodes_tab.configure_interfaces.',
ifc = this.props.interface,
cluster = this.props.cluster,
locked = this.props.locked,
networkConfiguration = cluster.get('networkConfiguration'),
networks = networkConfiguration.get('networks'),
networkingParameters = networkConfiguration.get('networking_parameters'),
slaveInterfaces = ifc.getSlaveInterfaces(),
assignedNetworks = ifc.get('assigned_networks'),
bondable = this.props.bondingAvailable && assignedNetworks && !assignedNetworks.find(function(interfaceNetwork) {
return interfaceNetwork.getFullNetwork(networks).get('meta').unmovable;
}),
slaveOnlineClass = function(slave) {
var slaveDown = slave.get('state') == 'down';
return {
'interface-online': !slaveDown,
'interface-offline': slaveDown
};
},
assignedNetworksGrouped = [],
networksToAdd = [],
showHelpMessage = !locked && !assignedNetworks.length;
assignedNetworks.each(function(interfaceNetwork) {
if (interfaceNetwork.getFullNetwork(networks).get('name') != 'floating') {
if (networksToAdd.length) {
assignedNetworksGrouped.push(networksToAdd);
}
networksToAdd = [];
}
networksToAdd.push(interfaceNetwork);
});
if (networksToAdd.length) {
assignedNetworksGrouped.push(networksToAdd);
}
return (
<div className={cx({'physical-network-box': true, nodrag: this.props.errors})}>
<div className='network-box-item'>
{ifc.isBond() &&
<div className='network-box-name'>
{this.props.bondingAvailable ?
<controls.Input
type='checkbox'
label={ifc.get('name')}
labelClassName='pull-left'
onChange={this.bondingChanged}
checked={ifc.get('checked')} />
:
<div className='network-bond-name pull-left disabled'>{ifc.get('name')}</div>
}
<div className='network-bond-mode pull-right'>
<controls.Input
type='select'
disabled={!this.props.bondingAvailable}
onChange={this.bondingModeChanged}
value={ifc.get('mode')}
label={i18n(configureInterfacesTransNS + 'bonding_mode') + ':'}
children={_.map(models.Interface.prototype.bondingModes, function(mode) {
return <option key={'option-' + mode} value={mode}>{i18n(configureInterfacesTransNS + 'bonding_modes.' + mode)}</option>;
})} />
</div>
<div className='clearfix'></div>
</div>
}
<div className='physical-network-checkbox'>
{!ifc.isBond() && bondable && <controls.Input type='checkbox' onChange={this.bondingChanged} checked={ifc.get('checked')} />}
</div>
<div className='network-connections-block'>
{_.map(slaveInterfaces, function(slaveInterface) {
return <div key={'network-connections-slave-' + slaveInterface.get('name')} className='network-interfaces-status'>
<div className={cx(slaveOnlineClass(slaveInterface))}></div>
<div className='network-interfaces-name'>{slaveInterface.get('name')}</div>
</div>;
})
}
</div>
<div className='network-connections-info-block'>
{_.map(slaveInterfaces, function(slaveInterface) {
return <div key={'network-connections-info-' + slaveInterface.get('name')} className='network-connections-info-block-item'>
<div className='network-connections-info-position'></div>
<div className='network-connections-info-description'>
<div>{i18n(configureInterfacesTransNS + 'mac')}: {slaveInterface.get('mac')}</div>
<div>{i18n(configureInterfacesTransNS + 'speed')}:
{utils.showBandwidth(slaveInterface.get('current_speed'))}</div>
{(this.props.bondingAvailable && slaveInterfaces.length >= 3) &&
<button className='btn btn-link btn-remove-interface'
type='button'
onClick={this.bondingRemoveInterface.bind(this, slaveInterface.get('name'))}>{i18n('common.remove_button')}</button>
}
</div>
</div>;
}, this)
}
</div>
<div className='logical-network-box' ref='logicalNetworkBox'>
{!showHelpMessage ? _.map(assignedNetworksGrouped, function(networkGroup) {
var network = networkGroup[0].getFullNetwork(networks);
if (!network) {
return;
}
var classes = {
'logical-network-group': true,
disabled: locked || network.get('meta').unmovable
},
vlanRange = network.getVlanRange(networkingParameters);
return <div key={'network-box-' + network.get('id')} className={cx(classes)}>
{_.map(networkGroup, function(interfaceNetwork) {
return (
<div key={'interface-network-' + interfaceNetwork.get('name')}
className='logical-network-item' data-name={interfaceNetwork.get('name')}>
<div className='name'>{i18n('network.' + interfaceNetwork.get('name'), {defaultValue: interfaceNetwork.get('name')})}</div>
{vlanRange &&
<div className='id'>
{i18n(configureInterfacesTransNS + 'vlan_id', {count: _.uniq(vlanRange).length})}:
{_.uniq(vlanRange).join('-')}
</div>
}
</div>
);
})
}
</div>;
}, this)
: <div className='network-help-message'>{i18n(configureInterfacesTransNS + 'drag_and_drop_description')}</div>
}
</div>
</div>
{this.props.errors &&
<div className='network-box-error-message common enable-selection'>
{this.props.errors}
</div>
}
</div>
);
}
});
return EditNodeInterfacesScreen;
});

View File

@ -1,36 +0,0 @@
<div class="edit-node-interfaces <%= locked ? 'changes-locked' : '' %>">
<h3>
<%- i18n('cluster_page.nodes_tab.configure_interfaces.title', {count: nodes.length, name: nodes.length && nodes.at(0).get('name')}) %>
</h3>
</div>
<div class="row">
<% if (bondingAvailable) { %>
<div class="page-control-box">
<div class="page-control-button-placeholder">
<button class="btn btn-bond" data-i18n="cluster_page.nodes_tab.configure_interfaces.bond_button"></button>
<button class="btn btn-unbond" data-i18n="cluster_page.nodes_tab.configure_interfaces.unbond_button"></button>
</div>
</div>
<div class="bond-speed-warning alert hide" data-i18n="cluster_page.nodes_tab.configure_interfaces.bond_speed_warning"></div>
<% } %>
<div class="node-networks">
<div class="progress-bar">
<div class="progress progress-striped progress-success active"><div class="bar"></div></div>
</div>
</div>
<!-- page-control box -->
<div class="page-control-box">
<div class="back-button pull-left">
<button class="btn btn-return" data-i18n="cluster_page.nodes_tab.back_to_nodes_button"></button>
</div>
<div class="page-control-button-placeholder">
<button data-i18n="common.load_defaults_button" class="btn btn-defaults"></button>
<button data-i18n="common.cancel_changes_button" class="btn btn-revert-changes"></button>
<button data-i18n="common.apply_button" class="btn btn-success btn-apply"></button>
</div>
</div>
</div>

View File

@ -10,6 +10,7 @@
"apply_button": "Apply",
"rename_button": "Rename",
"delete_button": "Delete",
"remove_button": "Remove",
"stop_button": "Stop",
"reset_button": "Reset",
"save_settings_button": "Save Settings",
@ -241,6 +242,7 @@
"title_plural": "Configure interfaces on __count__ nodes",
"vlan_id": "VLAN ID",
"vlan_id_plural": "VLAN IDs",
"mac": "MAC",
"bond_button": "Bond Interfaces",
"unbond_button": "Unbond Interfaces",
"bond_speed_warning": "Bonding interfaces of different speeds is not recommended",
@ -257,6 +259,10 @@
"saving_warning": "Unable to apply changes",
"load_defaults_warning": "Unable to load default configuration"
},
"node_loading_error": {
"title": "Error loading nodes",
"load_error": "Some nodes failed to load"
},
"validation": {
"too_many_untagged_networks": "Untagged networks can not be assigned to the same interface"
}