support multi nic

This commit is contained in:
wujun 2023-01-10 16:43:30 +08:00
parent 9d02902073
commit 5f59400e9e
8 changed files with 418 additions and 69 deletions

View File

@ -0,0 +1,40 @@
import Component from '@ember/component';
import layout from './template';
import { inject as service } from '@ember/service'
import { get, computed } from '@ember/object';
export default Component.extend({
scope: service(),
session: service(),
layout,
model: null,
idx: '',
disk: {},
disks: [],
bootOrderContent: computed('disk.bootOrder', 'disks.@each.bootOrder', 'idx', function() {
let bootOrderContent = [];
const bootOrder = get(this, 'disk').bootOrder;
const bootOrders = get(this, 'disks').map((disk) => {
return disk.bootOrder;
})
const disks = get(this, 'disks') || [];
const idx = get(this, 'idx');
for (let i = 0; i < disks.length + 1; i++) {
if (!bootOrders.includes(i) || i === 0 || i === bootOrder) {
if (!(idx === 0 && i === 0)) {
bootOrderContent.push({
label: i === 0 ? 'N/A' : `${ i }`,
value: i
});
}
}
}
return bootOrderContent
}),
});

View File

@ -0,0 +1,6 @@
<SearchableSelect
@class="form-control"
@content={{bootOrderContent}}
@value={{disk.bootOrder}}
@placeholder={{t "nodeDriver.harvester.storageClass.placeholder"}}
/>

View File

