Support for SRIOV on node interfaces
Implements: blueprint sr-iov-on-ui Implements: blueprint support-sriov Change-Id: I7ee194b088ad9c3256bad1fafb8c618260ef1e6f
This commit is contained in:
parent
ff387ba3ea
commit
ccefe38a47
|
@ -933,7 +933,8 @@ models.Interface = BaseModel.extend({
|
|||
});
|
||||
},
|
||||
validate(attrs) {
|
||||
var errors = [];
|
||||
var errors = {};
|
||||
var networkErrors = [];
|
||||
var networks = new models.Networks(this.get('assigned_networks')
|
||||
.invoke('getFullNetwork', attrs.networks));
|
||||
var untaggedNetworks = networks.filter((network) => {
|
||||
|
@ -944,23 +945,16 @@ models.Interface = BaseModel.extend({
|
|||
var maxUntaggedNetworksCount = networks.any({name: 'public'}) &&
|
||||
networks.any({name: 'floating'}) ? 2 : 1;
|
||||
if (untaggedNetworks.length > maxUntaggedNetworksCount) {
|
||||
errors.push(i18n(ns + 'too_many_untagged_networks'));
|
||||
networkErrors.push(i18n(ns + 'too_many_untagged_networks'));
|
||||
}
|
||||
var interfaceProperties = this.get('interface_properties');
|
||||
|
||||
if (interfaceProperties) {
|
||||
var ifcPropertiesErrors =
|
||||
this.validateInterfaceProperties(interfaceProperties);
|
||||
if (!_.isEmpty(ifcPropertiesErrors)) {
|
||||
errors.push({
|
||||
interface_properties: ifcPropertiesErrors
|
||||
});
|
||||
}
|
||||
}
|
||||
_.extend(errors, this.validateInterfaceProperties());
|
||||
|
||||
// check interface networks have the same vlan id
|
||||
var vlans = _.reject(networks.pluck('vlan_start'), _.isNull);
|
||||
if (_.uniq(vlans).length < vlans.length) errors.push(i18n(ns + 'networks_with_the_same_vlan'));
|
||||
if (_.uniq(vlans).length < vlans.length) {
|
||||
networkErrors.push(i18n(ns + 'networks_with_the_same_vlan'));
|
||||
}
|
||||
|
||||
// check interface network vlan ids included in Neutron L2 vlan range
|
||||
var vlanRanges = _.reject(networks.map(
|
||||
|
@ -973,20 +967,57 @@ models.Interface = BaseModel.extend({
|
|||
range[1] >= currentRange[0] && range[0] <= currentRange[1]
|
||||
)
|
||||
)
|
||||
) errors.push(i18n(ns + 'vlan_range_intersection'));
|
||||
) networkErrors.push(i18n(ns + 'vlan_range_intersection'));
|
||||
|
||||
if (this.shouldSRIOVBeValidated() &&
|
||||
networks.length &&
|
||||
attrs.networkingParameters.segmentation_type !== 'vlan') {
|
||||
networkErrors.push(i18n(ns + 'sriov_placement_error'));
|
||||
}
|
||||
|
||||
if (networkErrors.length) {
|
||||
errors.network_errors = networkErrors;
|
||||
}
|
||||
return errors;
|
||||
},
|
||||
validateInterfaceProperties(interfaceProperties) {
|
||||
validateInterfaceProperties() {
|
||||
var interfaceProperties = this.get('interface_properties');
|
||||
if (!interfaceProperties) return null;
|
||||
var errors = {};
|
||||
var ns = 'cluster_page.nodes_tab.configure_interfaces.validation.';
|
||||
var mtuValue = interfaceProperties.mtu;
|
||||
var mtuValue = parseInt(interfaceProperties.mtu, 10);
|
||||
if (mtuValue) {
|
||||
if (mtuValue < 42 || mtuValue > 65536) {
|
||||
if (_.isNaN(mtuValue) || mtuValue < 42 || mtuValue > 65536) {
|
||||
errors.mtu = i18n(ns + 'invalid_mtu');
|
||||
}
|
||||
}
|
||||
return _.isEmpty(errors) ? null : errors;
|
||||
_.extend(errors, this.validateSRIOV());
|
||||
return _.isEmpty(errors) ? null : {interface_properties: errors};
|
||||
},
|
||||
shouldSRIOVBeValidated() {
|
||||
var sriov = (this.get('interface_properties') || {}).sriov;
|
||||
return sriov && sriov.available && sriov.enabled && !this.isBond();
|
||||
},
|
||||
validateSRIOV() {
|
||||
if (!this.shouldSRIOVBeValidated()) return null;
|
||||
var sriov = (this.get('interface_properties') || {}).sriov;
|
||||
var ns = 'cluster_page.nodes_tab.configure_interfaces.validation.';
|
||||
var errors = {};
|
||||
var virtualFunctionsNumber = parseInt(sriov.sriov_numvfs, 10);
|
||||
if (virtualFunctionsNumber < 0 ||
|
||||
virtualFunctionsNumber > sriov.sriov_totalvfs ||
|
||||
_.isNaN(virtualFunctionsNumber)
|
||||
) {
|
||||
errors.sriov_numvfs = i18n(ns + 'invalid_virtual_functions_number',
|
||||
{max: sriov.sriov_totalvfs}
|
||||
);
|
||||
}
|
||||
if (sriov.physnet && !sriov.physnet.match(utils.regexes.networkName)) {
|
||||
errors.physnet = i18n(ns + 'invalid_physnet');
|
||||
} else if (!(_.trim(sriov.physnet) || '')) {
|
||||
errors.physnet = i18n(ns + 'empty_physnet');
|
||||
}
|
||||
return _.isEmpty(errors) ? null : {sriov: errors};
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -3569,12 +3569,13 @@ input[type=range] {
|
|||
}
|
||||
}
|
||||
.ifc-properties {
|
||||
@common-offset: 10px;
|
||||
border-top: 1px dotted @ifc-container-dark-color;
|
||||
padding: 10px 20px 10px 20px;
|
||||
padding: @common-offset @common-offset * 2 @common-offset;
|
||||
.checkbox-group {
|
||||
.custom-tumbler {
|
||||
float: right;
|
||||
margin: 0 10px 0 7px;
|
||||
margin: 0 @common-offset 0 7px;
|
||||
}
|
||||
}
|
||||
.toggle-offloading {
|
||||
|
@ -3590,23 +3591,50 @@ input[type=range] {
|
|||
}
|
||||
}
|
||||
.propety-item-container {
|
||||
margin-right: 10px;
|
||||
margin-right: @common-offset;
|
||||
.property-item {
|
||||
cursor: pointer;
|
||||
padding-top: 4px;
|
||||
padding-left: 0;
|
||||
margin-left: 10px;
|
||||
margin-left: @common-offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
.configuration-panel {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: @common-offset;
|
||||
}
|
||||
.mtu-control {
|
||||
div {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
.configuration-panel {
|
||||
margin-top: @common-offset;
|
||||
.interface-sub-tab {
|
||||
text-align: left;
|
||||
.has-error {
|
||||
.help-block {
|
||||
color: @red;
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
.description {
|
||||
font-size: @base-font-size - 2;
|
||||
color: @base-text-color + 35%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
.sriov-virtual-functions {
|
||||
margin-bottom: @common-offset;
|
||||
input {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.sriov-virtual-functions,
|
||||
.physnet {
|
||||
label {width: 200px;}
|
||||
}
|
||||
button.close {
|
||||
margin-right: 5px;
|
||||
margin-top: 5px;
|
||||
|
|
|
@ -54,7 +54,8 @@
|
|||
"neutron_tun": "Neutron with tunneling segmentation"
|
||||
},
|
||||
"count_label": "Count",
|
||||
"size_label": "Size"
|
||||
"size_label": "Size",
|
||||
"enabled": "Enabled"
|
||||
},
|
||||
"controls": {
|
||||
"file": {
|
||||
|
@ -423,7 +424,11 @@
|
|||
"too_many_untagged_networks": "Untagged networks cannot be assigned to the same interface.",
|
||||
"invalid_mtu": "Invalid MTU value",
|
||||
"networks_with_the_same_vlan": "Networks with the same VLAN ID cannot be assigned to the same interface.",
|
||||
"vlan_range_intersection": "The network VLAN ID range should not include VLAN IDs of other networks assigned to the interface."
|
||||
"vlan_range_intersection": "The network VLAN ID range should not include VLAN IDs of other networks assigned to the interface.",
|
||||
"sriov_placement_error": "No network could be placed on SRIOV enabled interface",
|
||||
"invalid_virtual_functions_number": "Virtual Functions number should be a positive number less than __max__",
|
||||
"invalid_physnet": "Invalid name",
|
||||
"empty_physnet": "Physical Network Name should not be empty"
|
||||
},
|
||||
"disable_offloading": "Offloading Disabled",
|
||||
"default_offloading": "Default Offloading",
|
||||
|
@ -433,7 +438,13 @@
|
|||
"offloading_disabled": "Disabled",
|
||||
"offloading_default": "Default",
|
||||
"mtu": "MTU",
|
||||
"mtu_placeholder": "Default"
|
||||
"mtu_placeholder": "Default",
|
||||
"sriov": "SR-IOV",
|
||||
"sriov_enabled": "Enabled",
|
||||
"sriov_disabled": "Disabled",
|
||||
"sriov_description": "Single root input/output virtualization - is a network interface that allows the isolation of the PCI Express resources for manageability and performance reasons",
|
||||
"virtual_functions": "Number of Virtual Functions",
|
||||
"physical_network": "Physical Network Name"
|
||||
},
|
||||
"configure_disks": {
|
||||
"no_disks": "No unassigned disks available.",
|
||||
|
|
|
@ -31,7 +31,8 @@ var utils = {
|
|||
url: /(?:https?:\/\/([\-\w\.]+)+(:\d+)?(\/([\w\/_\-\.]*(\?[\w\/_\-\.&%]*)?(#[\w\/_\-\.&%]*)?)?)?)/,
|
||||
ip: /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/,
|
||||
mac: /^([0-9a-f]{1,2}[\.:-]){5}([0-9a-f]{1,2})$/,
|
||||
cidr: /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/([1-9]|[1-2]\d|3[0-2])$/
|
||||
cidr: /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\/([1-9]|[1-2]\d|3[0-2])$/,
|
||||
networkName: /^[A-Za-z0-9 _]*[A-Za-z0-9][A-Za-z0-9 _]*$/
|
||||
},
|
||||
/*eslint-enable max-len*/
|
||||
serializeTabOptions(options) {
|
||||
|
|
|
@ -77,8 +77,7 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
getInitialState() {
|
||||
return {
|
||||
actionInProgress: false,
|
||||
interfaceNetworksErrors: {},
|
||||
interfacePropertiesErrors: {}
|
||||
interfacesErrors: {}
|
||||
};
|
||||
},
|
||||
componentWillMount() {
|
||||
|
@ -161,6 +160,7 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
var bondingMap = _.map(bonds,
|
||||
(bond) => _.map(bond.get('slaves'), (slave) => interfaces.indexOf(interfaces.find(slave)))
|
||||
);
|
||||
|
||||
this.setState({actionInProgress: true});
|
||||
return $.when(...nodes.map((node) => {
|
||||
var oldNodeBonds, nodeBonds;
|
||||
|
@ -365,8 +365,7 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
}
|
||||
},
|
||||
validate() {
|
||||
var interfaceNetworksErrors = {};
|
||||
var interfacePropertiesErrors = {};
|
||||
var interfacesErrors = {};
|
||||
var validationResult;
|
||||
var networkConfiguration = this.props.cluster.get('networkConfiguration');
|
||||
var networkingParameters = networkConfiguration.get('networking_parameters');
|
||||
|
@ -379,19 +378,14 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
networkingParameters: networkingParameters,
|
||||
networks: networks
|
||||
});
|
||||
if (validationResult.length) {
|
||||
interfacePropertiesErrors[ifc.get('name')] =
|
||||
_.compact(_.pluck(validationResult, 'interface_properties'))[0];
|
||||
validationResult = _.without(validationResult,
|
||||
_.find(validationResult, 'interface_properties'));
|
||||
interfaceNetworksErrors[ifc.get('name')] = validationResult.join(' ');
|
||||
|
||||
if (!_.isEmpty(validationResult)) {
|
||||
interfacesErrors[ifc.get('name')] = validationResult;
|
||||
}
|
||||
});
|
||||
if (!_.isEqual(this.state.interfaceNetworksErrors, interfaceNetworksErrors)) {
|
||||
this.setState({interfaceNetworksErrors: interfaceNetworksErrors});
|
||||
}
|
||||
if (!_.isEqual(this.state.interfacePropertiesErrors, interfacePropertiesErrors)) {
|
||||
this.setState({interfacePropertiesErrors: interfacePropertiesErrors});
|
||||
|
||||
if (!_.isEqual(this.state.interfacesErrors, interfacesErrors)) {
|
||||
this.setState({interfacesErrors});
|
||||
}
|
||||
},
|
||||
validateSpeedsForBonding(interfaces) {
|
||||
|
@ -401,8 +395,7 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
return _.uniq(speeds).length > 1 || !_.compact(speeds).length;
|
||||
},
|
||||
isSavingPossible() {
|
||||
return !_.chain(this.state.interfaceNetworksErrors).values().some().value() &&
|
||||
!_.chain(this.state.interfacePropertiesErrors).values().some().value() &&
|
||||
return !_.chain(this.state.interfacesErrors).values().some().value() &&
|
||||
!this.state.actionInProgress && this.hasChanges();
|
||||
},
|
||||
getIfcProperty(property) {
|
||||
|
@ -510,8 +503,7 @@ var EditNodeInterfacesScreen = React.createClass({
|
|||
locked={locked}
|
||||
bondingAvailable={bondingAvailable}
|
||||
configurationTemplateExists={configurationTemplateExists}
|
||||
errors={this.state.interfaceNetworksErrors[ifcName]}
|
||||
interfacePropertiesErrors={this.state.interfacePropertiesErrors[ifcName]}
|
||||
errors={this.state.interfacesErrors[ifcName]}
|
||||
validate={this.validate}
|
||||
removeInterfaceFromBond={this.removeInterfaceFromBond}
|
||||
bondingProperties={this.props.bondingConfig.properties}
|
||||
|
@ -599,7 +591,7 @@ var NodeInterface = React.createClass({
|
|||
}
|
||||
})
|
||||
],
|
||||
renderedIfcProperties: ['offloading_modes', 'mtu'],
|
||||
renderedIfcProperties: ['offloading_modes', 'mtu', 'sriov'],
|
||||
propTypes: {
|
||||
bondingAvailable: React.PropTypes.bool,
|
||||
locked: React.PropTypes.bool
|
||||
|
@ -722,21 +714,21 @@ var NodeInterface = React.createClass({
|
|||
var convertedValue = parseInt(value, 10);
|
||||
return _.isNaN(convertedValue) ? null : convertedValue;
|
||||
}
|
||||
if (name === 'mtu') {
|
||||
if (_.contains(['mtu', 'sriov.sriov_numvfs'], name)) {
|
||||
value = convertToNullIfNaN(value);
|
||||
}
|
||||
var interfaceProperties = _.cloneDeep(this.props.interface.get('interface_properties') || {});
|
||||
interfaceProperties[name] = value;
|
||||
_.set(interfaceProperties, name, value);
|
||||
this.props.interface.set('interface_properties', interfaceProperties);
|
||||
},
|
||||
renderConfigurableAttributes() {
|
||||
var ifc = this.props.interface;
|
||||
var ifcProperties = ifc.get('interface_properties');
|
||||
var errors = this.props.interfacePropertiesErrors;
|
||||
var errors = (this.props.errors || {}).interface_properties;
|
||||
var offloadingModes = ifc.get('offloading_modes') || [];
|
||||
return (
|
||||
<div className='properties-list'>
|
||||
<span className='propety-item-container'>
|
||||
<span className='property-item-container'>
|
||||
{i18n(ns + 'offloading_modes') + ':'}
|
||||
<button
|
||||
className='btn btn-link property-item'
|
||||
|
@ -753,36 +745,57 @@ var NodeInterface = React.createClass({
|
|||
</button>
|
||||
</span>
|
||||
{_.map(ifcProperties, (propertyValue, propertyName) => {
|
||||
if (_.isPlainObject(propertyValue) && !propertyValue.available) return null;
|
||||
if (_.contains(this.renderedIfcProperties, propertyName)) {
|
||||
var classes = {
|
||||
'text-danger': _.has(errors, propertyName),
|
||||
'property-item-container': true,
|
||||
[propertyName]: true
|
||||
};
|
||||
return (
|
||||
<span key={propertyName} className={utils.classNames(classes)}>
|
||||
{i18n(ns + propertyName) + ':'}
|
||||
<button
|
||||
className='btn btn-link property-item'
|
||||
onClick={() => this.switchActiveSubtab(propertyName)}
|
||||
>
|
||||
{propertyValue || i18n(ns + propertyName + '_placeholder')}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
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':
|
||||
return (
|
||||
<span key={propertyName} className={utils.classNames(classes)}>
|
||||
{i18n(ns + propertyName) + ':'}
|
||||
<button {...commonButtonProps}>
|
||||
{propertyValue.enabled ?
|
||||
i18n(ns + 'sriov_enabled')
|
||||
:
|
||||
i18n(ns + 'sriov_disabled')
|
||||
}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<span key={propertyName} className={utils.classNames(classes)}>
|
||||
{i18n(ns + propertyName) + ':'}
|
||||
<button {...commonButtonProps}>
|
||||
{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 = _.pick(
|
||||
this.props.interfacePropertiesErrors,
|
||||
this.state.activeInterfaceSectionName
|
||||
);
|
||||
var errors = this.getInterfacePropertyError();
|
||||
switch (this.state.activeInterfaceSectionName) {
|
||||
case 'offloading_modes':
|
||||
return (
|
||||
|
@ -815,11 +828,62 @@ var NodeInterface = React.createClass({
|
|||
onChange={this.onInterfacePropertiesChange}
|
||||
disabled={locked}
|
||||
wrapperClassName='pull-left mtu-control'
|
||||
error={errors && !_.isEmpty(errors) && _.values(errors).join(', ') || null}
|
||||
error={errors}
|
||||
/>
|
||||
);
|
||||
case 'sriov':
|
||||
return this.renderSRIOV(errors);
|
||||
}
|
||||
},
|
||||
renderSRIOV(errors) {
|
||||
var ifc = this.props.interface;
|
||||
var interfaceProperties = ifc.get('interface_properties');
|
||||
var isSRIOVEnabled = interfaceProperties.sriov.enabled;
|
||||
var locked = this.props.locked || !interfaceProperties.sriov.available;
|
||||
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={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={locked}
|
||||
wrapperClassName='sriov-virtual-functions'
|
||||
error={errors && errors.sriov_numvfs}
|
||||
/>,
|
||||
<Input
|
||||
key='sriov.physnet'
|
||||
type='text'
|
||||
label={i18n(ns + 'physical_network')}
|
||||
value={interfaceProperties.sriov.physnet || ''}
|
||||
name='sriov.physnet'
|
||||
onChange={this.onInterfacePropertiesChange}
|
||||
disabled={locked}
|
||||
wrapperClassName='physnet'
|
||||
error={errors && errors.physnet}
|
||||
/>
|
||||
]
|
||||
}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
switchActiveSubtab(subTabName) {
|
||||
var currentActiveTab = this.state.activeInterfaceSectionName;
|
||||
this.setState({
|
||||
|
@ -862,7 +926,7 @@ var NodeInterface = React.createClass({
|
|||
</div>
|
||||
{isConfigurationModeOn &&
|
||||
<div className='row configuration-panel'>
|
||||
<div className='col-xs-12'>
|
||||
<div className='col-xs-12 interface-sub-tab'>
|
||||
{this.renderInterfaceSubtab()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -889,11 +953,12 @@ var NodeInterface = React.createClass({
|
|||
};
|
||||
var bondProperties = ifc.get('bond_properties');
|
||||
var bondingPossible = this.props.bondingAvailable && !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: this.props.errors,
|
||||
nodrag: networkErrors,
|
||||
over: this.props.isOver && this.props.canDrop,
|
||||
'has-changes': this.props.hasChanges,
|
||||
[ifc.get('name')]: true
|
||||
|
@ -1041,9 +1106,9 @@ var NodeInterface = React.createClass({
|
|||
}
|
||||
</div>
|
||||
</div>
|
||||
{this.props.errors &&
|
||||
{networkErrors && !!networkErrors.length &&
|
||||
<div className='ifc-error alert alert-danger'>
|
||||
{this.props.errors}
|
||||
{networkErrors.join(', ')}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue