1406 lines
52 KiB
JavaScript
1406 lines
52 KiB
JavaScript
/*
|
|
* 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.
|
|
**/
|
|
import $ from 'jquery';
|
|
import _ from 'underscore';
|
|
import Backbone from 'backbone';
|
|
import React from 'react';
|
|
import i18n from 'i18n';
|
|
import utils from 'utils';
|
|
import models from 'models';
|
|
import dispatcher from 'dispatcher';
|
|
import Expression from 'expression';
|
|
import OffloadingModes from 'views/cluster_page_tabs/nodes_tab_screens/offloading_modes_control';
|
|
import {Input, Tooltip} from 'views/controls';
|
|
import {backboneMixin, unsavedChangesMixin} from 'component_mixins';
|
|
import {DragSource, DropTarget} from 'react-dnd';
|
|
import ReactDOM from 'react-dom';
|
|
|
|
var ns = 'cluster_page.nodes_tab.configure_interfaces.';
|
|
|
|
var EditNodeInterfacesScreen = React.createClass({
|
|
mixins: [
|
|
backboneMixin('interfaces', 'change reset update'),
|
|
backboneMixin('cluster'),
|
|
backboneMixin('nodes', 'change reset update'),
|
|
unsavedChangesMixin
|
|
],
|
|
statics: {
|
|
fetchData(options) {
|
|
var cluster = options.cluster;
|
|
var nodes = utils.getNodeListFromTabOptions(options);
|
|
|
|
if (!nodes || !nodes.areInterfacesConfigurable()) {
|
|
return $.Deferred().reject();
|
|
}
|
|
|
|
var networkConfiguration = cluster.get('networkConfiguration');
|
|
var networksMetadata = new models.ReleaseNetworkProperties();
|
|
|
|
return $.when(...nodes.map((node) => {
|
|
node.interfaces = new models.Interfaces();
|
|
return node.interfaces.fetch({
|
|
url: _.result(node, 'url') + '/interfaces',
|
|
reset: true
|
|
});
|
|
}).concat([
|
|
networkConfiguration.fetch({cache: true}),
|
|
networksMetadata.fetch({
|
|
url: '/api/releases/' + cluster.get('release_id') + '/networks'
|
|
})]))
|
|
.then(() => {
|
|
var interfaces = new models.Interfaces();
|
|
interfaces.set(_.cloneDeep(nodes.at(0).interfaces.toJSON()), {parse: true});
|
|
return {
|
|
interfaces: interfaces,
|
|
nodes: nodes,
|
|
bondingConfig: networksMetadata.get('bonding'),
|
|
configModels: {
|
|
version: app.version,
|
|
cluster: cluster,
|
|
settings: cluster.get('settings')
|
|
}
|
|
};
|
|
});
|
|
}
|
|
},
|
|
getInitialState() {
|
|
return {
|
|
actionInProgress: false,
|
|
interfacesErrors: {}
|
|
};
|
|
},
|
|
componentWillMount() {
|
|
this.setState({
|
|
initialInterfaces: _.cloneDeep(this.interfacesToJSON(this.props.interfaces)),
|
|
limitations: this.getEditLimitations()
|
|
});
|
|
},
|
|
componentDidMount() {
|
|
this.validate();
|
|
},
|
|
compareInterfacesProperties(interfaces, path, iteratee = _.identity,
|
|
source = 'interface_properties') {
|
|
// Checks if all the sub parameters are equal for all interfaces property
|
|
var ifcProperties = _.map(interfaces.map((ifc) => {
|
|
var interfaceProperty = ifc.get(source);
|
|
return _.get(interfaceProperty, path, interfaceProperty);
|
|
}), iteratee);
|
|
var shown = _.first(ifcProperties);
|
|
var equal = !_.any(ifcProperties, (ifcProperty) => !_.isEqual(ifcProperty, shown));
|
|
|
|
return {equal, shown};
|
|
},
|
|
getEditLimitations() {
|
|
// Gets limitations for interfaces parameters editing.
|
|
// Parameter should not be editable if its differently available
|
|
// across the nodes interfaces
|
|
return this.props.interfaces.reduce((result, ifc, index) => {
|
|
var interfaces = this.props.nodes.map((node) => {
|
|
return node.interfaces.at(index);
|
|
});
|
|
var key = ifc.isBond() ? ifc.get('name') : ifc.id;
|
|
result[key] = {
|
|
offloading_modes: this.compareInterfacesProperties(
|
|
interfaces, '',
|
|
(value) => utils.deepOmit(value, ['state']),
|
|
'offloading_modes'
|
|
),
|
|
dpdk: this.compareInterfacesProperties(interfaces, 'dpdk.available'),
|
|
sriov: this.compareInterfacesProperties(interfaces, 'sriov.available'),
|
|
mtu: {equal: true, shown: true}
|
|
};
|
|
return result;
|
|
}, {});
|
|
},
|
|
isLocked() {
|
|
return !!this.props.cluster.task({group: 'deployment', active: true}) ||
|
|
!_.all(this.props.nodes.invoke('areInterfacesConfigurable'));
|
|
},
|
|
interfacesPickFromJSON(json) {
|
|
// Pick certain interface fields that have influence on hasChanges.
|
|
return _.pick(json, [
|
|
'assigned_networks', 'mode', 'type', 'slaves', 'bond_properties',
|
|
'interface_properties', 'offloading_modes'
|
|
]);
|
|
},
|
|
interfacesToJSON(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 : (json) => _.omit(json, 'state');
|
|
return interfaces.map((ifc) => picker(ifc.toJSON()));
|
|
},
|
|
hasChangesInRemainingNodes() {
|
|
var initialInterfacesData = _.map(this.state.initialInterfaces, this.interfacesPickFromJSON);
|
|
var limitationsKeys = this.props.nodes.at(0).interfaces.map(
|
|
(ifc) => ifc.get(ifc.isBond ? 'name' : 'id')
|
|
);
|
|
|
|
return _.any(this.props.nodes.slice(1), (node) => {
|
|
var interfacesData = this.interfacesToJSON(node.interfaces, true);
|
|
return _.any(initialInterfacesData, (ifcData, index) => {
|
|
var limitations = this.state.limitations[limitationsKeys[index]];
|
|
var omittedProperties = _.filter(
|
|
_.keys(limitations),
|
|
(key) => !_.get(limitations[key], 'equal', true)
|
|
);
|
|
return _.any(ifcData, (data, attribute) => {
|
|
// Restricted parameters should not participate in changes detection
|
|
switch (attribute) {
|
|
case 'offloading_modes': {
|
|
// Do not compare offloading modes if they differ
|
|
if (!_.get(limitations, 'offloading_modes.equal', false)) return false;
|
|
// otherwise remove set states before it
|
|
return !_.isEqual(..._.invoke(
|
|
[data, interfacesData[index][attribute]],
|
|
(value) => utils.deepOmit(value, ['state']))
|
|
);
|
|
}
|
|
case 'interface_properties': {
|
|
// Omit restricted parameters from the comparison
|
|
return !_.isEqual(..._.invoke(
|
|
[data, interfacesData[index][attribute]],
|
|
_.omit, omittedProperties)
|
|
);
|
|
}
|
|
case 'slaves': {
|
|
// bond 'slaves' attribute contains information about slave name only
|
|
// but interface names can be different between nodes
|
|
// and can not be used for the comparison
|
|
return data.length !== (interfacesData[index].slaves || {}).length;
|
|
}
|
|
}
|
|
return !_.isEqual(data, interfacesData[index][attribute]);
|
|
});
|
|
});
|
|
});
|
|
},
|
|
hasChanges() {
|
|
return !this.isLocked() &&
|
|
(!_.isEqual(this.state.initialInterfaces, this.interfacesToJSON(this.props.interfaces)) ||
|
|
this.props.nodes.length > 1 && this.hasChangesInRemainingNodes());
|
|
},
|
|
loadDefaults() {
|
|
this.setState({actionInProgress: true});
|
|
$.when(this.props.interfaces.fetch({
|
|
url: _.result(this.props.nodes.at(0), 'url') + '/interfaces/default_assignment', reset: true
|
|
}, this)).done(() => {
|
|
this.setState({actionInProgress: false});
|
|
}).fail((response) => {
|
|
var errorNS = ns + 'configuration_error.';
|
|
utils.showErrorDialog({
|
|
title: i18n(errorNS + 'title'),
|
|
message: i18n(errorNS + 'load_defaults_warning'),
|
|
response: response
|
|
});
|
|
});
|
|
},
|
|
revertChanges() {
|
|
this.props.interfaces.reset(_.cloneDeep(this.state.initialInterfaces), {parse: true});
|
|
},
|
|
updateWithLimitations(sourceInterface, targetInterface) {
|
|
// Interface parameters should be updated with respect to limitations:
|
|
// restricted parameters should not be changed
|
|
var limitations = this.state.limitations[targetInterface.id];
|
|
var targetInterfaceProperties = targetInterface.get('interface_properties');
|
|
var sourceInterfaceProperties = sourceInterface.get('interface_properties');
|
|
|
|
if (targetInterface.get('offloading_modes')
|
|
&& _.get(limitations, 'offloading_modes.equal', false)) {
|
|
targetInterface.set({
|
|
offloading_modes: sourceInterface.get('offloading_modes')
|
|
});
|
|
// If set of offloading modes supported is the same, disable_offloading
|
|
// parameters updated as well (it is probably obsolete)
|
|
var disableOffloading = _.get(sourceInterfaceProperties, 'disable_offloading');
|
|
if (!_.isUndefined(disableOffloading)) {
|
|
_.set(targetInterfaceProperties, 'disable_offloading', disableOffloading);
|
|
}
|
|
}
|
|
|
|
_.each(sourceInterfaceProperties, (propertyValue, propertyName) => {
|
|
// Set all unrestricted parameters values
|
|
if (!_.isPlainObject(propertyValue)
|
|
&& _.get(limitations, propertyName + '.equal', false)) {
|
|
_.set(targetInterfaceProperties, propertyName, propertyValue);
|
|
}
|
|
});
|
|
targetInterface.set({
|
|
interface_properties: sourceInterfaceProperties
|
|
});
|
|
},
|
|
applyChanges() {
|
|
if (!this.isSavingPossible()) return $.Deferred().reject();
|
|
|
|
var nodes = this.props.nodes;
|
|
var interfaces = this.props.interfaces;
|
|
var bond = interfaces.filter((ifc) => ifc.isBond());
|
|
var bondsByName = bond.reduce((result, bond) => {
|
|
result[bond.get('name')] = bond;
|
|
return result;
|
|
}, {});
|
|
|
|
// 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(bond,
|
|
(bond) => _.map(bond.get('slaves'), (slave) => interfaces.indexOf(interfaces.find(slave)))
|
|
);
|
|
|
|
this.setState({actionInProgress: true});
|
|
return $.when(...nodes.map((node) => {
|
|
var oldNodeBonds, nodeBonds;
|
|
// removing previously configured bond
|
|
oldNodeBonds = node.interfaces.filter((ifc) => ifc.isBond());
|
|
node.interfaces.remove(oldNodeBonds);
|
|
// creating node-specific bond without slaves
|
|
nodeBonds = _.map(bond, (bond) => {
|
|
return new models.Interface(_.omit(bond.toJSON(), 'slaves'), {parse: true});
|
|
});
|
|
node.interfaces.add(nodeBonds);
|
|
// determining slaves using bonding map
|
|
_.each(nodeBonds, (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 and interface properties
|
|
node.interfaces.each((ifc, index) => {
|
|
var updatedIfc = ifc.isBond() ? bondsByName[ifc.get('name')] : interfaces.at(index);
|
|
ifc.set({
|
|
assigned_networks: new models.InterfaceNetworks(
|
|
updatedIfc.get('assigned_networks').toJSON()
|
|
)
|
|
});
|
|
this.updateWithLimitations(updatedIfc, ifc);
|
|
});
|
|
|
|
return Backbone.sync('update', node.interfaces, {url: _.result(node, 'url') + '/interfaces'});
|
|
}))
|
|
.done(() => {
|
|
this.setState({initialInterfaces:
|
|
_.cloneDeep(this.interfacesToJSON(this.props.interfaces))});
|
|
dispatcher.trigger('networkConfigurationUpdated');
|
|
})
|
|
.fail((response) => {
|
|
var errorNS = ns + 'configuration_error.';
|
|
|
|
utils.showErrorDialog({
|
|
title: i18n(errorNS + 'title'),
|
|
message: i18n(errorNS + 'saving_warning'),
|
|
response: response
|
|
});
|
|
}).always(() => this.setState({actionInProgress: false}));
|
|
},
|
|
configurationTemplateExists() {
|
|
return !_.isEmpty(this.props.cluster.get('networkConfiguration')
|
|
.get('networking_parameters').get('configuration_template'));
|
|
},
|
|
getAvailableBondingTypes(ifc) {
|
|
if (ifc.isBond()) return [ifc.get('bond_properties').type__];
|
|
|
|
return _.compact(_.flatten(_.map(this.props.bondingConfig.availability,
|
|
(modesAvailability) => _.map(modesAvailability,
|
|
(condition, mode) => (new Expression(condition, this.props.configModels, {strict: false}))
|
|
.evaluate({interface: ifc}) && mode
|
|
)
|
|
)));
|
|
},
|
|
findOffloadingModesIntersection(set1, set2) {
|
|
return _.map(
|
|
_.intersection(
|
|
_.pluck(set1, 'name'),
|
|
_.pluck(set2, 'name')
|
|
),
|
|
(name) => {
|
|
return {
|
|
name: name,
|
|
state: null,
|
|
sub: this.findOffloadingModesIntersection(
|
|
_.find(set1, {name: name}).sub,
|
|
_.find(set2, {name: name}).sub
|
|
)
|
|
};
|
|
});
|
|
},
|
|
getIntersectedOffloadingModes(interfaces) {
|
|
var offloadingModes = interfaces.map((ifc) => ifc.get('offloading_modes') || []);
|
|
if (!offloadingModes.length) return [];
|
|
|
|
return offloadingModes.reduce((result, modes) => {
|
|
return this.findOffloadingModesIntersection(result, modes);
|
|
});
|
|
},
|
|
bondInterfaces(bondType) {
|
|
this.setState({actionInProgress: true});
|
|
var interfaces = this.props.interfaces.filter((ifc) => ifc.get('checked') && !ifc.isBond());
|
|
var bond = this.props.interfaces.find((ifc) => ifc.get('checked') && ifc.isBond());
|
|
var limitations = this.state.limitations;
|
|
var bondName;
|
|
|
|
if (!bond) {
|
|
// if no bond selected - create new one
|
|
var bondMode = _.flatten(
|
|
_.pluck(this.props.bondingConfig.properties[bondType].mode, 'values')
|
|
)[0];
|
|
bondName = this.props.interfaces.generateBondName('bond');
|
|
|
|
bond = new models.Interface({
|
|
type: 'bond',
|
|
name: bondName,
|
|
mode: bondMode,
|
|
assigned_networks: new models.InterfaceNetworks(),
|
|
slaves: _.invoke(interfaces, 'pick', 'name'),
|
|
bond_properties: {
|
|
mode: bondMode,
|
|
type__: bondType
|
|
},
|
|
interface_properties: {
|
|
mtu: null,
|
|
disable_offloading: true,
|
|
dpdk: {
|
|
enabled: !_.any(interfaces,
|
|
(ifc) => !ifc.get('interface_properties').dpdk.enabled
|
|
),
|
|
available: !_.any(interfaces,
|
|
(ifc) => !ifc.get('interface_properties').dpdk.available
|
|
)
|
|
}
|
|
},
|
|
offloading_modes: this.getIntersectedOffloadingModes(interfaces)
|
|
});
|
|
limitations[bondName] = {};
|
|
} else {
|
|
// adding interfaces to existing bond
|
|
var bondProperties = _.cloneDeep(bond.get('interface_properties'));
|
|
bondName = bond.get('name');
|
|
|
|
if (bondProperties.dpdk.enabled) {
|
|
bondProperties.dpdk.enabled = !_.any(interfaces,
|
|
(ifc) => !ifc.get('interface_properties').dpdk.enabled
|
|
);
|
|
}
|
|
bond.set({
|
|
slaves: bond.get('slaves').concat(_.invoke(interfaces, 'pick', 'name')),
|
|
offloading_modes: this.getIntersectedOffloadingModes(interfaces.concat(bond)),
|
|
interface_properties: bondProperties
|
|
});
|
|
// remove the bond to add it later and trigger re-rendering
|
|
this.props.interfaces.remove(bond, {silent: true});
|
|
}
|
|
limitations[bondName] = _.reduce(interfaces, (result, ifc) => {
|
|
bond.get('assigned_networks').add(ifc.get('assigned_networks').models);
|
|
ifc.get('assigned_networks').reset();
|
|
ifc.set({checked: false});
|
|
return this.mergeLimitations(result, limitations[ifc.id]);
|
|
}, limitations[bondName]);
|
|
|
|
this.props.interfaces.add(bond);
|
|
this.setState({
|
|
actionInProgress: false,
|
|
limitations: limitations
|
|
});
|
|
},
|
|
mergeLimitations(limitation1, limitation2) {
|
|
return _.merge(limitation1, limitation2, (value1, value2, interfaceProperty) => {
|
|
switch (interfaceProperty) {
|
|
case 'mtu':
|
|
case 'offloading_modes':
|
|
// Offloading modes are presumed to be calculated intersection
|
|
return {equal: true, shown: true};
|
|
case 'dpdk':
|
|
if (_.isUndefined(value1) || _.isUndefined(value2)) break;
|
|
|
|
// Both interfaces should support DPDK in order bond to support it either
|
|
var equal = true;
|
|
var shown = value1.shown && value2.shown;
|
|
return {equal: equal, shown: shown};
|
|
case 'sriov':
|
|
return {equal: true, shown: false};
|
|
}
|
|
});
|
|
},
|
|
unbondInterfaces() {
|
|
this.setState({actionInProgress: true});
|
|
_.each(this.props.interfaces.where({checked: true}), (bond) => {
|
|
this.removeInterfaceFromBond(bond.get('name'));
|
|
});
|
|
this.setState({actionInProgress: false});
|
|
},
|
|
removeInterfaceFromBond(bondName, slaveInterfaceName) {
|
|
var networks = this.props.cluster.get('networkConfiguration').get('networks');
|
|
var bond = this.props.interfaces.find({name: bondName});
|
|
var slaves = bond.get('slaves');
|
|
var bondHasUnmovableNetwork = bond.get('assigned_networks').any((interfaceNetwork) => {
|
|
return interfaceNetwork.getFullNetwork(networks).get('meta').unmovable;
|
|
});
|
|
var slaveInterfaceNames = _.pluck(slaves, 'name');
|
|
var targetInterface = bond;
|
|
|
|
// if PXE interface is being removed - place networks there
|
|
if (bondHasUnmovableNetwork) {
|
|
var pxeInterface = this.props.interfaces.find((ifc) => {
|
|
return ifc.get('pxe') && _.contains(slaveInterfaceNames, ifc.get('name'));
|
|
});
|
|
if (!slaveInterfaceName || pxeInterface && pxeInterface.get('name') === slaveInterfaceName) {
|
|
targetInterface = pxeInterface;
|
|
}
|
|
}
|
|
|
|
// if slaveInterfaceName is set - remove it from slaves, otherwise remove all
|
|
if (slaveInterfaceName) {
|
|
var slavesUpdated = _.reject(slaves, {name: slaveInterfaceName});
|
|
var names = _.pluck(slavesUpdated, 'name');
|
|
var bondSlaveInterfaces = this.props.interfaces.filter(
|
|
(ifc) => _.contains(names, ifc.get('name'))
|
|
);
|
|
|
|
bond.set({
|
|
slaves: slavesUpdated,
|
|
offloading_modes: this.getIntersectedOffloadingModes(bondSlaveInterfaces)
|
|
});
|
|
} else {
|
|
bond.set('slaves', []);
|
|
}
|
|
|
|
// destroy bond if all slave interfaces have been removed
|
|
if (!slaveInterfaceName && targetInterface === bond) {
|
|
targetInterface = this.props.interfaces.find({name: slaveInterfaceNames[0]});
|
|
}
|
|
|
|
// move networks if needed
|
|
if (targetInterface !== bond) {
|
|
var interfaceNetworks = bond.get('assigned_networks').remove(
|
|
bond.get('assigned_networks').models
|
|
);
|
|
targetInterface.get('assigned_networks').add(interfaceNetworks);
|
|
}
|
|
|
|
// if no slaves left - remove the bond
|
|
if (!bond.get('slaves').length) {
|
|
this.props.interfaces.remove(bond);
|
|
}
|
|
},
|
|
validate() {
|
|
var {interfaces, cluster} = this.props;
|
|
if (!interfaces) return;
|
|
|
|
var interfacesErrors = {};
|
|
var networkConfiguration = cluster.get('networkConfiguration');
|
|
var networkingParameters = networkConfiguration.get('networking_parameters');
|
|
var networks = networkConfiguration.get('networks');
|
|
var slaveInterfaceNames = _.pluck(_.flatten(_.filter(interfaces.pluck('slaves'))), 'name');
|
|
|
|
interfaces.each((ifc) => {
|
|
if (!_.contains(slaveInterfaceNames, ifc.get('name'))) {
|
|
var errors = ifc.validate({
|
|
networkingParameters: networkingParameters,
|
|
networks: networks
|
|
}, {cluster});
|
|
if (!_.isEmpty(errors)) interfacesErrors[ifc.get('name')] = errors;
|
|
}
|
|
});
|
|
|
|
if (!_.isEqual(this.state.interfacesErrors, interfacesErrors)) {
|
|
this.setState({interfacesErrors});
|
|
}
|
|
},
|
|
validateSpeedsForBonding(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;
|
|
},
|
|
isSavingPossible() {
|
|
return !_.chain(this.state.interfacesErrors).values().some().value() &&
|
|
!this.state.actionInProgress && this.hasChanges();
|
|
},
|
|
getInterfaceProperty(property) {
|
|
var {interfaces, nodes} = this.props;
|
|
var bondsCount = interfaces.filter((ifc) => ifc.isBond()).length;
|
|
var getPropertyValues = (ifcIndex) => {
|
|
return _.uniq(nodes.map((node) => {
|
|
var nodeBondsCount = node.interfaces.filter((ifc) => ifc.isBond()).length;
|
|
var nodeInterface = node.interfaces.at(ifcIndex + nodeBondsCount);
|
|
if (property === 'current_speed') return utils.showBandwidth(nodeInterface.get(property));
|
|
return nodeInterface.get(property);
|
|
}));
|
|
};
|
|
return interfaces.map((ifc, index) => {
|
|
if (ifc.isBond()) {
|
|
return _.map(ifc.get('slaves'),
|
|
(slave) => getPropertyValues(interfaces.indexOf(interfaces.find(slave)) - bondsCount)
|
|
);
|
|
}
|
|
return [getPropertyValues(index - bondsCount)];
|
|
});
|
|
},
|
|
render() {
|
|
var {nodes, interfaces} = this.props;
|
|
var nodeNames = nodes.pluck('name');
|
|
var locked = this.isLocked();
|
|
var configurationTemplateExists = this.configurationTemplateExists();
|
|
|
|
var checkedInterfaces = interfaces.filter((ifc) => ifc.get('checked') && !ifc.isBond());
|
|
var checkedBonds = interfaces.filter((ifc) => ifc.get('checked') && ifc.isBond());
|
|
|
|
var creatingNewBond = checkedInterfaces.length >= 2 && !checkedBonds.length;
|
|
var addingInterfacesToExistingBond = !!checkedInterfaces.length && checkedBonds.length === 1;
|
|
|
|
var availableBondingTypes = {};
|
|
interfaces.each((ifc) => {
|
|
availableBondingTypes[ifc.get('name')] = this.getAvailableBondingTypes(ifc);
|
|
});
|
|
var bondType = _.intersection(... _.compact(_.map(availableBondingTypes,
|
|
(types, ifcName) => interfaces.find({name: ifcName}).get('checked') ? types : null
|
|
)))[0];
|
|
var bondingPossible = (creatingNewBond || addingInterfacesToExistingBond) && !!bondType;
|
|
|
|
var unbondingPossible = !checkedInterfaces.length && !!checkedBonds.length;
|
|
|
|
var hasChanges = this.hasChanges();
|
|
var slaveInterfaceNames = _.pluck(_.flatten(_.filter(interfaces.pluck('slaves'))), 'name');
|
|
var loadDefaultsEnabled = !this.state.actionInProgress;
|
|
var revertChangesEnabled = !this.state.actionInProgress && hasChanges;
|
|
|
|
var invalidSpeedsForBonding = bondingPossible &&
|
|
this.validateSpeedsForBonding(checkedBonds.concat(checkedInterfaces)) ||
|
|
interfaces.any((ifc) => ifc.isBond() && this.validateSpeedsForBonding([ifc]));
|
|
|
|
var interfaceSpeeds = this.getInterfaceProperty('current_speed');
|
|
var interfaceNames = this.getInterfaceProperty('name');
|
|
|
|
return (
|
|
<div className='row'>
|
|
<div className='title'>
|
|
{i18n(ns + (locked ? 'read_only_' : '') + 'title',
|
|
{count: nodes.length, name: nodeNames.join(', ')})}
|
|
</div>
|
|
{configurationTemplateExists &&
|
|
<div className='col-xs-12'>
|
|
<div className='alert alert-warning'>
|
|
{i18n(ns + 'configuration_template_warning')}
|
|
</div>
|
|
</div>
|
|
}
|
|
{_.any(availableBondingTypes, (bondingTypes) => bondingTypes.length) &&
|
|
!configurationTemplateExists &&
|
|
!locked &&
|
|
<div className='col-xs-12'>
|
|
<div className='page-buttons'>
|
|
<div className='well clearfix'>
|
|
<div className='btn-group pull-right'>
|
|
<button
|
|
className='btn btn-default btn-bond'
|
|
onClick={() => this.bondInterfaces(bondType)}
|
|
disabled={!bondingPossible}
|
|
>
|
|
{i18n(ns + 'bond_button')}
|
|
</button>
|
|
<button
|
|
className='btn btn-default btn-unbond'
|
|
onClick={this.unbondInterfaces}
|
|
disabled={!unbondingPossible}
|
|
>
|
|
{i18n(ns + 'unbond_button')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{!bondingPossible && checkedInterfaces.concat(checkedBonds).length > 1 &&
|
|
<div className='alert alert-warning'>
|
|
{i18n(ns + (
|
|
checkedBonds.length > 1 ? 'several_bonds_warning' : 'interfaces_cannot_be_bonded'
|
|
))}
|
|
</div>
|
|
}
|
|
{invalidSpeedsForBonding &&
|
|
<div className='alert alert-warning'>{i18n(ns + 'bond_speed_warning')}</div>
|
|
}
|
|
</div>
|
|
}
|
|
<div className='ifc-list col-xs-12'>
|
|
{interfaces.map((ifc, index) => {
|
|
var ifcName = ifc.get('name');
|
|
var limitations = this.state.limitations[ifc.isBond() ? ifcName : ifc.id];
|
|
|
|
if (!_.contains(slaveInterfaceNames, ifcName)) {
|
|
return (
|
|
<NodeInterfaceDropTarget
|
|
{...this.props}
|
|
key={'interface-' + ifcName}
|
|
interface={ifc}
|
|
limitations={limitations}
|
|
hasChanges={
|
|
!_.isEqual(
|
|
_.findWhere(this.state.initialInterfaces, {name: ifcName}),
|
|
_.omit(ifc.toJSON(), 'state')
|
|
)
|
|
}
|
|
locked={locked}
|
|
configurationTemplateExists={configurationTemplateExists}
|
|
errors={this.state.interfacesErrors[ifcName]}
|
|
validate={this.validate}
|
|
removeInterfaceFromBond={this.removeInterfaceFromBond}
|
|
bondingProperties={this.props.bondingConfig.properties}
|
|
availableBondingTypes={availableBondingTypes[ifcName]}
|
|
getAvailableBondingTypes={this.getAvailableBondingTypes}
|
|
interfaceSpeeds={interfaceSpeeds[index]}
|
|
interfaceNames={interfaceNames[index]}
|
|
/>
|
|
);
|
|
}
|
|
})}
|
|
</div>
|
|
<div className='col-xs-12 page-buttons content-elements'>
|
|
<div className='well clearfix'>
|
|
<div className='btn-group'>
|
|
<a
|
|
className='btn btn-default'
|
|
href={'#cluster/' + this.props.cluster.id + '/nodes'}
|
|
disabled={this.state.actionInProgress}
|
|
>
|
|
{i18n('cluster_page.nodes_tab.back_to_nodes_button')}
|
|
</a>
|
|
</div>
|
|
{!locked &&
|
|
<div className='btn-group pull-right'>
|
|
<button
|
|
className='btn btn-default btn-defaults'
|
|
onClick={this.loadDefaults}
|
|
disabled={!loadDefaultsEnabled}
|
|
>
|
|
{i18n('common.load_defaults_button')}
|
|
</button>
|
|
<button
|
|
className='btn btn-default 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={!this.isSavingPossible()}
|
|
>
|
|
{i18n('common.apply_button')}
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var NodeInterface = React.createClass({
|
|
statics: {
|
|
target: {
|
|
drop(props, monitor) {
|
|
var targetInterface = props.interface;
|
|
var sourceInterface = props.interfaces.findWhere({name: monitor.getItem().interfaceName});
|
|
var network = sourceInterface.get('assigned_networks')
|
|
.findWhere({name: monitor.getItem().networkName});
|
|
sourceInterface.get('assigned_networks').remove(network);
|
|
targetInterface.get('assigned_networks').add(network);
|
|
// trigger 'change' event to update screen buttons state
|
|
targetInterface.trigger('change', targetInterface);
|
|
},
|
|
canDrop(props, monitor) {
|
|
return monitor.getItem().interfaceName !== props.interface.get('name');
|
|
}
|
|
},
|
|
collect(connect, monitor) {
|
|
return {
|
|
connectDropTarget: connect.dropTarget(),
|
|
isOver: monitor.isOver(),
|
|
canDrop: monitor.canDrop()
|
|
};
|
|
}
|
|
},
|
|
mixins: [
|
|
backboneMixin('interface'),
|
|
backboneMixin({
|
|
modelOrCollection(props) {
|
|
return props.interface.get('assigned_networks');
|
|
}
|
|
})
|
|
],
|
|
renderedIfcProperties: ['offloading_modes', 'mtu', 'sriov', 'dpdk'],
|
|
getInitialState() {
|
|
return {
|
|
activeInterfaceSectionName: null,
|
|
pendingToggle: false,
|
|
collapsed: true
|
|
};
|
|
},
|
|
isLacpRateAvailable() {
|
|
return _.contains(this.getBondPropertyValues('lacp_rate', 'for_modes'), this.getBondMode());
|
|
},
|
|
isHashPolicyNeeded() {
|
|
return _.contains(this.getBondPropertyValues('xmit_hash_policy', 'for_modes'),
|
|
this.getBondMode());
|
|
},
|
|
getBondMode() {
|
|
var ifc = this.props.interface;
|
|
return ifc.get('mode') || (ifc.get('bond_properties') || {}).mode;
|
|
},
|
|
getAvailableBondingModes() {
|
|
var {configModels, bondingProperties} = this.props;
|
|
var ifc = this.props.interface;
|
|
var bondType = ifc.get('bond_properties').type__;
|
|
var modes = bondingProperties[bondType].mode;
|
|
|
|
var availableModes = [];
|
|
var interfaces = ifc.isBond() ? ifc.getSlaveInterfaces() : [ifc];
|
|
_.each(interfaces, (ifc) => {
|
|
availableModes.push(_.reduce(modes, (result, modeSet) => {
|
|
if (
|
|
modeSet.condition &&
|
|
!(new Expression(modeSet.condition, configModels, {strict: false}))
|
|
.evaluate({interface: ifc})
|
|
) {
|
|
return result;
|
|
}
|
|
return result.concat(modeSet.values);
|
|
}, []));
|
|
});
|
|
return _.intersection(...availableModes);
|
|
},
|
|
getBondPropertyValues(propertyName, value) {
|
|
var bondType = this.props.interface.get('bond_properties').type__;
|
|
return _.flatten(_.pluck(this.props.bondingProperties[bondType][propertyName], value));
|
|
},
|
|
updateBondProperties(options) {
|
|
var bondProperties = _.cloneDeep(this.props.interface.get('bond_properties')) || {};
|
|
bondProperties = _.extend(bondProperties, options);
|
|
if (!this.isHashPolicyNeeded()) bondProperties = _.omit(bondProperties, 'xmit_hash_policy');
|
|
if (!this.isLacpRateAvailable()) bondProperties = _.omit(bondProperties, 'lacp_rate');
|
|
this.props.interface.set('bond_properties', bondProperties);
|
|
},
|
|
bondingChanged(name, value) {
|
|
this.props.interface.set({checked: value});
|
|
},
|
|
bondingModeChanged(name, value) {
|
|
this.props.interface.set({mode: value});
|
|
this.updateBondProperties({mode: value});
|
|
if (this.isHashPolicyNeeded()) {
|
|
this.updateBondProperties({xmit_hash_policy: this.getBondPropertyValues('xmit_hash_policy',
|
|
'values')[0]});
|
|
}
|
|
if (this.isLacpRateAvailable()) {
|
|
this.updateBondProperties({lacp_rate: this.getBondPropertyValues('lacp_rate', 'values')[0]});
|
|
}
|
|
},
|
|
onPolicyChange(name, value) {
|
|
this.updateBondProperties({xmit_hash_policy: value});
|
|
},
|
|
onLacpChange(name, value) {
|
|
this.updateBondProperties({lacp_rate: value});
|
|
},
|
|
getBondingOptions(bondingModes, attributeName) {
|
|
return _.map(bondingModes, (mode) => {
|
|
return (
|
|
<option key={'option-' + mode} value={mode}>
|
|
{i18n(ns + attributeName + '.' + mode.replace('.', '_'), {defaultValue: mode})}
|
|
</option>
|
|
);
|
|
});
|
|
},
|
|
toggleOffloading() {
|
|
var interfaceProperties = this.props.interface.get('interface_properties');
|
|
var name = 'disable_offloading';
|
|
this.onInterfacePropertiesChange(name, !interfaceProperties[name]);
|
|
},
|
|
makeOffloadingModesExcerpt() {
|
|
var states = {
|
|
true: i18n('common.enabled'),
|
|
false: i18n('common.disabled'),
|
|
null: i18n('cluster_page.nodes_tab.configure_interfaces.offloading_default')
|
|
};
|
|
var ifcModes = this.props.interface.get('offloading_modes');
|
|
|
|
if (!ifcModes.length) {
|
|
return states[!this.props.interface.get('interface_properties').disable_offloading];
|
|
}
|
|
if (_.uniq(_.pluck(ifcModes, 'state')).length === 1) {
|
|
return states[ifcModes[0].state];
|
|
}
|
|
|
|
var lastState;
|
|
var added = 0;
|
|
var excerpt = [];
|
|
_.each(ifcModes,
|
|
(mode) => {
|
|
if (!_.isNull(mode.state) && mode.state !== lastState) {
|
|
lastState = mode.state;
|
|
added++;
|
|
excerpt.push((added > 1 ? ',' : '') + mode.name + ' ' + states[mode.state]);
|
|
}
|
|
// show no more than two modes in the button
|
|
if (added === 2) return false;
|
|
}
|
|
);
|
|
if (added < ifcModes.length) excerpt.push(', ...');
|
|
return excerpt;
|
|
},
|
|
onInterfacePropertiesChange(name, value) {
|
|
function convertToNullIfNaN(value) {
|
|
var convertedValue = parseInt(value, 10);
|
|
return _.isNaN(convertedValue) ? null : convertedValue;
|
|
}
|
|
if (_.contains(['mtu', 'sriov.sriov_numvfs'], name)) {
|
|
value = convertToNullIfNaN(value);
|
|
}
|
|
var interfaceProperties = _.cloneDeep(this.props.interface.get('interface_properties') || {});
|
|
_.set(interfaceProperties, name, value);
|
|
this.props.interface.set('interface_properties', interfaceProperties);
|
|
},
|
|
renderLockTooltip(property) {
|
|
return <Tooltip key={property + '-unavailable'} text={i18n(ns + 'availability_tooltip')}>
|
|
<span className='glyphicon glyphicon-lock' aria-hidden='true'></span>
|
|
</Tooltip>;
|
|
},
|
|
renderConfigurableAttributes() {
|
|
var ifc = this.props.interface;
|
|
var limitations = this.props.limitations;
|
|
var ifcProperties = ifc.get('interface_properties');
|
|
var errors = (this.props.errors || {}).interface_properties;
|
|
var offloadingModes = ifc.get('offloading_modes') || [];
|
|
var {collapsed, activeInterfaceSectionName} = this.state;
|
|
var offloadingRestricted = !limitations.offloading_modes.equal;
|
|
var offloadingTabClasses = {
|
|
forbidden: offloadingRestricted,
|
|
'property-item-container': true,
|
|
active: !collapsed && activeInterfaceSectionName === this.renderedIfcProperties[0]
|
|
};
|
|
return (
|
|
<div className='properties-list'>
|
|
<span className={utils.classNames(offloadingTabClasses)}>
|
|
{offloadingRestricted && this.renderLockTooltip('offloading')}
|
|
{i18n(ns + 'offloading_modes') + ':'}
|
|
<button
|
|
className='btn btn-link property-item'
|
|
onClick={() => this.switchActiveSubtab(this.renderedIfcProperties[0])}
|
|
disabled={offloadingRestricted}
|
|
>
|
|
{offloadingRestricted ?
|
|
i18n(ns + 'different_availability')
|
|
:
|
|
offloadingModes.length ?
|
|
this.makeOffloadingModesExcerpt()
|
|
:
|
|
ifcProperties.disable_offloading ?
|
|
i18n(ns + 'disable_offloading')
|
|
:
|
|
i18n(ns + 'default_offloading')
|
|
}
|
|
</button>
|
|
</span>
|
|
{_.map(ifcProperties, (propertyValue, propertyName) => {
|
|
var {equal, shown} = _.get(
|
|
limitations, propertyName,
|
|
{equal: true, shown: true}
|
|
);
|
|
var propertyShown = !equal || shown;
|
|
|
|
if (_.isPlainObject(propertyValue) && !propertyShown) return null;
|
|
|
|
if (_.contains(this.renderedIfcProperties, propertyName)) {
|
|
var classes = {
|
|
'text-danger': _.has(errors, propertyName),
|
|
'property-item-container': true,
|
|
[propertyName]: true,
|
|
active: !collapsed && activeInterfaceSectionName === propertyName,
|
|
forbidden: !equal
|
|
};
|
|
var commonButtonProps = {
|
|
className: 'btn btn-link property-item',
|
|
onClick: () => this.switchActiveSubtab(propertyName)
|
|
};
|
|
//@TODO (morale): create some common component out of this
|
|
switch (propertyName) {
|
|
case 'sriov':
|
|
case 'dpdk':
|
|
return (
|
|
<span key={propertyName} className={utils.classNames(classes)}>
|
|
{!equal && this.renderLockTooltip(propertyName)}
|
|
{i18n(ns + propertyName) + ':'}
|
|
<button {...commonButtonProps} disabled={!equal}>
|
|
{equal ?
|
|
propertyValue.enabled ?
|
|
i18n('common.enabled')
|
|
:
|
|
i18n('common.disabled')
|
|
:
|
|
i18n(ns + 'different_availability')
|
|
|
|
}
|
|
</button>
|
|
</span>
|
|
);
|
|
default:
|
|
return (
|
|
<span key={propertyName} className={utils.classNames(classes)}>
|
|
{!equal && this.renderLockTooltip(propertyName)}
|
|
{i18n(ns + propertyName) + ':'}
|
|
<button {...commonButtonProps} disabled={!equal}>
|
|
{propertyValue || i18n(ns + propertyName + '_placeholder')}
|
|
</button>
|
|
</span>
|
|
);
|
|
}
|
|
}
|
|
})}
|
|
</div>
|
|
);
|
|
},
|
|
getInterfacePropertyError() {
|
|
return ((this.props.errors ||
|
|
{}).interface_properties || {})[this.state.activeInterfaceSectionName] || null;
|
|
},
|
|
renderInterfaceSubtab() {
|
|
var ifc = this.props.interface;
|
|
var offloadingModes = ifc.get('offloading_modes') || [];
|
|
var {locked} = this.props;
|
|
var ifcProperties = ifc.get('interface_properties') || null;
|
|
var errors = this.getInterfacePropertyError();
|
|
switch (this.state.activeInterfaceSectionName) {
|
|
case 'offloading_modes':
|
|
return (
|
|
<div>
|
|
{offloadingModes.length ?
|
|
<OffloadingModes interface={ifc} disabled={locked} />
|
|
:
|
|
<Input
|
|
type='checkbox'
|
|
label={i18n(ns + 'disable_offloading')}
|
|
checked={ifcProperties.disable_offloading}
|
|
name='disable_offloading'
|
|
onChange={this.toggleOffloading}
|
|
disabled={locked}
|
|
wrapperClassName='toggle-offloading'
|
|
/>
|
|
}
|
|
</div>
|
|
);
|
|
case 'mtu':
|
|
return (
|
|
<Input
|
|
type='number'
|
|
min={42}
|
|
max={65536}
|
|
label={i18n(ns + 'mtu')}
|
|
value={ifcProperties.mtu || ''}
|
|
placeholder={i18n(ns + 'mtu_placeholder')}
|
|
name='mtu'
|
|
onChange={this.onInterfacePropertiesChange}
|
|
disabled={locked}
|
|
wrapperClassName='pull-left mtu-control'
|
|
error={errors}
|
|
/>
|
|
);
|
|
case 'sriov':
|
|
return this.renderSRIOV(errors);
|
|
case 'dpdk':
|
|
return this.renderDPDK(errors);
|
|
}
|
|
},
|
|
changeBondType(newType) {
|
|
this.props.interface.set('bond_properties.type__', newType);
|
|
var newMode = _.flatten(
|
|
_.pluck(this.props.bondingProperties[newType].mode, 'values')
|
|
)[0];
|
|
this.bondingModeChanged(null, newMode);
|
|
},
|
|
renderDPDK(errors) {
|
|
var ifc = this.props.interface;
|
|
var currentDPDKValue = ifc.get('interface_properties').dpdk.enabled;
|
|
var isBond = ifc.isBond();
|
|
|
|
// check if DPDK can be switched
|
|
var newBondType = isBond ?
|
|
_.without(_.intersection(... _.compact(
|
|
_.map(ifc.getSlaveInterfaces(), (slave) => {
|
|
slave.get('interface_properties').dpdk.enabled = !currentDPDKValue;
|
|
var bondTypes = this.props.getAvailableBondingTypes(slave);
|
|
slave.get('interface_properties').dpdk.enabled = currentDPDKValue;
|
|
return bondTypes;
|
|
})
|
|
)), ifc.get('bond_properties').type__)[0]
|
|
:
|
|
null;
|
|
|
|
return (
|
|
<div className='dpdk-panel'>
|
|
<div className='description'>{i18n(ns + 'dpdk_description')}</div>
|
|
<Input
|
|
type='checkbox'
|
|
label={i18n('common.enabled')}
|
|
checked={currentDPDKValue}
|
|
name='dpdk.enabled'
|
|
onChange={(propertyName, propertyValue) => {
|
|
this.onInterfacePropertiesChange('dpdk.enabled', propertyValue);
|
|
if (isBond) this.changeBondType(newBondType);
|
|
}}
|
|
disabled={this.props.locked || isBond && !newBondType}
|
|
tooltipText={isBond && !newBondType && i18n(ns + 'locked_dpdk_bond')}
|
|
wrapperClassName='dpdk-control'
|
|
error={errors && errors.common}
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
renderSRIOV(errors) {
|
|
var ifc = this.props.interface;
|
|
var interfaceProperties = ifc.get('interface_properties');
|
|
var isSRIOVEnabled = interfaceProperties.sriov.enabled;
|
|
var physnet = interfaceProperties.sriov.physnet;
|
|
return (
|
|
<div className='sriov-panel'>
|
|
<div className='description'>{i18n(ns + 'sriov_description')}</div>
|
|
<Input
|
|
type='checkbox'
|
|
label={i18n('common.enabled')}
|
|
checked={isSRIOVEnabled}
|
|
name='sriov.enabled'
|
|
onChange={this.onInterfacePropertiesChange}
|
|
disabled={this.props.locked}
|
|
wrapperClassName='sriov-control'
|
|
error={errors && errors.common}
|
|
/>
|
|
{isSRIOVEnabled &&
|
|
[
|
|
<Input
|
|
key='sriov.sriov_numvfs'
|
|
type='number'
|
|
min={0}
|
|
max={interfaceProperties.sriov.sriov_totalvfs}
|
|
label={i18n(ns + 'virtual_functions')}
|
|
value={interfaceProperties.sriov.sriov_numvfs}
|
|
name='sriov.sriov_numvfs'
|
|
onChange={this.onInterfacePropertiesChange}
|
|
disabled={this.props.locked}
|
|
wrapperClassName='sriov-virtual-functions'
|
|
error={errors && errors.sriov_numvfs}
|
|
/>,
|
|
<Input
|
|
key='sriov.physnet'
|
|
type='text'
|
|
label={i18n(ns + 'physical_network')}
|
|
value={physnet}
|
|
name='sriov.physnet'
|
|
onChange={this.onInterfacePropertiesChange}
|
|
disabled={this.props.locked}
|
|
wrapperClassName='physnet'
|
|
error={errors && errors.physnet}
|
|
tooltipText={_.trim(physnet) && _.trim(physnet) !== 'physnet2' &&
|
|
i18n(ns + 'validation.non_default_physnet')
|
|
}
|
|
/>
|
|
]
|
|
}
|
|
</div>
|
|
);
|
|
},
|
|
componentDidMount() {
|
|
$(ReactDOM.findDOMNode(this.refs.properties))
|
|
.on('show.bs.collapse', () => this.setState({pendingToggle: false, collapsed: false}))
|
|
.on('hide.bs.collapse', () => this.setState({pendingToggle: false, collapsed: true}));
|
|
},
|
|
componentDidUpdate() {
|
|
this.props.validate();
|
|
if (this.state.pendingToggle) {
|
|
$(ReactDOM.findDOMNode(this.refs.properties)).collapse('toggle');
|
|
}
|
|
},
|
|
switchActiveSubtab(subTabName) {
|
|
var currentActiveTab = this.state.activeInterfaceSectionName;
|
|
this.setState({
|
|
pendingToggle: !currentActiveTab || currentActiveTab === subTabName || this.state.collapsed,
|
|
activeInterfaceSectionName: subTabName
|
|
});
|
|
},
|
|
renderInterfaceProperties() {
|
|
if (!this.props.interface.get('interface_properties')) return null;
|
|
var isConfigurationModeOn = !_.isNull(this.state.activeInterfaceSectionName);
|
|
var toggleConfigurationPanelClasses = utils.classNames({
|
|
'glyphicon glyphicon-menu-down': true,
|
|
rotate: !this.state.collapsed
|
|
});
|
|
var defaultSubtab = _.find(this.renderedIfcProperties, (ifcProperty) => {
|
|
var limitation = _.get(this.props.limitations, ifcProperty);
|
|
return limitation && limitation.equal && !!limitation.shown;
|
|
});
|
|
return (
|
|
<div className='ifc-properties clearfix forms-box'>
|
|
<div className='row'>
|
|
<div className='col-xs-11'>
|
|
{this.renderConfigurableAttributes()}
|
|
</div>
|
|
<div className='col-xs-1 toggle-configuration-control'>
|
|
<i
|
|
className={toggleConfigurationPanelClasses}
|
|
onClick={() => this.switchActiveSubtab(
|
|
isConfigurationModeOn ?
|
|
this.state.activeInterfaceSectionName :
|
|
defaultSubtab
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className='row configuration-panel collapse' ref='properties'>
|
|
<div className='col-xs-12 forms-box interface-sub-tab'>
|
|
{this.renderInterfaceSubtab()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
render() {
|
|
var ifc = this.props.interface;
|
|
var {cluster, locked, availableBondingTypes, configurationTemplateExists} = this.props;
|
|
var isBond = ifc.isBond();
|
|
var availableBondingModes = isBond ? this.getAvailableBondingModes() : [];
|
|
var networkConfiguration = cluster.get('networkConfiguration');
|
|
var networks = networkConfiguration.get('networks');
|
|
var networkingParameters = networkConfiguration.get('networking_parameters');
|
|
var slaveInterfaces = ifc.getSlaveInterfaces();
|
|
var assignedNetworks = ifc.get('assigned_networks');
|
|
var connectionStatusClasses = (slave) => {
|
|
var slaveDown = slave.get('state') === 'down';
|
|
return {
|
|
'ifc-connection-status': true,
|
|
'ifc-online': !slaveDown,
|
|
'ifc-offline': slaveDown
|
|
};
|
|
};
|
|
var bondProperties = ifc.get('bond_properties');
|
|
var bondingPossible = !!availableBondingTypes.length && !configurationTemplateExists && !locked;
|
|
var networkErrors = (this.props.errors || {}).network_errors;
|
|
return this.props.connectDropTarget(
|
|
<div className='ifc-container'>
|
|
<div
|
|
className={utils.classNames({
|
|
'ifc-inner-container': true,
|
|
nodrag: networkErrors,
|
|
over: this.props.isOver && this.props.canDrop,
|
|
'has-changes': this.props.hasChanges,
|
|
[ifc.get('name')]: true
|
|
})}
|
|
>
|
|
<div className='ifc-header clearfix forms-box'>
|
|
<div className={utils.classNames({
|
|
'common-ifc-name pull-left': true,
|
|
'no-checkbox': !bondingPossible
|
|
})}>
|
|
{bondingPossible ?
|
|
<Input
|
|
type='checkbox'
|
|
label={ifc.get('name')}
|
|
onChange={this.bondingChanged}
|
|
checked={ifc.get('checked')}
|
|
/>
|
|
:
|
|
ifc.get('name')
|
|
}
|
|
</div>
|
|
{isBond && [
|
|
<Input
|
|
key='bonding_mode'
|
|
type='select'
|
|
disabled={!bondingPossible}
|
|
onChange={this.bondingModeChanged}
|
|
value={this.getBondMode()}
|
|
label={i18n(ns + 'bonding_mode')}
|
|
children={this.getBondingOptions(availableBondingModes, 'bonding_modes')}
|
|
wrapperClassName='pull-right'
|
|
/>,
|
|
this.isHashPolicyNeeded() &&
|
|
<Input
|
|
key='bonding_policy'
|
|
type='select'
|
|
value={bondProperties.xmit_hash_policy}
|
|
disabled={!bondingPossible}
|
|
onChange={this.onPolicyChange}
|
|
label={i18n(ns + 'bonding_policy')}
|
|
children={this.getBondingOptions(
|
|
this.getBondPropertyValues('xmit_hash_policy', 'values'),
|
|
'hash_policy'
|
|
)}
|
|
wrapperClassName='pull-right'
|
|
/>,
|
|
this.isLacpRateAvailable() &&
|
|
<Input
|
|
key='lacp_rate'
|
|
type='select'
|
|
value={bondProperties.lacp_rate}
|
|
disabled={!bondingPossible}
|
|
onChange={this.onLacpChange}
|
|
label={i18n(ns + 'lacp_rate')}
|
|
children={this.getBondingOptions(
|
|
this.getBondPropertyValues('lacp_rate', 'values'),
|
|
'lacp_rates'
|
|
)}
|
|
wrapperClassName='pull-right'
|
|
/>
|
|
]}
|
|
</div>
|
|
<div className='networks-block'>
|
|
<div className='row'>
|
|
<div className='col-xs-3'>
|
|
<div className='pull-left'>
|
|
{_.map(slaveInterfaces, (slaveInterface, index) => {
|
|
return (
|
|
<div
|
|
key={'info-' + slaveInterface.get('name')}
|
|
className='ifc-info-block clearfix'
|
|
>
|
|
<div className='ifc-connection pull-left'>
|
|
<div
|
|
className={utils.classNames(connectionStatusClasses(slaveInterface))}
|
|
/>
|
|
</div>
|
|
<div className='ifc-info pull-left'>
|
|
{isBond &&
|
|
<div>
|
|
{i18n(ns + 'name')}:
|
|
{' '}
|
|
<span className='ifc-name'>{this.props.interfaceNames[index]}</span>
|
|
</div>
|
|
}
|
|
{this.props.nodes.length === 1 &&
|
|
<div>{i18n(ns + 'mac')}: {slaveInterface.get('mac')}</div>
|
|
}
|
|
<div>
|
|
{i18n(ns + 'speed')}: {this.props.interfaceSpeeds[index].join(', ')}
|
|
</div>
|
|
{(bondingPossible && slaveInterfaces.length >= 3) &&
|
|
<button
|
|
className='btn btn-link'
|
|
onClick={_.partial(
|
|
this.props.removeInterfaceFromBond,
|
|
ifc.get('name'), slaveInterface.get('name')
|
|
)}
|
|
>
|
|
{i18n('common.remove_button')}
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
<div className='col-xs-9'>
|
|
{!configurationTemplateExists &&
|
|
<div className='ifc-networks'>
|
|
{assignedNetworks.length ?
|
|
assignedNetworks.map((interfaceNetwork) => {
|
|
var network = interfaceNetwork.getFullNetwork(networks);
|
|
if (!network) return null;
|
|
return (
|
|
<DraggableNetwork
|
|
key={'network-' + network.id}
|
|
{... _.pick(this.props, ['locked', 'interface'])}
|
|
networkingParameters={networkingParameters}
|
|
interfaceNetwork={interfaceNetwork}
|
|
network={network}
|
|
/>
|
|
);
|
|
})
|
|
:
|
|
i18n(ns + 'drag_and_drop_description')
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
{networkErrors && !!networkErrors.length &&
|
|
<div className='ifc-error alert alert-danger'>
|
|
{networkErrors.join(', ')}
|
|
</div>
|
|
}
|
|
</div>
|
|
{this.renderInterfaceProperties()}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var NodeInterfaceDropTarget = DropTarget(
|
|
'network',
|
|
NodeInterface.target,
|
|
NodeInterface.collect
|
|
)(NodeInterface);
|
|
|
|
var Network = React.createClass({
|
|
statics: {
|
|
source: {
|
|
beginDrag(props) {
|
|
return {
|
|
networkName: props.network.get('name'),
|
|
interfaceName: props.interface.get('name')
|
|
};
|
|
},
|
|
canDrag(props) {
|
|
return !(props.locked || props.network.get('meta').unmovable);
|
|
}
|
|
},
|
|
collect(connect, monitor) {
|
|
return {
|
|
connectDragSource: connect.dragSource(),
|
|
isDragging: monitor.isDragging()
|
|
};
|
|
}
|
|
},
|
|
render() {
|
|
var network = this.props.network;
|
|
var interfaceNetwork = this.props.interfaceNetwork;
|
|
var networkingParameters = this.props.networkingParameters;
|
|
var classes = {
|
|
'network-block pull-left': true,
|
|
disabled: !this.constructor.source.canDrag(this.props),
|
|
dragging: this.props.isDragging
|
|
};
|
|
var vlanRange = network.getVlanRange(networkingParameters);
|
|
|
|
return this.props.connectDragSource(
|
|
<div className={utils.classNames(classes)}>
|
|
<div className='network-name'>
|
|
{i18n(
|
|
'network.' + interfaceNetwork.get('name'),
|
|
{defaultValue: interfaceNetwork.get('name')}
|
|
)}
|
|
</div>
|
|
{vlanRange &&
|
|
<div className='vlan-id'>
|
|
{i18n(ns + 'vlan_id', {count: _.uniq(vlanRange).length})}:
|
|
{_.uniq(vlanRange).join('-')}
|
|
</div>
|
|
}
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var DraggableNetwork = DragSource('network', Network.source, Network.collect)(Network);
|
|
|
|
export default EditNodeInterfacesScreen;
|