From 532b6c4d50c34a6b7f9e7597e2959127c12feac0 Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Fri, 17 Oct 2025 15:28:21 +0800 Subject: [PATCH] feat: add access / trunk mode in create VM network page (#510) * feat: add l2VlanTrunkMode feature Signed-off-by: Andy Lee * refactor: remove unneeded code Signed-off-by: Andy Lee * refactor: fix edit l2vlan trunk mode edit page Signed-off-by: Andy Lee * fix: hide Route tab when trunk mode Signed-off-by: Andy Lee --------- Signed-off-by: Andy Lee --- pkg/harvester/config/feature-flags.js | 3 +- pkg/harvester/config/types.js | 12 +- ...sterhci.io.networkattachmentdefinition.vue | 174 +++++++++++++++++- pkg/harvester/l10n/en-us.yaml | 8 + ...cni.cncf.io.networkattachmentdefinition.js | 6 +- 5 files changed, 188 insertions(+), 15 deletions(-) diff --git a/pkg/harvester/config/feature-flags.js b/pkg/harvester/config/feature-flags.js index ef0ffd56..18406c7a 100644 --- a/pkg/harvester/config/feature-flags.js +++ b/pkg/harvester/config/feature-flags.js @@ -50,7 +50,8 @@ const FEATURE_FLAGS = { ], 'v1.6.1': [], 'v1.7.0': [ - 'vmMachineTypeAuto' + 'vmMachineTypeAuto', + 'l2VlanTrunkMode' ] }; diff --git a/pkg/harvester/config/types.js b/pkg/harvester/config/types.js index b7c5daff..39dd3212 100644 --- a/pkg/harvester/config/types.js +++ b/pkg/harvester/config/types.js @@ -4,9 +4,10 @@ export const BACKUP_TYPE = { }; export const NETWORK_TYPE = { - L2VLAN: 'L2VlanNetwork', - UNTAGGED: 'UntaggedNetwork', - OVERLAY: 'OverlayNetwork', + L2VLAN: 'L2VlanNetwork', + UNTAGGED: 'UntaggedNetwork', + OVERLAY: 'OverlayNetwork', + L2TRUNK_VLAN: 'L2VlanTrunkNetwork', }; export const VOLUME_MODE = { @@ -23,3 +24,8 @@ export const INTERNAL_STORAGE_CLASS = { VMSTATE_PERSISTENCE: 'vmstate-persistence', LONGHORN_STATIC: 'longhorn-static', }; + +export const L2VLAN_MODE = { + ACCESS: 'access', + TRUNK: 'trunk', +}; diff --git a/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue b/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue index 3b03d562..1b42bcf4 100644 --- a/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue +++ b/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue @@ -10,9 +10,13 @@ import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-anno import CreateEditView from '@shell/mixins/create-edit-view'; import { allHash } from '@shell/utils/promise'; import { HCI } from '../types'; -import { NETWORK_TYPE } from '../config/types'; +import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types'; +import { removeObject } from '@shell/utils/array'; -const { L2VLAN, UNTAGGED, OVERLAY } = NETWORK_TYPE; +const { + L2VLAN, UNTAGGED, OVERLAY, L2TRUNK_VLAN +} = NETWORK_TYPE; +const { ACCESS, TRUNK } = L2VLAN_MODE; const AUTO = 'auto'; const MANUAL = 'manual'; @@ -52,6 +56,8 @@ export default { return { config, type, + l2VlanMode: this.value.vlanType === L2TRUNK_VLAN ? TRUNK : ACCESS, + vlanTrunk: this.parseVlanTrunk(config), layer3Network: { mode: layer3Network.mode || AUTO, serverIPAddr: layer3Network.serverIPAddr || '', @@ -108,6 +114,10 @@ export default { }]; }, + l2VlanTrunkModeFeatureEnabled() { + return this.$store.getters['harvester-common/getFeatureEnabled']('l2VlanTrunkMode'); + }, + kubeovnVpcSubnetSupport() { return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet'); }, @@ -131,6 +141,16 @@ export default { }); }, + l2VlanModeOptions() { + return [{ + label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.access'), + value: ACCESS, + }, { + label: this.t('harvester.vlanStatus.vlanConfig.l2VlanMode.trunk'), + value: TRUNK, + }]; + }, + networkTypes() { const types = [L2VLAN, UNTAGGED]; @@ -149,6 +169,22 @@ export default { return this.type === L2VLAN; }, + isL2VlanTrunkMode() { + if (this.isView) { + return this.value.vlanType === L2VLAN && this.l2VlanMode === TRUNK; + } + + return this.type === L2VLAN && this.l2VlanMode === TRUNK; + }, + + isL2VlanAccessMode() { + if (this.isView) { + return this.value.vlanType === L2VLAN && this.l2VlanMode === ACCESS; + } + + return this.type === L2VLAN && this.l2VlanMode === ACCESS; + }, + isOverlayNetwork() { if (this.isView) { return this.value.vlanType === OVERLAY; @@ -168,11 +204,11 @@ export default { watch: { type(newType) { - if (newType === OVERLAY) { + if (newType === OVERLAY) { // overlay network configuration this.config.type = 'kube-ovn'; this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`; this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock'; - } else { + } else { // l2vlan or untagged network configuration this.config.type = 'bridge'; this.config.promiscMode = true; this.config.ipam = {}; @@ -180,7 +216,20 @@ export default { delete this.config.provider; delete this.config.server_socket; } - } + }, + l2VlanMode(newAccessMode) { + if (this.type !== L2VLAN) { + return; + } + + if (newAccessMode === TRUNK) { // trunk mode + this.config.vlan = 0; + this.config.vlanTrunk = this.vlanTrunk; + } else { // access mode + delete this.config.vlanTrunk; + this.config.vlan = ''; + } + }, }, methods: { @@ -188,7 +237,15 @@ export default { const errors = []; if (this.isL2VlanNetwork || this.isUntaggedNetwork) { - if (!this.config.vlan && !this.isUntaggedNetwork) { + if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.minID === '')) { + errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId') })); + } + + if (this.isL2VlanTrunkMode && this.vlanTrunk.some((trunk) => trunk.maxID === '')) { + errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId') })); + } + + if (this.isL2VlanAccessMode && !this.config.vlan && !this.isUntaggedNetwork) { errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') })); } @@ -217,6 +274,30 @@ export default { await this.save(buttonCb); }, + parseVlanTrunk(config) { + if (config?.vlanTrunk && config?.vlanTrunk?.length > 0) { + return config.vlanTrunk; + } + + return [{ minID: '', maxID: '' }]; + }, + + removeVlanTrunk(trunk) { + removeObject(this.vlanTrunk, trunk); + }, + + addVlanTrunk() { + if (!this.config.vlanTrunk) { + this.config.vlanTrunk = []; + } + this.vlanTrunk.push({ minID: this.minId, maxID: this.maxId }); + this.config.vlanTrunk = this.vlanTrunk; + }, + + vlanTrunkChange() { + this.config.vlanTrunk = this.vlanTrunk; + }, + input(neu) { if (neu === '') { this.config.vlan = ''; @@ -292,8 +373,76 @@ export default { required /> + +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+ + diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml index eabbe8f5..9636d2e8 100644 --- a/pkg/harvester/l10n/en-us.yaml +++ b/pkg/harvester/l10n/en-us.yaml @@ -1482,6 +1482,14 @@ harvester: vlanStatus: vlanConfig: label: Network Configuration + l2VlanMode: + access: Access + trunk: Trunk + label: Mode + vlanTrunk: + minId: Minimum VLAN ID + maxId: Maximum VLAN ID + add: Add VLAN Trunk clusterNetwork: title: Cluster Network Configuration diff --git a/pkg/harvester/models/k8s.cni.cncf.io.networkattachmentdefinition.js b/pkg/harvester/models/k8s.cni.cncf.io.networkattachmentdefinition.js index 0605db0c..83562481 100644 --- a/pkg/harvester/models/k8s.cni.cncf.io.networkattachmentdefinition.js +++ b/pkg/harvester/models/k8s.cni.cncf.io.networkattachmentdefinition.js @@ -2,7 +2,7 @@ import SteveModel from '@shell/plugins/steve/steve-class'; import { HCI } from '@shell/config/labels-annotations'; import { NETWORK_TYPE } from '../config/types'; -const { UNTAGGED, OVERLAY } = NETWORK_TYPE; +const { UNTAGGED, OVERLAY, L2TRUNK_VLAN } = NETWORK_TYPE; export default class NetworkAttachmentDef extends SteveModel { applyDefaults() { @@ -45,7 +45,7 @@ export default class NetworkAttachmentDef extends SteveModel { } get vlanId() { - return this.vlanType === UNTAGGED || this.vlanType === OVERLAY ? 'N/A' : this.parseConfig.vlan; + return this.vlanType === UNTAGGED || this.vlanType === OVERLAY || this.vlanType === L2TRUNK_VLAN ? 'N/A' : this.parseConfig.vlan; } get customValidationRules() { @@ -68,7 +68,7 @@ export default class NetworkAttachmentDef extends SteveModel { const route = annotations[HCI.NETWORK_ROUTE]; let config = {}; - if (this.vlanType === UNTAGGED || this.vlanType === OVERLAY) { + if (this.vlanType === UNTAGGED || this.vlanType === OVERLAY || this.vlanType === L2TRUNK_VLAN) { return 'N/A'; }