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:
Alejandro Bonilla
2026-10-07 11:21:27 +08:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 528255dea4
commit 5af181f5b6
11 changed files with 141 additions and 51 deletions
@@ -2,6 +2,7 @@
import InputOrDisplay from '@shell/components/InputOrDisplay';
import { LabeledInput } from '@components/Form/LabeledInput';
import LabeledSelect from '@shell/components/form/LabeledSelect';
import { Checkbox } from '@components/Form/Checkbox';
import { clone } from '@shell/utils/object';
import { _CREATE, _VIEW } from '@shell/config/query-params';
@@ -33,7 +34,7 @@ export default {
emits: ['update'],
components: {
LabeledInput, LabeledSelect, InputOrDisplay
LabeledInput, LabeledSelect, InputOrDisplay, Checkbox
},
props: {
@@ -169,6 +170,7 @@ export default {
this.value.isPod = true;
this.value.macAddress = '';
this.value.staticIp = '';
this.value.networkBoot = false;
} else {
this.value.isPod = false;
}
@@ -285,6 +287,22 @@ export default {
</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 class="row mb-20">
<a
@@ -1,5 +1,6 @@
<script>
import InfoBox from '@shell/components/InfoBox';
import { RadioGroup } from '@components/Form/Radio';
import { NETWORK_ATTACHMENT } from '@shell/config/types';
import { sortBy } from '@shell/utils/sort';
import { clone } from '@shell/utils/object';
@@ -10,7 +11,11 @@ import { HCI as HCI_ANNOTATIONS } from '../../../config/labels-annotations';
import Base from './base';
export default {
components: { InfoBox, Base },
components: {
InfoBox, Base, RadioGroup
},
emits: ['update:value', 'update:networkBootFirst'],
props: {
vm: {
@@ -35,6 +40,11 @@ export default {
isSingle: {
type: Boolean,
default: true
},
networkBootFirst: {
type: Boolean,
default: true
}
},
@@ -82,6 +92,20 @@ export default {
vmState() {
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>
<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
v-if="!isView"
type="button"