feat: add l2VlanTrunkMode feature

Signed-off-by: Andy Lee <andy.lee@suse.com>
This commit is contained in:
Andy Lee 2025-09-04 17:00:24 +08:00
parent 1b214b2b6f
commit 6e7a8cfd2f
No known key found for this signature in database
GPG Key ID: EC774C32160918ED
4 changed files with 169 additions and 7 deletions

View File

@ -48,9 +48,12 @@ const FEATURE_FLAGS = {
'cpuMemoryHotplug', 'cpuMemoryHotplug',
'cdiSettings', 'cdiSettings',
'vmCloneRunStrategy', 'vmCloneRunStrategy',
'l2VlanTrunkMode'
], ],
'v1.6.1': [], 'v1.6.1': [],
'v1.7.0': [] 'v1.7.0': [
'l2VlanTrunkMode',
]
}; };
const generateFeatureFlags = () => { const generateFeatureFlags = () => {

View File

@ -23,3 +23,8 @@ export const INTERNAL_STORAGE_CLASS = {
VMSTATE_PERSISTENCE: 'vmstate-persistence', VMSTATE_PERSISTENCE: 'vmstate-persistence',
LONGHORN_STATIC: 'longhorn-static', LONGHORN_STATIC: 'longhorn-static',
}; };
export const L2VLAN_MODE = {
ACCESS: 'access',
TRUNK: 'trunk',
};

View File

@ -10,9 +10,11 @@ import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-anno
import CreateEditView from '@shell/mixins/create-edit-view'; import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise'; import { allHash } from '@shell/utils/promise';
import { HCI } from '../types'; 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 } = NETWORK_TYPE;
const { ACCESS, TRUNK } = L2VLAN_MODE;
const AUTO = 'auto'; const AUTO = 'auto';
const MANUAL = 'manual'; const MANUAL = 'manual';
@ -52,6 +54,8 @@ export default {
return { return {
config, config,
type, type,
l2VlanMode: type === L2VLAN && this.value?.vlanTrunk ? TRUNK : ACCESS,
vlanTrunk: [{ minID: '', maxID: '' }],
layer3Network: { layer3Network: {
mode: layer3Network.mode || AUTO, mode: layer3Network.mode || AUTO,
serverIPAddr: layer3Network.serverIPAddr || '', serverIPAddr: layer3Network.serverIPAddr || '',
@ -108,6 +112,10 @@ export default {
}]; }];
}, },
l2VlanTrunkModeFeatureEnabled() {
return this.$store.getters['harvester-common/getFeatureEnabled']('l2VlanTrunkMode');
},
kubeovnVpcSubnetSupport() { kubeovnVpcSubnetSupport() {
return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet'); return this.$store.getters['harvester-common/getFeatureEnabled']('kubeovnVpcSubnet');
}, },
@ -131,6 +139,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() { networkTypes() {
const types = [L2VLAN, UNTAGGED]; const types = [L2VLAN, UNTAGGED];
@ -149,6 +167,22 @@ export default {
return this.type === L2VLAN; 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() { isOverlayNetwork() {
if (this.isView) { if (this.isView) {
return this.value.vlanType === OVERLAY; return this.value.vlanType === OVERLAY;
@ -168,11 +202,11 @@ export default {
watch: { watch: {
type(newType) { type(newType) {
if (newType === OVERLAY) { if (newType === OVERLAY) { // overlay network configuration
this.config.type = 'kube-ovn'; this.config.type = 'kube-ovn';
this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`; this.config.provider = `${ this.value.metadata.name }.${ this.value.metadata.namespace }.ovn`;
this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock'; this.config.server_socket = '/run/openvswitch/kube-ovn-daemon.sock';
} else { } else { // l2vlan or untagged network configuration
this.config.type = 'bridge'; this.config.type = 'bridge';
this.config.promiscMode = true; this.config.promiscMode = true;
this.config.ipam = {}; this.config.ipam = {};
@ -180,7 +214,20 @@ export default {
delete this.config.provider; delete this.config.provider;
delete this.config.server_socket; 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: { methods: {
@ -188,7 +235,15 @@ export default {
const errors = []; const errors = [];
if (this.isL2VlanNetwork || this.isUntaggedNetwork) { 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') })); errors.push(this.$store.getters['i18n/t']('validation.required', { key: this.t('tableHeaders.networkVlan') }));
} }
@ -217,6 +272,23 @@ export default {
await this.save(buttonCb); await this.save(buttonCb);
}, },
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;
// console.log("🚀 ~ vlanTrunkChange ~ this.config.vlanTrunk:", this.config.vlanTrunk)
},
input(neu) { input(neu) {
if (neu === '') { if (neu === '') {
this.config.vlan = ''; this.config.vlan = '';
@ -292,8 +364,73 @@ export default {
required required
/> />
<LabeledSelect
v-if="isL2VlanNetwork && l2VlanTrunkModeFeatureEnabled"
v-model:value="l2VlanMode"
class="mb-20"
:options="l2VlanModeOptions"
:mode="mode"
:disabled="isEdit"
:label="t('harvester.vlanStatus.vlanConfig.l2VlanMode.label')"
required
/>
<div v-if="isL2VlanTrunkMode && l2VlanTrunkModeFeatureEnabled">
<div
v-for="(trunk, i) in vlanTrunk"
:key="i"
>
<div class="row mt-10">
<div class="col trunk-span">
<LabeledInput
v-model:value.number="trunk.minID"
class="mb-20"
required
:min="1"
:max="4094"
type="number"
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.minId')"
:mode="mode"
@update:value="vlanTrunkChange"
/>
</div>
<div class="col trunk-span">
<LabeledInput
v-model:value.number="trunk.maxID"
class="mb-20"
:max="4094"
:min="1"
required
type="number"
:label="t('harvester.vlanStatus.vlanConfig.vlanTrunk.maxId')"
:mode="mode"
@update:value="vlanTrunkChange"
/>
</div>
<div class="col remove-btn mb-20">
<button
type="button"
:disabled="isView"
:aria-label="t('generic.ariaLabel.remove', {index: i+1})"
role="button"
class="btn role-link"
@click="removeVlanTrunk(trunk)"
>
{{ t('harvester.fields.remove') }}
</button>
</div>
</div>
</div>
<button
v-if="isL2VlanTrunkMode"
type="button"
class="btn btn-sm bg-primary mb-20"
@click="addVlanTrunk"
>
{{ t('harvester.vlanStatus.vlanConfig.vlanTrunk.add') }}
</button>
</div>
<LabeledInput <LabeledInput
v-if="isL2VlanNetwork" v-if="isL2VlanAccessMode"
v-model:value.number="config.vlan" v-model:value.number="config.vlan"
class="mb-20" class="mb-20"
required required
@ -375,3 +512,12 @@ export default {
</Tabbed> </Tabbed>
</CruResource> </CruResource>
</template> </template>
<style lang="scss" scoped>
.remove-btn {
align-self: center;
}
.trunk-span{
flex: 5;
}
</style>

View File

@ -1461,6 +1461,14 @@ harvester:
vlanStatus: vlanStatus:
vlanConfig: vlanConfig:
label: Network Configuration label: Network Configuration
l2VlanMode:
access: Access
trunk: Trunk
label: Mode
vlanTrunk:
minId: Minimum VLAN ID
maxId: Maximum VLAN ID
add: Add VLAN Trunk
clusterNetwork: clusterNetwork:
title: Cluster Network Configuration title: Cluster Network Configuration