mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-07 14:46:15 +08:00
feat: support network boot (PXE) for VM interfaces (#1176)
Add an "Enable network boot (PXE)" option to bridge network interfaces and a VM-level choice to place network boot devices before or after volumes. The form now owns interface boot order: enabled NICs get sequential boot orders ahead of or behind the volumes, and a stale bootOrder is no longer inherited from the old spec. This also fixes editing a VM whose interface bootOrder was set in YAML (the documented workaround): the form used to renumber disks from 1 while keeping the NIC's bootOrder, producing a duplicate that KubeVirt rejects. The boot order shown on the VM, snapshot and backup detail pages now includes network interfaces. Related: harvester/harvester#3325 Signed-off-by: Alejandro Bonilla <abonilla@suse.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
528255dea4
commit
5af181f5b6
@@ -15,6 +15,7 @@ import OverviewKeypairs from '../kubevirt.io.virtualmachine/VirtualMachineTabs/V
|
|||||||
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||||
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||||
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||||
|
import { getBootDevices } from '../../utils/vm';
|
||||||
const UNDEFINED = 'n/a';
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -74,18 +75,8 @@ export default {
|
|||||||
return image?.spec?.displayName || '-';
|
return image?.spec?.displayName || '-';
|
||||||
},
|
},
|
||||||
|
|
||||||
disks() {
|
bootDevices() {
|
||||||
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
return getBootDevices(this?.spec?.template?.spec?.domain?.devices);
|
||||||
|
|
||||||
return disks.filter((disk) => {
|
|
||||||
return !!disk.bootOrder;
|
|
||||||
}).sort((a, b) => {
|
|
||||||
if (a.bootOrder < b.bootOrder) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cdroms() {
|
cdroms() {
|
||||||
@@ -101,8 +92,10 @@ export default {
|
|||||||
getDeviceType(o) {
|
getDeviceType(o) {
|
||||||
if (o.disk) {
|
if (o.disk) {
|
||||||
return 'Disk';
|
return 'Disk';
|
||||||
} else {
|
} else if (o.cdrom) {
|
||||||
return 'CD-ROM';
|
return 'CD-ROM';
|
||||||
|
} else {
|
||||||
|
return 'Network';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isEmpty(o) {
|
isEmpty(o) {
|
||||||
@@ -184,10 +177,10 @@ export default {
|
|||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="(disk, i) in disks"
|
v-for="(device, i) in bootDevices"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
{{ device.bootOrder }}. {{ device.name }} ({{ getDeviceType(device) }})
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -233,6 +226,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
|
:network-boot-first="networkBootFirst"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import OverviewKeypairs from '../kubevirt.io.virtualmachine/VirtualMachineTabs/V
|
|||||||
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
import Volume from '../../edit/kubevirt.io.virtualmachine/VirtualMachineVolume';
|
||||||
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
import Network from '../../edit/kubevirt.io.virtualmachine/VirtualMachineNetwork';
|
||||||
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
import CloudConfig from '../../edit/kubevirt.io.virtualmachine/VirtualMachineCloudConfig';
|
||||||
|
import { getBootDevices } from '../../utils/vm';
|
||||||
const UNDEFINED = 'n/a';
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -74,18 +75,8 @@ export default {
|
|||||||
return image?.spec?.displayName || '-';
|
return image?.spec?.displayName || '-';
|
||||||
},
|
},
|
||||||
|
|
||||||
disks() {
|
bootDevices() {
|
||||||
const disks = this?.spec?.template?.spec?.domain?.devices?.disks || [];
|
return getBootDevices(this?.spec?.template?.spec?.domain?.devices);
|
||||||
|
|
||||||
return disks.filter((disk) => {
|
|
||||||
return !!disk.bootOrder;
|
|
||||||
}).sort((a, b) => {
|
|
||||||
if (a.bootOrder < b.bootOrder) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cdroms() {
|
cdroms() {
|
||||||
@@ -101,8 +92,10 @@ export default {
|
|||||||
getDeviceType(o) {
|
getDeviceType(o) {
|
||||||
if (o.disk) {
|
if (o.disk) {
|
||||||
return 'Disk';
|
return 'Disk';
|
||||||
} else {
|
} else if (o.cdrom) {
|
||||||
return 'CD-ROM';
|
return 'CD-ROM';
|
||||||
|
} else {
|
||||||
|
return 'Network';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isEmpty(o) {
|
isEmpty(o) {
|
||||||
@@ -184,10 +177,10 @@ export default {
|
|||||||
<div>
|
<div>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="(disk, i) in disks"
|
v-for="(device, i) in bootDevices"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
{{ device.bootOrder }}. {{ device.name }} ({{ getDeviceType(device) }})
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -234,6 +227,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
|
:network-boot-first="networkBootFirst"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
+9
-16
@@ -6,6 +6,7 @@ import HarvesterIpAddress from '../../../formatters/HarvesterIpAddress';
|
|||||||
import VMConsoleBar from '../../../components/VMConsoleBar';
|
import VMConsoleBar from '../../../components/VMConsoleBar';
|
||||||
import { HCI } from '../../../types';
|
import { HCI } from '../../../types';
|
||||||
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
|
import { getVmCPUMemoryValues } from '../../../utils/cpuMemory';
|
||||||
|
import { getBootDevices } from '../../../utils/vm';
|
||||||
|
|
||||||
const UNDEFINED = 'n/a';
|
const UNDEFINED = 'n/a';
|
||||||
|
|
||||||
@@ -69,18 +70,8 @@ export default {
|
|||||||
return image?.spec?.displayName || 'N/A';
|
return image?.spec?.displayName || 'N/A';
|
||||||
},
|
},
|
||||||
|
|
||||||
disks() {
|
bootDevices() {
|
||||||
const disks = this.value?.spec?.template?.spec?.domain?.devices?.disks || [];
|
return getBootDevices(this.value?.spec?.template?.spec?.domain?.devices);
|
||||||
|
|
||||||
return disks.filter((disk) => {
|
|
||||||
return !!disk.bootOrder;
|
|
||||||
}).sort((a, b) => {
|
|
||||||
if (a.bootOrder < b.bootOrder) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
cdroms() {
|
cdroms() {
|
||||||
@@ -122,8 +113,10 @@ export default {
|
|||||||
getDeviceType(o) {
|
getDeviceType(o) {
|
||||||
if (o.disk) {
|
if (o.disk) {
|
||||||
return 'Disk';
|
return 'Disk';
|
||||||
} else {
|
} else if (o.cdrom) {
|
||||||
return 'CD-ROM';
|
return 'CD-ROM';
|
||||||
|
} else {
|
||||||
|
return 'Network';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isEmpty(o) {
|
isEmpty(o) {
|
||||||
@@ -219,16 +212,16 @@ export default {
|
|||||||
<div class="col span-6">
|
<div class="col span-6">
|
||||||
<InputOrDisplay
|
<InputOrDisplay
|
||||||
:name="t('harvester.virtualMachine.detail.details.bootOrder')"
|
:name="t('harvester.virtualMachine.detail.details.bootOrder')"
|
||||||
:value="disks"
|
:value="bootDevices"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
>
|
>
|
||||||
<template #value>
|
<template #value>
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="(disk, i) in disks"
|
v-for="(device, i) in bootDevices"
|
||||||
:key="i"
|
:key="i"
|
||||||
>
|
>
|
||||||
{{ disk.bootOrder }}. {{ disk.name }} ({{ getDeviceType(disk) }})
|
{{ device.bootOrder }}. {{ device.name }} ({{ getDeviceType(device) }})
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export default {
|
|||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
|
this['networkRows'] = this.getNetworkRows(neu, { fromTemplate: false, init: false });
|
||||||
|
this['networkBootFirst'] = this.isNetworkBootFirst(neu.spec);
|
||||||
|
|
||||||
// getInitConfig() seeds these on created(); refresh them here too so the
|
// getInitConfig() seeds these on created(); refresh them here too so the
|
||||||
// VM-wide performance panel does not go stale when the spec changes.
|
// VM-wide performance panel does not go stale when the spec changes.
|
||||||
@@ -276,6 +277,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
|
:network-boot-first="networkBootFirst"
|
||||||
mode="view"
|
mode="view"
|
||||||
:vm="value"
|
:vm="value"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -324,6 +324,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
|
v-model:network-boot-first="networkBootFirst"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
import InputOrDisplay from '@shell/components/InputOrDisplay';
|
||||||
import { LabeledInput } from '@components/Form/LabeledInput';
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
import LabeledSelect from '@shell/components/form/LabeledSelect';
|
||||||
|
import { Checkbox } from '@components/Form/Checkbox';
|
||||||
|
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
import { _CREATE, _VIEW } from '@shell/config/query-params';
|
||||||
@@ -33,7 +34,7 @@ export default {
|
|||||||
emits: ['update'],
|
emits: ['update'],
|
||||||
|
|
||||||
components: {
|
components: {
|
||||||
LabeledInput, LabeledSelect, InputOrDisplay
|
LabeledInput, LabeledSelect, InputOrDisplay, Checkbox
|
||||||
},
|
},
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
@@ -169,6 +170,7 @@ export default {
|
|||||||
this.value.isPod = true;
|
this.value.isPod = true;
|
||||||
this.value.macAddress = '';
|
this.value.macAddress = '';
|
||||||
this.value.staticIp = '';
|
this.value.staticIp = '';
|
||||||
|
this.value.networkBoot = false;
|
||||||
} else {
|
} else {
|
||||||
this.value.isPod = false;
|
this.value.isPod = false;
|
||||||
}
|
}
|
||||||
@@ -285,6 +287,22 @@ export default {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!isMasquerade"
|
||||||
|
data-testid="input-hen-networkBoot"
|
||||||
|
class="row mb-20"
|
||||||
|
>
|
||||||
|
<div class="col span-12">
|
||||||
|
<Checkbox
|
||||||
|
v-model:value="value.networkBoot"
|
||||||
|
:label="t('harvester.virtualMachine.network.networkBoot.label')"
|
||||||
|
:tooltip="t('harvester.virtualMachine.network.networkBoot.tip')"
|
||||||
|
:mode="mode"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="!isMasquerade && isSingle">
|
<div v-if="!isMasquerade && isSingle">
|
||||||
<div class="row mb-20">
|
<div class="row mb-20">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import InfoBox from '@shell/components/InfoBox';
|
import InfoBox from '@shell/components/InfoBox';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
import { NETWORK_ATTACHMENT } from '@shell/config/types';
|
||||||
import { sortBy } from '@shell/utils/sort';
|
import { sortBy } from '@shell/utils/sort';
|
||||||
import { clone } from '@shell/utils/object';
|
import { clone } from '@shell/utils/object';
|
||||||
@@ -10,7 +11,11 @@ import { HCI as HCI_ANNOTATIONS } from '../../../config/labels-annotations';
|
|||||||
import Base from './base';
|
import Base from './base';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { InfoBox, Base },
|
components: {
|
||||||
|
InfoBox, Base, RadioGroup
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['update:value', 'update:networkBootFirst'],
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
vm: {
|
vm: {
|
||||||
@@ -35,6 +40,11 @@ export default {
|
|||||||
isSingle: {
|
isSingle: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: true
|
default: true
|
||||||
|
},
|
||||||
|
|
||||||
|
networkBootFirst: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -82,6 +92,20 @@ export default {
|
|||||||
|
|
||||||
vmState() {
|
vmState() {
|
||||||
return this.vm?.stateDisplay;
|
return this.vm?.stateDisplay;
|
||||||
|
},
|
||||||
|
|
||||||
|
hasNetworkBoot() {
|
||||||
|
return this.rows.some((R) => R.networkBoot);
|
||||||
|
},
|
||||||
|
|
||||||
|
networkBootOrderOptions() {
|
||||||
|
return [{
|
||||||
|
label: this.t('harvester.virtualMachine.network.networkBoot.first'),
|
||||||
|
value: true
|
||||||
|
}, {
|
||||||
|
label: this.t('harvester.virtualMachine.network.networkBoot.last'),
|
||||||
|
value: false
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -218,6 +242,22 @@ export default {
|
|||||||
/>
|
/>
|
||||||
</InfoBox>
|
</InfoBox>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="hasNetworkBoot"
|
||||||
|
data-testid="input-hen-networkBootFirst"
|
||||||
|
class="mb-20"
|
||||||
|
>
|
||||||
|
<RadioGroup
|
||||||
|
:value="networkBootFirst"
|
||||||
|
name="networkBootFirst"
|
||||||
|
:label="t('harvester.virtualMachine.network.networkBoot.order')"
|
||||||
|
:options="networkBootOrderOptions"
|
||||||
|
:mode="mode"
|
||||||
|
:row="true"
|
||||||
|
@update:value="$emit('update:networkBootFirst', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="!isView"
|
v-if="!isView"
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -791,6 +791,7 @@ export default {
|
|||||||
>
|
>
|
||||||
<Network
|
<Network
|
||||||
v-model:value="networkRows"
|
v-model:value="networkRows"
|
||||||
|
v-model:network-boot-first="networkBootFirst"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:is-single="isSingle"
|
:is-single="isSingle"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -871,6 +871,12 @@ harvester:
|
|||||||
title: Network
|
title: Network
|
||||||
addNetwork: Add Network
|
addNetwork: Add Network
|
||||||
addPort: Add Port
|
addPort: Add Port
|
||||||
|
networkBoot:
|
||||||
|
label: Enable network boot (PXE)
|
||||||
|
tip: Adds this interface to the VM boot order so the guest firmware can boot from the network. Requires a DHCP/TFTP (PXE) server reachable on the selected network; the management network does not provide one.
|
||||||
|
order: Network boot order
|
||||||
|
first: Before volumes
|
||||||
|
last: After volumes
|
||||||
cloudConfig:
|
cloudConfig:
|
||||||
title: Cloud Configuration
|
title: Cloud Configuration
|
||||||
createTemplateTitle: 'Create {name}'
|
createTemplateTitle: 'Create {name}'
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export default {
|
|||||||
imageId: '',
|
imageId: '',
|
||||||
diskRows: [],
|
diskRows: [],
|
||||||
networkRows: [],
|
networkRows: [],
|
||||||
|
networkBootFirst: true,
|
||||||
filesystemRows: [],
|
filesystemRows: [],
|
||||||
machineType: '',
|
machineType: '',
|
||||||
machineTypes: [],
|
machineTypes: [],
|
||||||
@@ -384,6 +385,7 @@ export default {
|
|||||||
const diskRows = this.getDiskRows(vm, volumeBackups);
|
const diskRows = this.getDiskRows(vm, volumeBackups);
|
||||||
|
|
||||||
const networkRows = this.getNetworkRows(vm, { fromTemplate, init });
|
const networkRows = this.getNetworkRows(vm, { fromTemplate, init });
|
||||||
|
const networkBootFirst = this.isNetworkBootFirst(spec);
|
||||||
const hasCreateVolumes = this.getHasCreatedVolumes(spec) || [];
|
const hasCreateVolumes = this.getHasCreatedVolumes(spec) || [];
|
||||||
|
|
||||||
let { userData = undefined, networkData = undefined } = this.getCloudInitNoCloud(spec);
|
let { userData = undefined, networkData = undefined } = this.getCloudInitNoCloud(spec);
|
||||||
@@ -461,6 +463,7 @@ export default {
|
|||||||
|
|
||||||
this['hasCreateVolumes'] = hasCreateVolumes;
|
this['hasCreateVolumes'] = hasCreateVolumes;
|
||||||
this['networkRows'] = networkRows;
|
this['networkRows'] = networkRows;
|
||||||
|
this['networkBootFirst'] = networkBootFirst;
|
||||||
this['imageId'] = imageId;
|
this['imageId'] = imageId;
|
||||||
|
|
||||||
this['diskRows'] = diskRows;
|
this['diskRows'] = diskRows;
|
||||||
@@ -676,12 +679,29 @@ export default {
|
|||||||
model: I.model,
|
model: I.model,
|
||||||
networkName: isPod ? MANAGEMENT_NETWORK : network?.multus?.networkName,
|
networkName: isPod ? MANAGEMENT_NETWORK : network?.multus?.networkName,
|
||||||
staticIp: annotations[`${ staticIpPrefix }${ I.name }`] || '',
|
staticIp: annotations[`${ staticIpPrefix }${ I.name }`] || '',
|
||||||
|
networkBoot: !!I.bootOrder,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Network boot devices are placed either before or after all volumes in the boot sequence.
|
||||||
|
* Derive which one from an existing spec, so a round trip through the form keeps it.
|
||||||
|
*/
|
||||||
|
isNetworkBootFirst(spec) {
|
||||||
|
const devices = spec?.template?.spec?.domain?.devices || {};
|
||||||
|
const nicOrders = (devices.interfaces || []).map((I) => I.bootOrder).filter(Boolean);
|
||||||
|
const diskOrders = (devices.disks || []).map((D) => D.bootOrder).filter(Boolean);
|
||||||
|
|
||||||
|
if (nicOrders.length === 0 || diskOrders.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(...nicOrders) < Math.min(...diskOrders);
|
||||||
|
},
|
||||||
|
|
||||||
parseVM() {
|
parseVM() {
|
||||||
this.userData = this.getUserData({ osType: this.osType, installAgent: this.installAgent });
|
this.userData = this.getUserData({ osType: this.osType, installAgent: this.installAgent });
|
||||||
this.parseOther();
|
this.parseOther();
|
||||||
@@ -894,9 +914,10 @@ export default {
|
|||||||
const disks = [];
|
const disks = [];
|
||||||
const volumes = [];
|
const volumes = [];
|
||||||
const volumeClaimTemplates = [];
|
const volumeClaimTemplates = [];
|
||||||
|
const bootOrderOffset = this.networkBootFirst ? this.networkRows.filter((R) => R.networkBoot).length : 0;
|
||||||
|
|
||||||
disk.forEach( (R, index) => {
|
disk.forEach( (R, index) => {
|
||||||
const _disk = this.parseDisk(R, index);
|
const _disk = this.parseDisk(R, index + bootOrderOffset);
|
||||||
|
|
||||||
disks.push(_disk);
|
disks.push(_disk);
|
||||||
|
|
||||||
@@ -1154,9 +1175,11 @@ export default {
|
|||||||
const networks = [];
|
const networks = [];
|
||||||
const interfaces = [];
|
const interfaces = [];
|
||||||
|
|
||||||
|
let bootOrder = this.networkBootFirst ? 1 : this.diskRows.length + 1;
|
||||||
|
|
||||||
networkRow.forEach( (R) => {
|
networkRow.forEach( (R) => {
|
||||||
const _network = this.parseNetwork(R);
|
const _network = this.parseNetwork(R);
|
||||||
const _interface = this.parseInterface(R);
|
const _interface = this.parseInterface(R, R.networkBoot ? bootOrder++ : undefined);
|
||||||
|
|
||||||
networks.push(_network);
|
networks.push(_network);
|
||||||
interfaces.push(_interface);
|
interfaces.push(_interface);
|
||||||
@@ -1412,7 +1435,7 @@ export default {
|
|||||||
return arr.map( (id) => this.getSSHValue(id)).filter( (O) => O !== undefined);
|
return arr.map( (id) => this.getSSHValue(id)).filter( (O) => O !== undefined);
|
||||||
},
|
},
|
||||||
|
|
||||||
parseInterface(R) {
|
parseInterface(R, bootOrder) {
|
||||||
const _interface = {};
|
const _interface = {};
|
||||||
const type = R.type;
|
const type = R.type;
|
||||||
|
|
||||||
@@ -1422,6 +1445,10 @@ export default {
|
|||||||
_interface.macAddress = R.macAddress;
|
_interface.macAddress = R.macAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bootOrder) {
|
||||||
|
_interface.bootOrder = bootOrder;
|
||||||
|
}
|
||||||
|
|
||||||
_interface.model = R.model;
|
_interface.model = R.model;
|
||||||
_interface.name = R.name;
|
_interface.name = R.name;
|
||||||
|
|
||||||
@@ -1929,6 +1956,11 @@ export default {
|
|||||||
delete merged['bridge'];
|
delete merged['bridge'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// boot order is owned by the form, don't inherit a stale one from the old spec
|
||||||
|
if (!iface.bootOrder) {
|
||||||
|
delete merged.bootOrder;
|
||||||
|
}
|
||||||
|
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,12 @@ export function parseVolumeClaimTemplates(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_IMAGE = 'EMPTY_IMAGE';
|
export const EMPTY_IMAGE = 'EMPTY_IMAGE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disks and interfaces that take part in the boot sequence, sorted by boot order.
|
||||||
|
*/
|
||||||
|
export function getBootDevices(devices = {}) {
|
||||||
|
return [...(devices.disks || []), ...(devices.interfaces || [])]
|
||||||
|
.filter((device) => !!device.bootOrder)
|
||||||
|
.sort((a, b) => a.bootOrder - b.bootOrder);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user