@ -48,22 +48,26 @@ export default Component.extend(NodeDriver, {
driverName: DRIVER, driverName: DRIVER,
model: {}, model: {},
currentCluster: null, currentCluster: null,
clusters: [], clusters: [],
clusterContent: [], clusterContent: [],
imageContent: [], imageContent: [],
networkContent: [], networkContent: [],
namespaceContent: [], namespaceContent: [],
nodes: [], nodes: [],
namespaces: [], namespaces: [],
nodeSchedulings: [], nodeSchedulings: [],
podSchedulings: [], podSchedulings: [],
networkDataContent: [], networkDataContent: [],
userDataContent: [], storageClassContent: [],
controller: null, defaultStorageClass: '',
signal: '', userDataContent: [],
isImportMode: true, controller: null,
loading: false, signal: '',
isImportMode: true,
loading: false,
disks: [],
interfaces: [],
config: alias(`model.${ CONFIG }`), config: alias(`model.${ CONFIG }`),
@ -78,6 +82,10 @@ export default Component.extend(NodeDriver, {
if (!!get(this, 'config.vmAffinity')) { if (!!get(this, 'config.vmAffinity')) {
this.initSchedulings(); this.initSchedulings();
} }
this.initDisks()
this.initInterfaces()
}, },
actions: { actions: {
@ -99,10 +107,47 @@ export default Component.extend(NodeDriver, {
this.get('nodeSchedulings').pushObject(neu); this.get('nodeSchedulings').pushObject(neu);
}, },
addVolume(type) {
let neu = {}
if (type === 'volume') {
neu = {
storageClassName: get(this, 'defaultStorageClass'),
size: 10,
bootOrder: 0
};
} else if (type === 'image') {
neu = {
imageName: '',
size: 40,
bootOrder: 0
};
}
this.get('disks').pushObject(neu);
},
addNetwork() {
const neu = {
networkName: '',
macAddress: ''
}
this.get('interfaces').pushObject(neu);
},
removeNodeScheduling(scheduling) { removeNodeScheduling(scheduling) {
this.get('nodeSchedulings').removeObject(scheduling); this.get('nodeSchedulings').removeObject(scheduling);
}, },
removeDisk(disk) {
this.get('disks').removeObject(disk);
},
removeNetwork(network) {
this.get('interfaces').removeObject(network);
},
updateNodeScheduling() { updateNodeScheduling() {
this.parseNodeScheduling(); this.parseNodeScheduling();
}, },
@ -124,7 +169,7 @@ export default Component.extend(NodeDriver, {
updatePodScheduling() { updatePodScheduling() {
this.parsePodScheduling(); this.parsePodScheduling();
} },
}, },
clearData: observer('currentCredential.id', function() { clearData: observer('currentCredential.id', function() {
@ -135,6 +180,31 @@ export default Component.extend(NodeDriver, {
set(this, 'podSchedulings', []); set(this, 'podSchedulings', []);
set(this, 'vmAffinity', {}); set(this, 'vmAffinity', {});
set(this, 'config.vmAffinity', ''); set(this, 'config.vmAffinity', '');
set(this, 'config.diskInfo', '');
set(this, 'config.networkInfo', '');
this.initDisks()
this.initInterfaces()
}),
setDiskInfo: observer('disks.@each.{imageName,bootOrder,storageClassName,size}', function() {
const diskInfo = {
disks: get(this, 'disks').map((disk) => {
return {
...disk,
size: Number(disk.size),
};
})
};
set(this, 'config.diskInfo', JSON.stringify(diskInfo));
}),
setNetworkInfo: observer('interfaces.@each.{networkName,macAddress}', function() {
const networkInfo = { interfaces: get(this, 'interfaces') };
set(this, 'config.networkInfo', JSON.stringify(networkInfo))
}), }),
nodeSchedulingsChanged: observer('nodeSchedulings.[]', function() { nodeSchedulingsChanged: observer('nodeSchedulings.[]', function() {
@ -256,10 +326,15 @@ export default Component.extend(NodeDriver, {
}) })
const storageClass = resp.storageClass.body.data || []; const storageClass = resp.storageClass.body.data || [];
const storageClassContent = storageClass.map((s) => { let defaultStorageClass = '';
const storageClassContent = storageClass.filter((s) => !s.parameters?.backingImage).map((s) => {
const isDefault = s.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true'; const isDefault = s.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true';
const label = isDefault ? `${ s.metadata.name } (${ this.intl.t('generic.default') })` : s.metadata.name; const label = isDefault ? `${ s.metadata.name } (${ this.intl.t('generic.default') })` : s.metadata.name;
if (isDefault) {
defaultStorageClass = s.metadata.name;
}
return { return {
label, label,
value: s.metadata.name, value: s.metadata.name,
@ -272,7 +347,8 @@ export default Component.extend(NodeDriver, {
namespaceContent, namespaceContent,
userDataContent, userDataContent,
networkDataContent, networkDataContent,
storageClassContent storageClassContent,
defaultStorageClass
}); });
}).catch((err) => { }).catch((err) => {
setProperties(this, { setProperties(this, {
@ -339,7 +415,9 @@ export default Component.extend(NodeDriver, {
networkData: '', networkData: '',
vmNamespace: '', vmNamespace: '',
userData: '', userData: '',
vmAffinity: '' vmAffinity: '',
diskInfo: '',
networkInfo: ''
}); });
set(this, `model.${ CONFIG }`, config); set(this, `model.${ CONFIG }`, config);
@ -357,24 +435,14 @@ export default Component.extend(NodeDriver, {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.namespace.label') })); errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.namespace.label') }));
} }
if (!get(this, 'config.diskBus')) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.diskBus.label') }));
}
if (!get(this, 'config.imageName')) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.imageName.label') }));
}
if (!get(this, 'config.networkName')) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.networkName.label') }));
}
if (!get(this, 'config.sshUser')) { if (!get(this, 'config.sshUser')) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.sshUser.label') })); errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.sshUser.label') }));
} }
this.validateScheduling(errors); this.validateScheduling(errors);
this.validateDiskAndNetwork(errors);
// Set the array of errors for display, // Set the array of errors for display,
// and return true if saving should continue. // and return true if saving should continue.
@ -411,6 +479,10 @@ export default Component.extend(NodeDriver, {
&& Object.getPrototypeOf(obj) === Object.prototype; && Object.getPrototypeOf(obj) === Object.prototype;
}, },
isImageVolume(volume) {
return Object.prototype.hasOwnProperty.call(volume, 'imageName');
},
initSchedulings() { initSchedulings() {
const nodeSchedulings = []; const nodeSchedulings = [];
const podSchedulings = []; const podSchedulings = [];
@ -496,6 +568,59 @@ export default Component.extend(NodeDriver, {
set(this, 'podSchedulings', podSchedulings); set(this, 'podSchedulings', podSchedulings);
}, },
initDisks() {
let disks = [];
if (!get(this, 'config.diskInfo')) {
const imageName = get(this, 'config.imageName') || '';
disks = [{
imageName,
bootOrder: 1,
size: 40,
}];
if (get(this, 'config.diskBus')) {
disks[0].bus = get(this, 'config.diskBus');
}
const diskInfo = JSON.stringify({ disks });
set(this, 'config.diskInfo', diskInfo);
} else {
const diskInfo = get(this, 'config.diskInfo');
disks = JSON.parse(diskInfo).disks || [];
}
set(this, 'disks', disks);
},
initInterfaces() {
let _interfaces = [];
if (!get(this, 'config.networkInfo')) {
const networkName = get(this, 'config.networkName') || '';
_interfaces = [{
networkName,
macAddress: '',
}];
if (get(this, 'config.networkModel')) {
_interfaces[0].model = get(this, 'config.networkModel');
}
const networkInfo = JSON.stringify({ interfaces: _interfaces });
set(this, 'config.networkInfo', networkInfo);
} else {
const networkInfo = get(this, 'config.networkInfo');
_interfaces = JSON.parse(networkInfo).interfaces || [];
}
set(this, 'interfaces', _interfaces);
},
parseNodeScheduling() { parseNodeScheduling() {
const arr = this.nodeSchedulings; const arr = this.nodeSchedulings;
const out = {}; const out = {};
@ -633,6 +758,43 @@ export default Component.extend(NodeDriver, {
if (nodeHasMissingKey || podHasMissingKey) { if (nodeHasMissingKey || podHasMissingKey) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('formNodeRequirement.key.label') })); errors.push(this.intl.t('generic.required', { key: this.intl.t('formNodeRequirement.key.label') }));
} }
} },
isValidMac(value) {
return /^[A-Fa-f0-9]{2}(-[A-Fa-f0-9]{2}){5}$|^[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){5}$/.test(value);
},
validateDiskAndNetwork(errors) {
const disks = get(this, 'disks');
disks.forEach((disk) => {
if (Object.prototype.hasOwnProperty.call(disk, 'imageName') && !disk.imageName) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.imageName.label') }));
}
if (Object.prototype.hasOwnProperty.call(disk, 'storageClassName') && !disk.storageClassName) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.storageClass.label') }));
}
if (!disk.size) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.diskSize.label') }));
}
if (!disk.size) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.diskSize.label') }));
}
});
const interfaces = get(this, 'interfaces');
interfaces.forEach((_interface) => {
if (!_interface.networkName) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.networkName.label') }));
}
if (_interface.macAddress && !this.isValidMac(_interface.macAddress)) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.network.macFormat') }));
}
});
}
}); });

