mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2025-12-13 13:11:43 +00:00
feat: add vm-migration-network setting (#395)
Signed-off-by: Yi-Ya Chen <yiya.chen@suse.com>
This commit is contained in:
parent
060105ead3
commit
4486f71c8f
270
pkg/harvester/components/settings/vm-migration-network.vue
Normal file
270
pkg/harvester/components/settings/vm-migration-network.vue
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
<script>
|
||||||
|
import { LabeledInput } from '@components/Form/LabeledInput';
|
||||||
|
import LabeledSelect from '@shell/components/form/LabeledSelect.vue';
|
||||||
|
import { RadioGroup } from '@components/Form/Radio';
|
||||||
|
import { Banner } from '@components/Banner';
|
||||||
|
import ArrayList from '@shell/components/form/ArrayList';
|
||||||
|
import { allHash } from '@shell/utils/promise';
|
||||||
|
import { isValidCIDR } from '@shell/utils/validators/cidr';
|
||||||
|
import { NODE } from '@shell/config/types';
|
||||||
|
import { _EDIT } from '@shell/config/query-params';
|
||||||
|
import { HCI } from '../../types';
|
||||||
|
|
||||||
|
const DEFAULT_NETWORK = {
|
||||||
|
clusterNetwork: '',
|
||||||
|
vlan: '',
|
||||||
|
range: '',
|
||||||
|
exclude: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'VMMigrationNetwork',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LabeledInput,
|
||||||
|
LabeledSelect,
|
||||||
|
RadioGroup,
|
||||||
|
Banner,
|
||||||
|
ArrayList,
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
registerBeforeHook: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
type: String,
|
||||||
|
default: _EDIT,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ value: '' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetch() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await allHash({
|
||||||
|
clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
|
||||||
|
vlanStatus: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.VLAN_STATUS }),
|
||||||
|
nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.fetchError = null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch network data:', e); // eslint-disable-line no-console
|
||||||
|
this.fetchError = this.t('harvester.setting.vmMigrationNetwork.fetchError', { error: e.message || e }, true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
const { parsed, enabled, parseError } = this.parseInitialValue();
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled,
|
||||||
|
network: { ...DEFAULT_NETWORK, ...parsed },
|
||||||
|
fetchError: null,
|
||||||
|
parseError,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
created() {
|
||||||
|
this.registerBeforeHook?.(this.willSave, 'willSave');
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
allErrors() {
|
||||||
|
return [this.fetchError, this.parseError].filter(Boolean);
|
||||||
|
},
|
||||||
|
|
||||||
|
clusterNetworkOptions() {
|
||||||
|
const inStore = this.$store.getters['currentProduct'].inStore;
|
||||||
|
const networks = this.$store.getters[`${ inStore }/all`](HCI.CLUSTER_NETWORK) || [];
|
||||||
|
|
||||||
|
return networks.map((net) => ({
|
||||||
|
label: net.isReadyForStorageNetwork ? net.id : `${ net.id } (${ this.t('generic.notReady') })`,
|
||||||
|
value: net.id,
|
||||||
|
disabled: !net.isReadyForStorageNetwork,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
disableEdit() {
|
||||||
|
return !!(this.fetchError || this.parseError);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
parseInitialValue() {
|
||||||
|
let parsed = {};
|
||||||
|
let enabled = false;
|
||||||
|
let parseError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (typeof this.value.value === 'string' && this.value.value.trim()) {
|
||||||
|
parsed = JSON.parse(this.value.value);
|
||||||
|
enabled = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[VMMigrationNetwork] Failed to parse value:', e); // eslint-disable-line no-console
|
||||||
|
parseError = this.t('harvester.setting.vmMigrationNetwork.parseError', { error: e.message }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(parsed.exclude)) {
|
||||||
|
parsed.exclude = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
parsed, enabled, parseError
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
clearErrors() {
|
||||||
|
this.fetchError = null;
|
||||||
|
this.parseError = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
inputVlan(val) {
|
||||||
|
this.network.vlan = val ? Math.min(4094, Math.max(1, Number(val))) : '';
|
||||||
|
this.update();
|
||||||
|
},
|
||||||
|
|
||||||
|
useDefault() {
|
||||||
|
this.network = { ...DEFAULT_NETWORK };
|
||||||
|
this.value.value = '';
|
||||||
|
this.enabled = false;
|
||||||
|
this.clearErrors();
|
||||||
|
},
|
||||||
|
|
||||||
|
update() {
|
||||||
|
try {
|
||||||
|
this.value.value = this.enabled ? JSON.stringify({
|
||||||
|
...this.network,
|
||||||
|
exclude: (this.network.exclude || []).filter((e) => !!e?.trim()),
|
||||||
|
}) : '';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to stringify network config:', e); // eslint-disable-line no-console
|
||||||
|
this.value.value = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
validateInputs() {
|
||||||
|
const errors = [];
|
||||||
|
|
||||||
|
if (!this.network.clusterNetwork) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.clusterNetwork') }, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.network.range) {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.range.label') }, true));
|
||||||
|
} else if (!isValidCIDR(this.network.range)) {
|
||||||
|
errors.push(this.t('harvester.setting.vmMigrationNetwork.range.invalid', null, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.network.vlan === '') {
|
||||||
|
errors.push(this.t('validation.required', { key: this.t('harvester.setting.vmMigrationNetwork.vlan') }, true));
|
||||||
|
} else {
|
||||||
|
const vlan = Number(this.network.vlan);
|
||||||
|
|
||||||
|
if (isNaN(vlan) || vlan < 1 || vlan > 4094) {
|
||||||
|
errors.push(this.t('validation.between', {
|
||||||
|
key: this.t('harvester.setting.vmMigrationNetwork.vlan'),
|
||||||
|
min: 1,
|
||||||
|
max: 4094,
|
||||||
|
}, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cidr of this.network.exclude || []) {
|
||||||
|
if (cidr && !isValidCIDR(cidr)) {
|
||||||
|
errors.push(this.t('harvester.setting.storageNetwork.exclude.invalid', { value: cidr }, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
},
|
||||||
|
|
||||||
|
async willSave() {
|
||||||
|
if (!this.enabled) {
|
||||||
|
this.useDefault();
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update();
|
||||||
|
|
||||||
|
const errors = this.validateInputs();
|
||||||
|
|
||||||
|
return errors.length ? Promise.reject(errors) : Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Banner
|
||||||
|
v-for="(errorMsg, index) in allErrors"
|
||||||
|
:key="index"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
{{ errorMsg }}
|
||||||
|
</Banner>
|
||||||
|
<RadioGroup
|
||||||
|
v-model:value="enabled"
|
||||||
|
class="mb-20"
|
||||||
|
name="enableMigrationNetwork"
|
||||||
|
:options="[true, false]"
|
||||||
|
:labels="[t('generic.enabled'), t('generic.disabled')]"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<template v-if="enabled">
|
||||||
|
<LabeledSelect
|
||||||
|
v-model:value="network.clusterNetwork"
|
||||||
|
required
|
||||||
|
label-key="harvester.setting.vmMigrationNetwork.clusterNetwork"
|
||||||
|
class="mb-20"
|
||||||
|
:mode="mode"
|
||||||
|
:options="clusterNetworkOptions"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value.number="network.vlan"
|
||||||
|
required
|
||||||
|
type="number"
|
||||||
|
class="mb-20"
|
||||||
|
:min="1"
|
||||||
|
:max="4094"
|
||||||
|
:mode="mode"
|
||||||
|
placeholder="e.g. 1 - 4094"
|
||||||
|
label-key="harvester.setting.vmMigrationNetwork.vlan"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
@update:value="inputVlan"
|
||||||
|
/>
|
||||||
|
<LabeledInput
|
||||||
|
v-model:value="network.range"
|
||||||
|
required
|
||||||
|
class="mb-5"
|
||||||
|
:mode="mode"
|
||||||
|
:placeholder="t('harvester.setting.vmMigrationNetwork.range.placeholder')"
|
||||||
|
label-key="harvester.setting.vmMigrationNetwork.range.label"
|
||||||
|
:disabled="disableEdit"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
<ArrayList
|
||||||
|
v-model:value="network.exclude"
|
||||||
|
:show-header="true"
|
||||||
|
:default-add-value="''"
|
||||||
|
:mode="mode"
|
||||||
|
:add-disabled="disableEdit"
|
||||||
|
:add-label="t('harvester.setting.vmMigrationNetwork.exclude.addButton')"
|
||||||
|
:value-label="t('harvester.setting.vmMigrationNetwork.exclude.label')"
|
||||||
|
:value-placeholder="t('harvester.setting.storageNetwork.exclude.placeholder')"
|
||||||
|
@update:value="update"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -41,7 +41,8 @@ const FEATURE_FLAGS = {
|
|||||||
'v1.6.0': [
|
'v1.6.0': [
|
||||||
'vmMachineTypes',
|
'vmMachineTypes',
|
||||||
'customSupportBundle',
|
'customSupportBundle',
|
||||||
'csiOnlineExpandValidation'
|
'csiOnlineExpandValidation',
|
||||||
|
'vmNetworkMigration'
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,7 @@ export const HCI_SETTING = {
|
|||||||
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
LONGHORN_V2_DATA_ENGINE_ENABLED: 'longhorn-v2-data-engine-enabled',
|
||||||
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
ADDITIONAL_GUEST_MEMORY_OVERHEAD_RATIO: 'additional-guest-memory-overhead-ratio',
|
||||||
UPGRADE_CONFIG: 'upgrade-config',
|
UPGRADE_CONFIG: 'upgrade-config',
|
||||||
|
VM_MIGRATION_NETWORK: 'vm-migration-network',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_ALLOWED_SETTINGS = {
|
export const HCI_ALLOWED_SETTINGS = {
|
||||||
@ -107,6 +108,9 @@ export const HCI_ALLOWED_SETTINGS = {
|
|||||||
featureFlag: 'upgradeConfigSetting',
|
featureFlag: 'upgradeConfigSetting',
|
||||||
docPath: 'UPGRADE_CONFIG_URL'
|
docPath: 'UPGRADE_CONFIG_URL'
|
||||||
},
|
},
|
||||||
|
[HCI_SETTING.VM_MIGRATION_NETWORK]: {
|
||||||
|
kind: 'json', from: 'import', canReset: true, featureFlag: 'vmNetworkMigration',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
export const HCI_SINGLE_CLUSTER_ALLOWED_SETTING = {
|
||||||
|
|||||||
@ -1087,6 +1087,20 @@ harvester:
|
|||||||
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
tip: 'Specify an IP range in the IPv4 CIDR format. <code>Number of IPs Required = Number of Nodes * 2 + Number of Disks * 2 + Number of Images to Download/Upload </code>. For more information about storage network settings, see the <a href="{url}" target="_blank">documentation</a>.'
|
||||||
vmForceDeletionPolicy:
|
vmForceDeletionPolicy:
|
||||||
period: Period
|
period: Period
|
||||||
|
vmMigrationNetwork:
|
||||||
|
parseError: "Failed to parse existing configuration."
|
||||||
|
fetchError: "Failed to load required network resources: {error}. Please refresh the page or try again later."
|
||||||
|
clusterNetwork: Cluster Network
|
||||||
|
vlan: VLAN ID
|
||||||
|
range:
|
||||||
|
placeholder: e.g. 172.16.0.0/24
|
||||||
|
label: IP Range
|
||||||
|
invalid: '"Range" is invalid.'
|
||||||
|
exclude:
|
||||||
|
label: Excluded IPs
|
||||||
|
placeholder: e.g. 172.16.0.1/32
|
||||||
|
invalid: '"Exclude list" is invalid.'
|
||||||
|
addButton: Add Exclude IP
|
||||||
ratio : Ratio
|
ratio : Ratio
|
||||||
autoRotateRKE2Certs:
|
autoRotateRKE2Certs:
|
||||||
expiringInHours: Expiring in
|
expiringInHours: Expiring in
|
||||||
@ -1634,6 +1648,7 @@ advancedSettings:
|
|||||||
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
|
'harv-longhorn-v2-data-engine-enabled': 'Enable the Longhorn V2 data engine. Default is false. <ul><li>Changing this setting will restart RKE2 on all nodes. This will not affect running VM workloads.</li><li>If you see "not enough hugepages-2Mi capacity" errors when enabling this setting, wait a minute for the error to clear. If the error remains, reboot the affected node.</li></ul>'
|
||||||
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
|
'harv-additional-guest-memory-overhead-ratio': 'The ratio for kubevirt to adjust the VM overhead memory. The value could be zero, empty value or floating number between 1.0 and 10.0, default to 1.5.'
|
||||||
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
|
'harv-upgrade-config': 'Configure image preloading and VM restore options for upgrades. See related fields in <a href="{url}" target="_blank" rel="noopener">settings/upgrade-config</a>'
|
||||||
|
'harv-vm-migration-network': 'Segregated network for VM migration traffic.'
|
||||||
|
|
||||||
typeLabel:
|
typeLabel:
|
||||||
kubevirt.io.virtualmachine: |-
|
kubevirt.io.virtualmachine: |-
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user