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,
model: {},
currentCluster: null,
clusters: [],
clusterContent: [],
imageContent: [],
networkContent: [],
namespaceContent: [],
nodes: [],
namespaces: [],
nodeSchedulings: [],
podSchedulings: [],
networkDataContent: [],
userDataContent: [],
controller: null,
signal: '',
isImportMode: true,
loading: false,
currentCluster: null,
clusters: [],
clusterContent: [],
imageContent: [],
networkContent: [],
namespaceContent: [],
nodes: [],
namespaces: [],
nodeSchedulings: [],
podSchedulings: [],
networkDataContent: [],
storageClassContent: [],
defaultStorageClass: '',
userDataContent: [],
controller: null,
signal: '',
isImportMode: true,
loading: false,
disks: [],
interfaces: [],
config: alias(`model.${ CONFIG }`),
@ -78,6 +82,10 @@ export default Component.extend(NodeDriver, {
if (!!get(this, 'config.vmAffinity')) {
this.initSchedulings();
}
this.initDisks()
this.initInterfaces()
},
actions: {
@ -99,10 +107,47 @@ export default Component.extend(NodeDriver, {
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) {
this.get('nodeSchedulings').removeObject(scheduling);
},
removeDisk(disk) {
this.get('disks').removeObject(disk);
},
removeNetwork(network) {
this.get('interfaces').removeObject(network);
},
updateNodeScheduling() {
this.parseNodeScheduling();
},
@ -124,7 +169,7 @@ export default Component.extend(NodeDriver, {
updatePodScheduling() {
this.parsePodScheduling();
}
},
},
clearData: observer('currentCredential.id', function() {
@ -135,6 +180,31 @@ export default Component.extend(NodeDriver, {
set(this, 'podSchedulings', []);
set(this, '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() {
@ -256,10 +326,15 @@ export default Component.extend(NodeDriver, {
})
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 label = isDefault ? `${ s.metadata.name } (${ this.intl.t('generic.default') })` : s.metadata.name;
if (isDefault) {
defaultStorageClass = s.metadata.name;
}
return {
label,
value: s.metadata.name,
@ -272,7 +347,8 @@ export default Component.extend(NodeDriver, {
namespaceContent,
userDataContent,
networkDataContent,
storageClassContent
storageClassContent,
defaultStorageClass
});
}).catch((err) => {
setProperties(this, {
@ -339,7 +415,9 @@ export default Component.extend(NodeDriver, {
networkData: '',
vmNamespace: '',
userData: '',
vmAffinity: ''
vmAffinity: '',
diskInfo: '',
networkInfo: ''
});
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') }));
}
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')) {
errors.push(this.intl.t('generic.required', { key: this.intl.t('nodeDriver.harvester.sshUser.label') }));
}
this.validateScheduling(errors);
this.validateDiskAndNetwork(errors);
// Set the array of errors for display,
// and return true if saving should continue.
@ -411,6 +479,10 @@ export default Component.extend(NodeDriver, {
&& Object.getPrototypeOf(obj) === Object.prototype;
},
isImageVolume(volume) {
return Object.prototype.hasOwnProperty.call(volume, 'imageName');
},
initSchedulings() {
const nodeSchedulings = [];
const podSchedulings = [];
@ -496,6 +568,59 @@ export default Component.extend(NodeDriver, {
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() {
const arr = this.nodeSchedulings;
const out = {};
@ -633,6 +758,43 @@ export default Component.extend(NodeDriver, {
if (nodeHasMissingKey || podHasMissingKey) {
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 class="row">
<div class="col span-6">
<label class="acc-label">
{{t "nodeDriver.harvester.imageName.label"}}{{field-required}}
</label>
<SearchableSelect
@class="form-control"
@content={{imageContent}}
@value={{config.imageName}}
@placeholder={{t "nodeDriver.harvester.imageName.placeholder"}}
/>
</div>
<label class="acc-label">
{{t "nodeDriver.harvester.disks.title"}}
</label>
</div>
{{#each disks as |disk idx|}}
<div class="box mb-10">
<div class="row">
<div class="pull-left">
{{#if (has-property disk 'imageName') }}
<h4>{{t 'nodeDriver.harvester.disks.diskType.imageVolume'}}</h4>
{{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="col span-6">
<label class="acc-label">
{{t "nodeDriver.harvester.diskSize.label"}}{{field-required}}
</label>
<div class="input-group">
{{input-integer
min=1
value=config.diskSize
classNames="form-control"
placeholder=(t "nodeDriver.harvester.diskSize.placeholder")
}}
<div class="input-group-addon bg-default">
{{t "nodeDriver.harvester.diskSize.unit"}}
<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 mt-0 span-6">
<label class="acc-label">
{{t "nodeDriver.harvester.disks.bootOrder.label"}}{{field-required}}
</label>
{{boot-order-select disk=disk disks=disks idx=idx}}
</div>
</div>
<div class="row">
<div class="col span-6">
<label class="acc-label">
{{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>
{{/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="col span-6">
<label class="acc-label">
{{t "nodeDriver.harvester.networkName.label"}}{{field-required}}
</label>
<SearchableSelect
@class="form-control"
@content={{networkContent}}
@value={{config.networkName}}
@placeholder={{t "nodeDriver.harvester.networkName.placeholder"}}
/>
</div>
<label class="acc-label">
{{t "nodeDriver.harvester.network.title"}}
</label>
</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}}
{{/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:
label: Image
placeholder: Please select a image
storageClass:
label: Storage Class
placeholder: Please select a storageClass
diskBus:
label: Bus
access:
@ -9796,6 +9799,9 @@ nodeDriver:
label: SSH User
placeholder: e.g. ubuntu
toolTip: SSH user to login with the selected OS image.
macAddress:
label: Mac Address
toolTip: MAC address as seen inside the guest system.
networkName:
label: Network Name
placeholder: Please select a network
@ -9812,6 +9818,22 @@ nodeDriver:
namespace:
label: Namespace
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:
label: Disk
unit: GiB