View File

@ -114,49 +114,159 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col span-6"> <label class="acc-label">
<label class="acc-label"> {{t "nodeDriver.harvester.disks.title"}}
{{t "nodeDriver.harvester.imageName.label"}}{{field-required}} </label>
</label> </div>
<SearchableSelect {{#each disks as |disk idx|}}
@class="form-control" <div class="box mb-10">
@content={{imageContent}} <div class="row">
@value={{config.imageName}} <div class="pull-left">
@placeholder={{t "nodeDriver.harvester.imageName.placeholder"}} {{#if (has-property disk 'imageName') }}
/> <h4>{{t 'nodeDriver.harvester.disks.diskType.imageVolume'}}</h4>
</div> {{else}}
<h4>{{t 'nodeDriver.harvester.disks.diskType.volume'}}</h4>
{{/if}}
</div>
<div class="pull-right">
<button class="btn bg-link icon-btn" type="button" disabled={{eq idx 0}} {{action "removeDisk" disk}}>
<i class="icon icon-minus text-small" />
<span>{{t "generic.remove" }}</span>
</button>
</div>
</div>
<div class="row">
<div class="col mt-0 span-6">
{{#if (has-property disk 'imageName') }}
<label class="acc-label">
{{t "nodeDriver.harvester.imageName.label"}}{{field-required}}
</label>
<SearchableSelect
@class="form-control"
@content={{imageContent}}
@value={{disk.imageName}}
@placeholder={{t "nodeDriver.harvester.imageName.placeholder"}}
/>
{{else}}
<label class="acc-label">
{{t "nodeDriver.harvester.storageClass.label"}}{{field-required}}
</label>
<SearchableSelect
@class="form-control"
@content={{storageClassContent}}
@value={{disk.storageClassName}}
@placeholder={{t "nodeDriver.harvester.storageClass.placeholder"}}
/>
{{/if}}
</div>
<div class="col span-6"> <div class="col mt-0 span-6">
<label class="acc-label"> <label class="acc-label">
{{t "nodeDriver.harvester.diskSize.label"}}{{field-required}} {{t "nodeDriver.harvester.disks.bootOrder.label"}}{{field-required}}
</label> </label>
<div class="input-group">
{{input-integer {{boot-order-select disk=disk disks=disks idx=idx}}
min=1 </div>
value=config.diskSize </div>
classNames="form-control"
placeholder=(t "nodeDriver.harvester.diskSize.placeholder") <div class="row">
}} <div class="col span-6">
<div class="input-group-addon bg-default"> <label class="acc-label">
{{t "nodeDriver.harvester.diskSize.unit"}} {{t "nodeDriver.harvester.diskSize.label"}}{{field-required}}
</label>
<div class="input-group">
{{input-integer
min=1
value=disk.size
classNames="form-control"
placeholder=(t "nodeDriver.harvester.diskSize.placeholder")
}}
<div class="input-group-addon bg-default">
{{t "nodeDriver.harvester.diskSize.unit"}}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> {{/each}}
<button class="btn bg-link icon-btn" type="button" {{action "addVolume" "volume" }}>
<i class="icon icon-plus text-small" />
<span>{{t "nodeDriver.harvester.disks.addVolume.volume"}}</span>
</button>
<button class="btn bg-link icon-btn" type="button" {{action "addVolume" "image"}}>
<i class="icon icon-plus text-small" />
<span>{{t "nodeDriver.harvester.disks.addVolume.imageVolume"}}</span>
</button>
<hr class="mb-20 mt-20">
<div class="row"> <div class="row">
<div class="col span-6"> <label class="acc-label">
<label class="acc-label"> {{t "nodeDriver.harvester.network.title"}}
{{t "nodeDriver.harvester.networkName.label"}}{{field-required}} </label>
</label>
<SearchableSelect
@class="form-control"
@content={{networkContent}}
@value={{config.networkName}}
@placeholder={{t "nodeDriver.harvester.networkName.placeholder"}}
/>
</div>
</div> </div>
{{#each interfaces as |network idx|}}
<div class="box mb-10">
<div class="row">
<div class="pull-left">
<h4>{{t "nodeDriver.harvester.network.label"}}</h4>
</div>
<div class="pull-right">
<button class="btn bg-link icon-btn" type="button" disabled={{eq idx 0}} {{action "removeNetwork" network}}>
<i class="icon icon-minus text-small" />
<span>{{t "generic.remove" }}</span>
</button>
</div>
</div>
<div class="row">
<div class="col mt-0 span-6">
<label class="acc-label">
{{t "nodeDriver.harvester.networkName.label"}}{{field-required}}
</label>
<SearchableSelect
@class="form-control"
@content={{networkContent}}
@value={{network.networkName}}
@placeholder={{t "nodeDriver.harvester.networkName.placeholder"}}
/>
</div>
<div class="col mt-0 span-6">
<label class="acc-label">
{{t "nodeDriver.harvester.macAddress.label"}}
<span class="inline-block">
{{#tooltip-element
type="tooltip-basic"
model=(t "nodeDriver.harvester.macAddress.toolTip" htmlSafe=true)
tooltipTemplate="tooltip-static"
aria-describedby="tooltip-base"
tooltipFor="tooltipPoolCreateAfter"
placement="top"
tagName="div"
}}
<i class="icon icon-help icon-blue"></i>
{{/tooltip-element}}
</span>
</label>
{{input
type="text"
value=network.macAddress
classNames="form-control"
}}
</div>
</div>
</div>
{{/each}}
<button class="btn bg-link icon-btn" type="button" {{action "addNetwork" }}>
<i class="icon icon-plus text-small" />
<span>{{ t "nodeDriver.harvester.network.addNetwork.network" }}</span>
</button>
{{/if}} {{/if}}
{{/accordion-list-item}} {{/accordion-list-item}}

View File

@ -0,0 +1 @@
export { default } from 'nodes/components/boot-order-select/component';

View File

@ -0,0 +1,7 @@
import { helper } from '@ember/component/helper';
export function hasProperty(params) {
return Object.prototype.hasOwnProperty.call(params[0], params[1]);
}
export default helper(hasProperty);

View File

@ -0,0 +1 @@
export { default } from 'shared/helpers/has-property';

View File

@ -9773,6 +9773,9 @@ nodeDriver:
imageName: imageName:
label: Image label: Image
placeholder: Please select a image placeholder: Please select a image
storageClass:
label: Storage Class
placeholder: Please select a storageClass
diskBus: diskBus:
label: Bus label: Bus
access: access:
@ -9796,6 +9799,9 @@ nodeDriver:
label: SSH User label: SSH User
placeholder: e.g. ubuntu placeholder: e.g. ubuntu
toolTip: SSH user to login with the selected OS image. toolTip: SSH user to login with the selected OS image.
macAddress:
label: Mac Address
toolTip: MAC address as seen inside the guest system.
networkName: networkName:
label: Network Name label: Network Name
placeholder: Please select a network placeholder: Please select a network
@ -9812,6 +9818,22 @@ nodeDriver:
namespace: namespace:
label: Namespace label: Namespace
placeholder: e.g. default placeholder: e.g. default
disks:
title: Volumes
diskType:
volume: Volume
imageVolume: Image Volume
bootOrder:
label: bootOrder
addVolume:
volume: Add Volume
imageVolume: Add Image Volume
network:
title: Networks
label: Network
macFormat: 'Invalid MAC address format.'
addNetwork:
network: Add Network
diskSize: diskSize:
label: Disk label: Disk
unit: GiB unit: GiB