diff --git a/pkg/harvester/config/feature-flags.js b/pkg/harvester/config/feature-flags.js index c5a1fb27..1ca4aa22 100644 --- a/pkg/harvester/config/feature-flags.js +++ b/pkg/harvester/config/feature-flags.js @@ -73,7 +73,8 @@ const FEATURE_FLAGS = { 'supportFilesystem', 'disableResourcePooling', 'expandOnlineEncryptedVolume', - 'longhornV2HugepageSettings' + 'longhornV2HugepageSettings', + 'staticIPForVM', ], }; diff --git a/pkg/harvester/config/labels-annotations.js b/pkg/harvester/config/labels-annotations.js index d61d37f9..960038ac 100644 --- a/pkg/harvester/config/labels-annotations.js +++ b/pkg/harvester/config/labels-annotations.js @@ -81,6 +81,7 @@ export const HCI = { VOLUME_MODE_ACCESS_MODES: 'cdi.harvesterhci.io/storageProfileVolumeModeAccessModes', VOLUME_SNAPSHOT_CLASS: 'cdi.harvesterhci.io/storageProfileVolumeSnapshotClass', MAC_ADDRESS: 'harvesterhci.io/mac-address', + STATIC_IP: 'static-ip.harvesterhci.io', NODE_UPGRADE_PAUSE_MAP: 'harvesterhci.io/node-upgrade-pause-map', CDI_POPULATOR_KIND: 'cdi.kubevirt.io/storage.populator.kind', CNI_NETWORKS: 'k8s.v1.cni.cncf.io/networks', diff --git a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineNetwork/base.vue b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineNetwork/base.vue index 358e38f7..7d6e5cb0 100644 --- a/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineNetwork/base.vue +++ b/pkg/harvester/edit/kubevirt.io.virtualmachine/VirtualMachineNetwork/base.vue @@ -133,6 +133,10 @@ export default { }]; return this.isMasquerade ? masquerade : other; + }, + + staticIPForVMEnabled() { + return this.$store.getters['harvester-common/getFeatureEnabled']('staticIPForVM'); } }, @@ -150,6 +154,7 @@ export default { isSingle(neu) { if (!neu) { this.value['macAddress'] = ''; + this.value['staticIp'] = ''; this.update(); } } @@ -163,6 +168,7 @@ export default { if (neu === MANAGEMENT_NETWORK) { this.value.isPod = true; this.value.macAddress = ''; + this.value.staticIp = ''; } else { this.value.isPod = false; } @@ -289,9 +295,30 @@ export default { -
+
+
+ + + +
diff --git a/pkg/harvester/formatters/HarvesterIpAddress.vue b/pkg/harvester/formatters/HarvesterIpAddress.vue index 605f5ac9..04eff11d 100644 --- a/pkg/harvester/formatters/HarvesterIpAddress.vue +++ b/pkg/harvester/formatters/HarvesterIpAddress.vue @@ -36,6 +36,10 @@ export default { computed: { ips() { + if (this.staticAnnotationIP.length) { + return this.staticAnnotationIP; + } + if (this.vmiIp.length) { return [...this.vmiIp, ...this.networkAnnotationIP] .filter(Boolean) @@ -45,6 +49,24 @@ export default { return this.customAnnotationIP; }, + staticAnnotationIP() { + const annotations = this.row?.metadata?.annotations || {}; + const staticIpPrefix = `${ HCI_ANNOTATIONS.STATIC_IP }/`; + + return Object.entries(annotations) + .filter(([key, value]) => key.startsWith(staticIpPrefix) && !!value) + .map(([key, value]) => { + const nicName = key.slice(staticIpPrefix.length); + const ip = String(value).replace(/\/[\d\D]*/, ''); + + return { + ip, + name: nicName + }; + }) + .filter(({ ip }) => isIpv4(ip)); + }, + customAnnotationIP() { const annotationIp = get(this.row, `metadata.annotations."${ HCI_ANNOTATIONS.CUSTOM_IP }"`); diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index a3cd87f5..a3ce542c 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -376,6 +376,7 @@ harvester: network: Network model: Model macAddress: MAC address + staticIp: Static IP port: Port protocol: Protocol remove: Remove @@ -750,6 +751,7 @@ harvester: dragTip: Drag and drop volumes, or use the volume's arrows, to change the boot order. volumeTip: The virtual machine only contains a CD-ROM volume. You may want to add additional disk volumes. macTip: "MAC address as seen inside the guest system." + staticIpTip: "Static IP that will be stored to annotation static-ip.harvesterhci.io/{nic_name}." volumeUpdate: 'Set volume { name } successfully' type: Type size: Size diff --git a/pkg/harvester/mixins/harvester-vm/index.js b/pkg/harvester/mixins/harvester-vm/index.js index 387ab619..a6a575c1 100644 --- a/pkg/harvester/mixins/harvester-vm/index.js +++ b/pkg/harvester/mixins/harvester-vm/index.js @@ -630,6 +630,8 @@ export default { const networks = vm.spec.template.spec.networks || []; const interfaces = vm.spec.template.spec.domain.devices.interfaces || []; + const annotations = vm.metadata?.annotations || {}; + const staticIpPrefix = `${ HCI_ANNOTATIONS.STATIC_IP }/`; const out = interfaces.map( (I, index) => { const network = networks.find( (N) => I.name === N.name); @@ -646,6 +648,7 @@ export default { newCreateId: (fromTemplate || init) ? randomStr(10) : false, model: I.model, networkName: isPod ? MANAGEMENT_NETWORK : network?.multus?.networkName, + staticIp: annotations[`${ staticIpPrefix }${ I.name }`] || '', }; }); @@ -747,6 +750,8 @@ export default { const vm = this.resourceType === HCI.VM ? this.value : this.value.spec.vm; + this.syncStaticIpAnnotations(vm); + // parse reserved memory if (!this.reservedMemory) { delete vm.metadata.annotations[HCI_ANNOTATIONS.VM_RESERVED_MEMORY]; @@ -768,6 +773,30 @@ export default { } }, + syncStaticIpAnnotations(vm) { + if (!vm?.metadata) { + return; + } + + if (!vm.metadata.annotations) { + vm.metadata.annotations = {}; + } + + const staticIpPrefix = `${ HCI_ANNOTATIONS.STATIC_IP }/`; + + Object.keys(vm.metadata.annotations).forEach((key) => { + if (key.startsWith(staticIpPrefix)) { + delete vm.metadata.annotations[key]; + } + }); + + this.networkRows.forEach((row) => { + if (row.name && row.staticIp) { + vm.metadata.annotations[`${ staticIpPrefix }${ row.name }`] = row.staticIp; + } + }); + }, + setCPUAndMemory() { if (this.cpuMemoryHotplugEnabled) { // set CPU @@ -1807,6 +1836,15 @@ export default { }, watch: { + networkRows: { + handler() { + const vm = this.resourceType === HCI.VM ? this.value : this.value?.spec?.vm; + + this.syncStaticIpAnnotations(vm); + }, + deep: true + }, + diskRows: { handler(neu, old) { if (Array.isArray(neu)) {