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
/>
+