mirror of https://github.com/rancher/ui.git
support multi nic
This commit is contained in:
parent
9d02902073
commit
5f59400e9e
|
|
@ -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
|
||||
}),
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<SearchableSelect
|
||||
@class="form-control"
|
||||
@content={{bootOrderContent}}
|
||||
@value={{disk.bootOrder}}
|
||||
@placeholder={{t "nodeDriver.harvester.storageClass.placeholder"}}
|
||||
/>
|
||||
|
|
@ -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') }));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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="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">
|
||||
<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="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}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'nodes/components/boot-order-select/component';
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from 'shared/helpers/has-property';
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue