diff --git a/src/components/CreateDeviceRow/CreateDeviceRow.js b/src/components/CreateDeviceRow/CreateDeviceRow.js index c09c6d5c5..2531d6bac 100644 --- a/src/components/CreateDeviceRow/CreateDeviceRow.js +++ b/src/components/CreateDeviceRow/CreateDeviceRow.js @@ -17,9 +17,9 @@ export class CreateDeviceRow extends React.Component { }; onFormChange = (newValue, key, valid) => { - this.props.onChange(newValue, key); + const changeValid = this.props.onChange(newValue, key, valid); this.setState({ - valid, + valid: typeof changeValid === 'boolean' ? changeValid : valid, }); }; @@ -46,5 +46,5 @@ CreateDeviceRow.propTypes = { onCancel: PropTypes.func.isRequired, LoadingComponent: PropTypes.func.isRequired, deviceFields: PropTypes.object.isRequired, - columnSizes: PropTypes.object.isRequired, + columnSizes: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired, }; diff --git a/src/components/CreateDeviceRow/tests/CreateDeviceRow.test.js b/src/components/CreateDeviceRow/tests/CreateDeviceRow.test.js index c401fbdd0..79778351b 100644 --- a/src/components/CreateDeviceRow/tests/CreateDeviceRow.test.js +++ b/src/components/CreateDeviceRow/tests/CreateDeviceRow.test.js @@ -1,9 +1,11 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { shallow, mount } from 'enzyme'; import { CreateDeviceRow } from '..'; import { default as CreateDeviceRowFixture } from '../fixtures/CreateDeviceRow.fixture'; +import { setInput } from '../../../tests/enzyme'; +import { Text } from '../../Form'; const testCreateDeviceRow = () => ; @@ -12,4 +14,17 @@ describe('', () => { const component = shallow(testCreateDeviceRow()); expect(component).toMatchSnapshot(); }); + + it('uses valid value from onChange', () => { + const onChange = value => value.value === 'truthyValue'; + const component = mount(); + + setInput(component.find('#device-name').find(Text), 'truthyValue'); + component.update(); + expect(component.state('valid')).toBeTruthy(); + + setInput(component.find('#device-name').find(Text), 'otherValue'); + component.update(); + expect(component.state('valid')).toBeFalsy(); + }); }); diff --git a/src/components/CreateNicRow/CreateNicRow.js b/src/components/CreateNicRow/CreateNicRow.js index 90416f285..c305e0661 100644 --- a/src/components/CreateNicRow/CreateNicRow.js +++ b/src/components/CreateNicRow/CreateNicRow.js @@ -4,21 +4,37 @@ import { get } from 'lodash'; import { CreateDeviceRow } from '../CreateDeviceRow'; import { getName, getNetworks, getInterfaces } from '../../selectors'; -import { validateDNS1123SubdomainValue } from '../../utils/validations'; +import { validateDNS1123SubdomainValue, validateNetworkBinding } from '../../utils/validations'; +import { getNetworkBindings } from '../../utils/utils'; import { POD_NETWORK } from '../../constants'; -import { HEADER_NAME, HEADER_MAC, SELECT_NETWORK } from '../Wizard/CreateVmWizard/strings'; +import { + HEADER_NAME, + HEADER_MAC, + SELECT_NETWORK, + HEADER_BINDING_METHOD, + SELECT_BINDING, +} from '../Wizard/CreateVmWizard/strings'; import { NETWORK_TYPE_POD, NETWORK_TYPE_MULTUS, NAME_KEY } from '../Wizard/CreateVmWizard/constants'; import { Loading } from '../Loading'; import { settingsValue } from '../../k8s/selectors'; import { DROPDOWN, CUSTOM, LABEL } from '../Form'; -const columnSizes = { +const mainColumnSize = { lg: 3, md: 3, sm: 3, xs: 3, }; +const otherColumnSize = { + lg: 2, + md: 2, + sm: 2, + xs: 2, +}; + +const columnSizes = [mainColumnSize, mainColumnSize, otherColumnSize, otherColumnSize, otherColumnSize]; + const getUsedNetworks = vm => { const interfaces = getInterfaces(vm); const networks = getNetworks(vm); @@ -61,6 +77,7 @@ const getNetworkChoices = (vm, networks) => { }; const getNicColumns = (nic, networks, LoadingComponent) => { + const bindingChoices = getNetworkBindings(get(settingsValue(nic, 'network'), 'networkType')); let network; if (networks) { const networkChoices = getNetworkChoices(nic.vm, networks); @@ -71,6 +88,7 @@ const getNicColumns = (nic, networks, LoadingComponent) => { choices: networkChoices, disabled: nic.creating || networkChoices.length === 0, required: true, + title: 'Network', }; } else { network = { @@ -94,6 +112,20 @@ const getNicColumns = (nic, networks, LoadingComponent) => { type: LABEL, }, network, + binding: { + id: 'binding', + type: DROPDOWN, + defaultValue: SELECT_BINDING, + title: HEADER_BINDING_METHOD, + required: true, + disabled: nic.creating, + choices: bindingChoices, + validate: settings => + validateNetworkBinding( + get(settingsValue(settings, 'network'), 'networkType'), + settingsValue(settings, 'binding') + ), + }, mac: { id: 'mac-address', title: HEADER_MAC, @@ -102,25 +134,44 @@ const getNicColumns = (nic, networks, LoadingComponent) => { }; }; -const onFormChange = (newValue, key, onChange) => { - if (key === 'network' && get(newValue, 'value.networkType') === NETWORK_TYPE_POD) { - // reset mac value - onChange({ value: '' }, 'mac'); +const onFormChange = (nic, formFields, newValue, key, valid, onChange) => { + let changeValid = valid; + if (key === 'network') { + const newNic = { + ...nic, + [key]: newValue, + }; + const validation = formFields.binding.validate(newNic); + if (validation && validation.message) { + changeValid = false; + validation.message = `Network ${validation.message}`; + } + onChange({ value: settingsValue(nic, 'binding'), validation }, 'binding'); + + if (get(newValue, 'value.networkType') === NETWORK_TYPE_POD) { + // reset mac value + onChange({ value: '' }, 'mac'); + } } + onChange(newValue, key); + return changeValid; }; -export const CreateNicRow = ({ nic, onAccept, onCancel, onChange, networks, LoadingComponent }) => ( - onFormChange(newValue, key, onChange)} - device={nic} - LoadingComponent={LoadingComponent} - columnSizes={columnSizes} - deviceFields={getNicColumns(nic, networks, LoadingComponent)} - /> -); +export const CreateNicRow = ({ nic, onAccept, onCancel, onChange, networks, LoadingComponent }) => { + const fields = getNicColumns(nic, networks, LoadingComponent); + return ( + onFormChange(nic, fields, newValue, key, valid, onChange)} + device={nic} + LoadingComponent={LoadingComponent} + columnSizes={columnSizes} + deviceFields={fields} + /> + ); +}; CreateNicRow.propTypes = { nic: PropTypes.object.isRequired, diff --git a/src/components/CreateNicRow/tests/__snapshots__/CreateNicRow.test.js.snap b/src/components/CreateNicRow/tests/__snapshots__/CreateNicRow.test.js.snap index e38fc4813..beac8353d 100644 --- a/src/components/CreateNicRow/tests/__snapshots__/CreateNicRow.test.js.snap +++ b/src/components/CreateNicRow/tests/__snapshots__/CreateNicRow.test.js.snap @@ -4,16 +4,56 @@ exports[` renders correctly 1`] = ` renders correctly 1`] = ` "disabled": false, "id": "network-type", "required": true, + "title": "Network", "type": "dropdown", }, } diff --git a/src/components/Form/FormFactory.js b/src/components/Form/FormFactory.js index 376901854..7a92643f0 100644 --- a/src/components/Form/FormFactory.js +++ b/src/components/Form/FormFactory.js @@ -270,7 +270,7 @@ export const ListFormFactory = ({ fields, fieldsValues, onFormChange, actions, c const form = formGroups.map((formGroup, index) => ( { const errors = Array(4).fill(null); @@ -106,6 +110,7 @@ const resolveInitialNetworks = (networks, networkConfigs, namespace, sourceType) editable: true, edit: false, networkType, + binding: getInterfaceBinding(templateNetwork.interface), }; } return { @@ -138,7 +143,7 @@ export class NetworksTab extends React.Component { publishResults = rows => { let valid = this.props.sourceType === PROVISION_SOURCE_PXE ? rows.some(row => row.isBootable) : true; const nics = rows.map( - ({ templateNetwork, rootNetwork, id, isBootable, name, mac, network, errors, networkType }) => { + ({ templateNetwork, rootNetwork, id, isBootable, name, mac, network, errors, networkType, binding }) => { const result = { id, isBootable, @@ -148,6 +153,7 @@ export class NetworksTab extends React.Component { errors, networkType, rootNetwork, + binding, }; if (templateNetwork) { @@ -206,6 +212,7 @@ export class NetworksTab extends React.Component { name: `nic${state.nextId - 1}`, mac: '', network: '', + binding: '', }, ], })); @@ -218,7 +225,7 @@ export class NetworksTab extends React.Component { label: HEADER_NAME, props: { style: { - width: '32%', + width: '24%', }, }, }, @@ -233,7 +240,7 @@ export class NetworksTab extends React.Component { label: HEADER_MAC, props: { style: { - width: '32%', + width: '24%', }, }, }, @@ -251,7 +258,7 @@ export class NetworksTab extends React.Component { label: HEADER_NETWORK, props: { style: { - width: '32%', + width: '24%', }, }, }, @@ -267,6 +274,23 @@ export class NetworksTab extends React.Component { initialValue: SELECT_NETWORK, }), }, + { + header: { + label: HEADER_BINDING_METHOD, + props: { + style: { + width: '24%', + }, + }, + }, + property: 'binding', + renderConfig: nic => ({ + id: 'binding-edit', + type: DROPDOWN, + choices: getNetworkBindings(nic.networkType), + initialValue: SELECT_BINDING, + }), + }, ]; if (!this.props.isCreateRemoveDisabled) { diff --git a/src/components/Wizard/CreateVmWizard/constants.js b/src/components/Wizard/CreateVmWizard/constants.js index 0c3941781..db82e2901 100644 --- a/src/components/Wizard/CreateVmWizard/constants.js +++ b/src/components/Wizard/CreateVmWizard/constants.js @@ -30,6 +30,9 @@ export const BATCH_CHANGES_KEY = 'internalBatchChanges'; // NetworksTab export const NETWORK_TYPE_MULTUS = 'multus'; export const NETWORK_TYPE_POD = 'pod'; +export const NETWORK_BINDING_MASQUERADE = 'masquerade'; +export const NETWORK_BINDING_BRIDGE = 'bridge'; +export const NETWORK_BINDING_SRIOV = 'sriov'; // StorageTab export const STORAGE_TYPE_PVC = 'pvc'; diff --git a/src/components/Wizard/CreateVmWizard/strings.js b/src/components/Wizard/CreateVmWizard/strings.js index 30651a5c4..87e6fd0d9 100644 --- a/src/components/Wizard/CreateVmWizard/strings.js +++ b/src/components/Wizard/CreateVmWizard/strings.js @@ -43,6 +43,7 @@ export const getVmwareOsString = osName => `Select matching for: ${osName}`; // NetworksTab export const SELECT_NETWORK = '--- Select Network Definition ---'; +export const SELECT_BINDING = '--- Select binding ---'; export const REMOVE_NIC_BUTTON = 'Remove NIC'; export const CREATE_NIC_BUTTON = 'Create NIC'; export const PXE_INFO = 'Pod network is not PXE bootable'; @@ -53,6 +54,7 @@ export const HEADER_MAC = 'MAC Address'; export const HEADER_NETWORK = 'Network Configuration'; export const ERROR_NETWORK_NOT_FOUND = 'Network config not found'; export const ERROR_NETWORK_NOT_SELECTED = 'Network config must be selected'; +export const HEADER_BINDING_METHOD = 'Binding method'; // StorageTab export const ERROR_NO_BOOTABLE_DISK = 'A bootable disk could not be found'; diff --git a/src/components/Wizard/CreateVmWizard/tests/__snapshots__/NetworksTab.test.js.snap b/src/components/Wizard/CreateVmWizard/tests/__snapshots__/NetworksTab.test.js.snap index 2895f8697..7b9bc780c 100644 --- a/src/components/Wizard/CreateVmWizard/tests/__snapshots__/NetworksTab.test.js.snap +++ b/src/components/Wizard/CreateVmWizard/tests/__snapshots__/NetworksTab.test.js.snap @@ -21,7 +21,7 @@ exports[` renders correctly 1`] = ` "label": "Name", "props": Object { "style": Object { - "width": "32%", + "width": "24%", }, }, }, @@ -33,7 +33,7 @@ exports[` renders correctly 1`] = ` "label": "MAC Address", "props": Object { "style": Object { - "width": "32%", + "width": "24%", }, }, }, @@ -45,13 +45,25 @@ exports[` renders correctly 1`] = ` "label": "Network Configuration", "props": Object { "style": Object { - "width": "32%", + "width": "24%", }, }, }, "property": "network", "renderConfig": [Function], }, + Object { + "header": Object { + "label": "Binding method", + "props": Object { + "style": Object { + "width": "24%", + }, + }, + }, + "property": "binding", + "renderConfig": [Function], + }, Object { "header": Object { "props": Object { diff --git a/src/k8s/vmBuilder.js b/src/k8s/vmBuilder.js index b18b9de32..b08c6c793 100644 --- a/src/k8s/vmBuilder.js +++ b/src/k8s/vmBuilder.js @@ -16,6 +16,9 @@ import { NETWORK_TYPE_POD, DATA_VOLUME_SOURCE_URL, DATA_VOLUME_SOURCE_BLANK, + NETWORK_BINDING_BRIDGE, + NETWORK_BINDING_MASQUERADE, + NETWORK_BINDING_SRIOV, } from '../components/Wizard/CreateVmWizard/constants'; import { getCloudInitVolume } from '../selectors'; @@ -95,6 +98,7 @@ export const addInterface = (vm, defaultInterface, network) => { ...(network.templateNetwork ? network.templateNetwork.interface : defaultInterface), name: network.name, }; + if (network.mac) { interfaceSpec.macAddress = network.mac; } @@ -104,10 +108,33 @@ export const addInterface = (vm, defaultInterface, network) => { interfaceSpec.bootOrder = interfaceSpec.bootOrder ? interfaceSpec.bootOrder : assignBootOrderIndex(vm); } + addBindingToInterface(interfaceSpec, network.binding); + const interfaces = getInterfaces(vm); interfaces.push(interfaceSpec); }; +export const addBindingToInterface = (interfaceSpec, binding) => { + delete interfaceSpec.bridge; + delete interfaceSpec.masquerade; + delete interfaceSpec.sriov; + + switch (binding) { + case NETWORK_BINDING_BRIDGE: + interfaceSpec.bridge = {}; + break; + case NETWORK_BINDING_MASQUERADE: + interfaceSpec.masquerade = {}; + break; + case NETWORK_BINDING_SRIOV: + interfaceSpec.sriov = {}; + break; + default: + interfaceSpec.bridge = {}; + break; + } +}; + export const addNetwork = (vm, network) => { const networkSpec = { ...(network.templateNetwork ? network.templateNetwork.network : {}), diff --git a/src/selectors/vm/selectors.js b/src/selectors/vm/selectors.js index 6a2525355..b55949f40 100644 --- a/src/selectors/vm/selectors.js +++ b/src/selectors/vm/selectors.js @@ -11,6 +11,11 @@ import { OS_WINDOWS_PREFIX, TEMPLATE_OS_NAME_ANNOTATION, } from '../../constants'; +import { + NETWORK_BINDING_BRIDGE, + NETWORK_BINDING_SRIOV, + NETWORK_BINDING_MASQUERADE, +} from '../../components/Wizard/CreateVmWizard/constants'; export const getDisks = vm => get(vm, 'spec.template.spec.domain.devices.disks', []); export const getInterfaces = vm => get(vm, 'spec.template.spec.domain.devices.interfaces', []); @@ -69,3 +74,16 @@ export const getFlavorDescription = vm => { export const isVmRunning = vm => get(vm, 'spec.running', false); export const isVmReady = vm => get(vm, 'status.ready', false); export const isVmCreated = vm => get(vm, 'status.created', false); + +export const getInterfaceBinding = intface => { + if (intface.bridge) { + return NETWORK_BINDING_BRIDGE; + } + if (intface.sriov) { + return NETWORK_BINDING_SRIOV; + } + if (intface.masquerade) { + return NETWORK_BINDING_MASQUERADE; + } + return null; +}; diff --git a/src/utils/patches.js b/src/utils/patches.js index e6f49baff..15940af74 100644 --- a/src/utils/patches.js +++ b/src/utils/patches.js @@ -12,7 +12,7 @@ import { DEVICE_TYPE_INTERFACE, } from '../constants'; import { NETWORK_TYPE_POD } from '../components/Wizard/CreateVmWizard/constants'; -import { assignBootOrderIndex, getBootableDevicesInOrder, getDevices } from '../k8s/vmBuilder'; +import { assignBootOrderIndex, getBootableDevicesInOrder, getDevices, addBindingToInterface } from '../k8s/vmBuilder'; export const getPxeBootPatch = vm => { const patches = []; @@ -315,13 +315,14 @@ export const getAddNicPatch = (vm, nic) => { const i = { name: nic.name, model: nic.model, - bridge: {}, bootOrder: assignBootOrderIndex(vm), }; if (nic.mac) { i.macAddress = nic.mac; } + addBindingToInterface(i, nic.binding); + const network = { name: nic.name, }; diff --git a/src/utils/tests/validation.test.js b/src/utils/tests/validation.test.js index db09378ff..c519e0137 100644 --- a/src/utils/tests/validation.test.js +++ b/src/utils/tests/validation.test.js @@ -5,6 +5,7 @@ import { validateContainer, getValidationObject, validateVmwareURL, + validateNetworkBinding, } from '../validations'; import { DNS1123_START_ERROR, @@ -17,6 +18,13 @@ import { END_WHITESPACE_ERROR, START_WHITESPACE_ERROR, } from '../strings'; +import { + NETWORK_TYPE_POD, + NETWORK_BINDING_BRIDGE, + NETWORK_BINDING_MASQUERADE, + NETWORK_BINDING_SRIOV, + NETWORK_TYPE_MULTUS, +} from '../../components/Wizard/CreateVmWizard/constants'; const validatesEmpty = validateFunction => { expect(validateFunction('')).toEqual(getValidationObject(EMPTY_ERROR)); @@ -125,3 +133,26 @@ describe('validation.js - validateVmwareURL', () => { expect(validateVmwareURL('http://hello.com ')).toEqual(getValidationObject(END_WHITESPACE_ERROR)); }); }); + +describe('validation.js - validateNetworkBinding', () => { + it('validate pod network type bindings', () => { + expect(validateNetworkBinding(NETWORK_TYPE_POD, NETWORK_BINDING_BRIDGE)).toBeNull(); + expect(validateNetworkBinding(NETWORK_TYPE_POD, NETWORK_BINDING_MASQUERADE)).toBeNull(); + expect(validateNetworkBinding(NETWORK_TYPE_POD, NETWORK_BINDING_SRIOV)).toBeNull(); + }); + it('validate multus network type bindings', () => { + expect(validateNetworkBinding(NETWORK_TYPE_MULTUS, NETWORK_BINDING_BRIDGE)).toBeNull(); + expect(validateNetworkBinding(NETWORK_TYPE_POD, NETWORK_BINDING_MASQUERADE)).toBeDefined(); + expect(validateNetworkBinding(NETWORK_TYPE_POD, NETWORK_BINDING_SRIOV)).toBeNull(); + }); + it('validate null network type bindings', () => { + expect(validateNetworkBinding(null, NETWORK_BINDING_BRIDGE)).toBeNull(); + expect(validateNetworkBinding(null, NETWORK_BINDING_MASQUERADE)).toBeNull(); + expect(validateNetworkBinding(null, NETWORK_BINDING_SRIOV)).toBeNull(); + }); + it('validate empty binding', () => { + expect(validateNetworkBinding(null, null)).toBeNull(); + expect(validateNetworkBinding(null, null)).toBeNull(); + expect(validateNetworkBinding(null, null)).toBeNull(); + }); +}); diff --git a/src/utils/utils.js b/src/utils/utils.js index 0364ef0d3..3b3b1639c 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -1,5 +1,13 @@ import { NamespaceModel, ProjectModel } from '../models'; +import { + NETWORK_TYPE_POD, + NETWORK_TYPE_MULTUS, + NETWORK_BINDING_BRIDGE, + NETWORK_BINDING_SRIOV, + NETWORK_BINDING_MASQUERADE, +} from '../components/Wizard/CreateVmWizard/constants'; + export function prefixedId(idPrefix, id) { return idPrefix && id ? `${idPrefix}-${id}` : null; } @@ -92,3 +100,13 @@ export const formatNetTraffic = (bytesPerSecond, preferredUnit, fixed = 2) => { formatted.unit = `${formatted.unit}ps`; return formatted; }; + +export const getNetworkBindings = networkType => { + switch (networkType) { + case NETWORK_TYPE_MULTUS: + return [NETWORK_BINDING_BRIDGE, NETWORK_BINDING_SRIOV]; + case NETWORK_TYPE_POD: + default: + return [NETWORK_BINDING_MASQUERADE, NETWORK_BINDING_BRIDGE, NETWORK_BINDING_SRIOV]; + } +}; diff --git a/src/utils/validations.js b/src/utils/validations.js index 26eac10dd..b27ece757 100644 --- a/src/utils/validations.js +++ b/src/utils/validations.js @@ -13,7 +13,7 @@ import { END_WHITESPACE_ERROR, } from './strings'; -import { parseUrl } from './utils'; +import { parseUrl, getNetworkBindings } from './utils'; import { VALIDATION_ERROR_TYPE } from '../constants'; @@ -107,3 +107,8 @@ export const validateVmwareURL = value => { */ return null; }; + +export const validateNetworkBinding = (networkType, networkBinding) => + networkBinding && !getNetworkBindings(networkType).includes(networkBinding) + ? getValidationObject(`${networkType} cannot have ${networkBinding} binding`) + : null;