diff --git a/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue b/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue
index fb739588..59f323ad 100644
--- a/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue
+++ b/pkg/harvester/edit/harvesterhci.io.networkattachmentdefinition.vue
@@ -9,6 +9,7 @@ import LabeledSelect from '@shell/components/form/LabeledSelect';
import { HCI as HCI_LABELS_ANNOTATIONS } from '@pkg/harvester/config/labels-annotations';
import CreateEditView from '@shell/mixins/create-edit-view';
import { allHash } from '@shell/utils/promise';
+import { NAMESPACE, NODE } from '@shell/config/types';
import { HCI } from '../types';
import { NETWORK_TYPE, L2VLAN_MODE } from '../config/types';
import { removeObject } from '@shell/utils/array';
@@ -20,6 +21,7 @@ const { ACCESS, TRUNK } = L2VLAN_MODE;
const AUTO = 'auto';
const MANUAL = 'manual';
+const KUBE_SYSTEM = 'kube-system';
export default {
emits: ['update:value'],
@@ -70,7 +72,12 @@ export default {
async fetch() {
const inStore = this.$store.getters['currentProduct'].inStore;
- await allHash({ clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }) });
+ await allHash({
+ clusterNetworks: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.CLUSTER_NETWORK }),
+ namespaces: this.$store.dispatch(`${ inStore }/findAll`, { type: NAMESPACE }),
+ nodes: this.$store.dispatch(`${ inStore }/findAll`, { type: NODE }),
+ linkMonitors: this.$store.dispatch(`${ inStore }/findAll`, { type: HCI.LINK_MONITOR }),
+ });
},
created() {
@@ -199,6 +206,80 @@ export default {
}
return this.type === UNTAGGED;
+ },
+
+ showNicsTab() {
+ return this.isOverlayNetwork && this.value.metadata.namespace === KUBE_SYSTEM;
+ },
+
+ namespaceOptions() {
+ const ns = this.$store.getters['harvester/all'](NAMESPACE) || [];
+
+ // Allow users to select the "kube-system" namespace as the external subnet from Kube-OVN.
+ // This expects the provider network to be in the "kube-system" namespace for VPC NAT gateway functionality.
+ return ns
+ .filter((ns) => !ns.isSystem || ns.id === KUBE_SYSTEM)
+ .map((ns) => ({ name: ns.id }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+ },
+
+ nodes() {
+ const inStore = this.$store.getters['currentProduct'].inStore;
+ const nodes = this.$store.getters[`${ inStore }/all`](NODE);
+
+ return nodes.filter((n) => n.isEtcd !== 'true');
+ },
+
+ nics() {
+ const inStore = this.$store.getters['currentProduct'].inStore;
+ const linkMonitor = this.$store.getters[`${ inStore }/byId`](HCI.LINK_MONITOR, 'nic') || {};
+ const linkStatus = linkMonitor?.status?.linkStatus || {};
+ const nodes = this.nodes.map((n) => n.id);
+
+ const out = [];
+
+ // Collect all nics from all nodes (for overlay, we select from all nodes)
+ Object.keys(linkStatus).map((nodeName) => {
+ if (nodes.includes(nodeName)) {
+ const nics = linkStatus[nodeName] || [];
+
+ nics.map((nic) => {
+ out.push({
+ ...nic,
+ nodeName,
+ });
+ });
+ }
+ });
+
+ return out;
+ },
+
+ nicOptions() {
+ const out = [];
+ const seen = new Set();
+
+ (this.nics || []).forEach((nic) => {
+ if (!seen.has(nic.name)) {
+ seen.add(nic.name);
+ out.push({
+ label: nic.name,
+ value: nic.name,
+ });
+ }
+ });
+
+ return out.sort((a, b) => a.label.localeCompare(b.label));
+ },
+
+ master: {
+ get() {
+ return this.config?.master || '';
+ },
+
+ set(value) {
+ this.config.master = value;
+ }
}
},
@@ -214,6 +295,7 @@ export default {
this.config.ipam = {};
this.config.bridge = '';
delete this.config.provider;
+ delete this.config.master;
delete this.config.server_socket;
}
},
@@ -230,6 +312,13 @@ export default {
this.config.vlan = '';
}
},
+ 'value.metadata.namespace'(newNamespace) {
+ // NIC selection is only valid for overlay in kube-system namespace.
+ if (newNamespace !== KUBE_SYSTEM) {
+ delete this.config.master;
+ this.value.spec.config = JSON.stringify({ ...this.config });
+ }
+ },
},
methods: {
@@ -324,6 +413,10 @@ export default {
delete this.config.promiscMode;
delete this.config.vlan;
delete this.config.ipam;
+
+ if (this.value.metadata.namespace !== KUBE_SYSTEM) {
+ delete this.config.master;
+ }
}
if (this.isUntaggedNetwork) {
@@ -350,6 +443,7 @@ export default {
ref="nd"
:value="value"
:mode="mode"
+ :namespace-options="namespaceOptions"
@update:value="$emit('update:value', $event)"
/>
+
+
+
diff --git a/pkg/harvester/l10n/en-us.yaml b/pkg/harvester/l10n/en-us.yaml
index 99be2aed..b1b7d7a2 100644
--- a/pkg/harvester/l10n/en-us.yaml
+++ b/pkg/harvester/l10n/en-us.yaml
@@ -1248,6 +1248,7 @@ harvester:
tabs:
basics: Basics
layer3Network: Route
+ nic: Network Interface Card
clusterNetwork:
label: Cluster Network
create: Create a new cluster network
@@ -1634,8 +1635,9 @@ harvester:
schedulingRules: Select node(s) matching rules
uplink:
nics:
- label: NICs
+ label: NIC
addLabel: Add NIC
+ overlayWarning: The NIC selected here must match the NIC provided in the provider network.
placeholder: Select a NIC that is available on all the selected nodes
validate:
available: NIC "{nic}" is not available on the selected nodes
diff --git a/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue b/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue
index ae6bc9f3..04841360 100644
--- a/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue
+++ b/pkg/harvester/list/harvesterhci.io.networkattachmentdefinition.vue
@@ -166,6 +166,7 @@ export default {
:schema="schema"
:groupable="true"
:rows="filterRows"
+ :ignore-filter="true"
key-field="_key"
>