Support for SRIOV on node interfaces

Implements: blueprint sr-iov-on-ui
Implements: blueprint support-sriov

Change-Id: I7ee194b088ad9c3256bad1fafb8c618260ef1e6f
This commit is contained in:
Alexandra Morozova 2016-03-14 16:29:30 +01:00
parent ff387ba3ea
commit ccefe38a47
5 changed files with 207 additions and 71 deletions

View File

@ -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};
}
});

View File

@ -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;

View File

@ -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.",

View File

@ -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) {

View File

@ -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>