diff --git a/docs/DEVICE-CONNECTIVITY.md b/docs/DEVICE-CONNECTIVITY.md index 589b950426..faabdbb6f1 100644 --- a/docs/DEVICE-CONNECTIVITY.md +++ b/docs/DEVICE-CONNECTIVITY.md @@ -108,6 +108,8 @@ EVE supports: - VLAN sub-interfaces with a LAG as the parent - VLAN filtering for switch network instances. This is actually a feature of the Switch NI, VLAN network adapter is not used in this case. +- Separating VLANs from untagged network traffic: VLAN sub-interfaces enable access to tagged + VLANs, while the parent port can serve as an L3 endpoint for the untagged segment of the network Both VLAN and LAG adapters can be used as ports for Local network instance and for EVE management traffic. diff --git a/docs/images/eve-vlans-and-lags.png b/docs/images/eve-vlans-and-lags.png index 21511eaa08..eec356e854 100644 Binary files a/docs/images/eve-vlans-and-lags.png and b/docs/images/eve-vlans-and-lags.png differ diff --git a/docs/images/eve-vlans-and-lags.xml b/docs/images/eve-vlans-and-lags.xml new file mode 100644 index 0000000000..adfd6baa73 --- /dev/null +++ b/docs/images/eve-vlans-and-lags.xml @@ -0,0 +1,288 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pkg/pillar/cmd/zedagent/parseconfig.go b/pkg/pillar/cmd/zedagent/parseconfig.go index 45c42bff96..77b6789921 100644 --- a/pkg/pillar/cmd/zedagent/parseconfig.go +++ b/pkg/pillar/cmd/zedagent/parseconfig.go @@ -825,12 +825,50 @@ func parseSystemAdapterConfig(getconfigCtx *getconfigContext, config *zconfig.Ed portConfig.Key = "bootstrap" // Instead of "zedagent". } var newPorts []*types.NetworkPortConfig + logicalLabelToAdapterName := make(map[string]string) + for _, sysAdapter := range sysAdapters { + if sysAdapter.LowerLayerName != "" { + logicalLabelToAdapterName[sysAdapter.LowerLayerName] = sysAdapter.Name + } else { + logicalLabelToAdapterName[sysAdapter.Name] = sysAdapter.Name + } + } for _, sysAdapter := range sysAdapters { ports, err := parseOneSystemAdapterConfig(getconfigCtx, sysAdapter, version) if err != nil { portConfig.RecordFailure(err.Error()) } - newPorts = append(newPorts, ports...) + for _, port := range ports { + if port.Logicallabel != sysAdapter.Name { + // If a referenced lower-layer port has its own SystemAdapter assigned, + // skip it here and let it be parsed by its own dedicated + // parseOneSystemAdapterConfig call. + _, hasAdapter := logicalLabelToAdapterName[port.Logicallabel] + if hasAdapter { + continue + } + } + newPorts = append(newPorts, port) + } + } + // Make sure that references to lower-layer ports with a SystemAdapter assigned + // use the SystemAdapter.Name, which differs from the LogicalLabel of the port + // when SystemAdapter.Name differs from SystemAdapter.LowerLayerName. + for _, port := range newPorts { + vlanParent := port.L2LinkConfig.VLAN.ParentPort + if vlanParent != "" { + adapterName := logicalLabelToAdapterName[vlanParent] + if adapterName != "" { + port.L2LinkConfig.VLAN.ParentPort = adapterName + } + } + for i := range port.L2LinkConfig.Bond.AggregatedPorts { + aggregatedPort := port.L2LinkConfig.Bond.AggregatedPorts[i] + adapterName := logicalLabelToAdapterName[aggregatedPort] + if adapterName != "" { + port.L2LinkConfig.Bond.AggregatedPorts[i] = adapterName + } + } } validateAndAssignNetPorts(portConfig, newPorts) @@ -925,6 +963,16 @@ func validateAndAssignNetPorts(dpc *types.DevicePortConfig, newPorts []*types.Ne port2.RecordFailure(errStr) break } + if port.IfName == "" && + port.PCIAddr == port2.PCIAddr && port.USBAddr == port2.USBAddr { + errStr := fmt.Sprintf( + "Port collides with another port with the same physical address (%s, %s)", + port.PCIAddr, port.USBAddr) + log.Error(errStr) + port.RecordFailure(errStr) + port2.RecordFailure(errStr) + break + } } if skip { continue @@ -984,6 +1032,14 @@ func validateAndAssignNetPorts(dpc *types.DevicePortConfig, newPorts []*types.Ne port.RecordFailure(errStr) continue } + if len(l2Refs.bondMasters) > 0 && port.IsL3Port { + errStr := fmt.Sprintf( + "Port %s aggregated by bond (%s) cannot be used with IP configuration", + port.Logicallabel, l2Refs.bondMasters[0].Logicallabel) + log.Error(errStr) + port.RecordFailure(errStr) + continue + } for i, vlanSubIntf := range l2Refs.vlanSubIntfs { for j := 0; j < i; j++ { if vlanSubIntf.VLAN.ID == l2Refs.vlanSubIntfs[j].VLAN.ID { @@ -1003,7 +1059,7 @@ func validateAndAssignNetPorts(dpc *types.DevicePortConfig, newPorts []*types.Ne for len(propagateFrom) > 0 { var propagateFromNext []*types.NetworkPortConfig for _, port := range propagateFrom { - if port.IsL3Port || !port.HasError() { + if !port.HasError() { continue } l2Refs := invertedRefs[port.Logicallabel] diff --git a/pkg/pillar/cmd/zedagent/parseconfig_test.go b/pkg/pillar/cmd/zedagent/parseconfig_test.go index 958f3582c4..7af5a84ccf 100644 --- a/pkg/pillar/cmd/zedagent/parseconfig_test.go +++ b/pkg/pillar/cmd/zedagent/parseconfig_test.go @@ -6,6 +6,7 @@ package zedagent import ( "crypto/sha256" "encoding/hex" + "fmt" "os" "path/filepath" "sort" @@ -362,6 +363,40 @@ func TestParseVlans(t *testing.T) { g.Expect(port.L2LinkConfig.L2Type).To(Equal(types.L2LinkTypeNone)) g.Expect(port.WirelessCfg.WType).To(Equal(types.WirelessTypeNone)) + // With VLAN sub-interfaces configured, the parent interface can be used + // for the untagged traffic. + config.SystemAdapterList = append(config.SystemAdapterList, &zconfig.SystemAdapter{ + Name: "adapter-shopfloor-untagged", + Uplink: false, + NetworkUUID: network1UUID, + LowerLayerName: "shopfloor", + Cost: 5, + }) + parseSystemAdapterConfig(getconfigCtx, config, fromController, true) + portConfig, err = getconfigCtx.pubDevicePortConfig.Get("zedagent") + g.Expect(err).To(BeNil()) + dpc = portConfig.(types.DevicePortConfig) + g.Expect(dpc.HasError()).To(BeFalse()) + // The number of ports has not changed, just the shopfloor ethernet port was + // elevated to L3. + g.Expect(dpc.Ports).To(HaveLen(3)) + sortDPCPorts(&dpc) + port = dpc.Ports[0] + g.Expect(port.Logicallabel).To(Equal("adapter-shopfloor-untagged")) + g.Expect(port.Phylabel).To(Equal("ethernet0")) + g.Expect(port.IsL3Port).To(BeTrue()) // This changed from false to true + g.Expect(port.IfName).To(Equal("eth0")) + g.Expect(port.NetworkUUID.String()).To(Equal(network1UUID)) + g.Expect(port.Cost).To(BeEquivalentTo(5)) + g.Expect(port.DhcpConfig.Type).To(BeEquivalentTo(types.NetworkTypeIPv4)) + g.Expect(port.DhcpConfig.Dhcp).To(Equal(types.DhcpTypeClient)) + g.Expect(port.L2LinkConfig.L2Type).To(Equal(types.L2LinkTypeNone)) + g.Expect(port.WirelessCfg.WType).To(Equal(types.WirelessTypeNone)) + port = dpc.Ports[1] + g.Expect(port.Logicallabel).To(Equal("adapter-shopfloor-vlan100")) + port = dpc.Ports[2] + g.Expect(port.Logicallabel).To(Equal("adapter-shopfloor-vlan200")) + // Add adapter for "warehouse-vlan100" config.SystemAdapterList = append(config.SystemAdapterList, &zconfig.SystemAdapter{ Name: "adapter-warehouse-vlan100", @@ -372,7 +407,6 @@ func TestParseVlans(t *testing.T) { Addr: "192.168.1.150", }) parseSystemAdapterConfig(getconfigCtx, config, fromController, true) - portConfig, err = getconfigCtx.pubDevicePortConfig.Get("zedagent") g.Expect(err).To(BeNil()) dpc = portConfig.(types.DevicePortConfig) @@ -381,7 +415,7 @@ func TestParseVlans(t *testing.T) { g.Expect(dpc.Ports).To(HaveLen(5)) sortDPCPorts(&dpc) // VLAN warehouse.100 - port = dpc.Ports[2] + port = dpc.Ports[3] g.Expect(port.Logicallabel).To(Equal("adapter-warehouse-vlan100")) g.Expect(port.Phylabel).To(BeEmpty()) g.Expect(port.IsL3Port).To(BeTrue()) @@ -530,6 +564,24 @@ func TestParseBonds(t *testing.T) { g.Expect(port.DhcpConfig.Dhcp).To(Equal(types.DhcpTypeNOOP)) g.Expect(port.L2LinkConfig.L2Type).To(Equal(types.L2LinkTypeNone)) g.Expect(port.WirelessCfg.WType).To(Equal(types.WirelessTypeNone)) + + // It is not allowed to use physical port with IP if it is under a bond. + config.SystemAdapterList = append(config.SystemAdapterList, &zconfig.SystemAdapter{ + Name: "shopfloor0", + Uplink: true, + NetworkUUID: networkUUID, + }) + parseSystemAdapterConfig(getconfigCtx, config, fromController, true) + portConfig, err = getconfigCtx.pubDevicePortConfig.Get("zedagent") + g.Expect(err).To(BeNil()) + dpc = portConfig.(types.DevicePortConfig) + g.Expect(dpc.Ports).To(HaveLen(3)) + sortDPCPorts(&dpc) + // underlying physical "shopfloor0" adapter + port = dpc.Ports[1] + g.Expect(port.HasError()).To(BeTrue()) + g.Expect(port.LastError).To(Equal( + "Port shopfloor0 aggregated by bond (adapter-shopfloor) cannot be used with IP configuration")) } func TestParseVlansOverBonds(t *testing.T) { @@ -717,6 +769,72 @@ func TestParseVlansOverBonds(t *testing.T) { g.Expect(port.DhcpConfig.Dhcp).To(Equal(types.DhcpTypeNOOP)) g.Expect(port.L2LinkConfig.L2Type).To(Equal(types.L2LinkTypeNone)) g.Expect(port.WirelessCfg.WType).To(Equal(types.WirelessTypeNone)) + + // With VLAN sub-interfaces configured, the parent interface can be used + // for the untagged traffic. + config.SystemAdapterList = append(config.SystemAdapterList, &zconfig.SystemAdapter{ + Name: "adapter-shopfloor-untagged", + Uplink: false, + NetworkUUID: network1UUID, + LowerLayerName: "bond-shopfloor", + Cost: 2, + }) + parseSystemAdapterConfig(getconfigCtx, config, fromController, true) + portConfig, err = getconfigCtx.pubDevicePortConfig.Get("zedagent") + g.Expect(err).To(BeNil()) + dpc = portConfig.(types.DevicePortConfig) + g.Expect(dpc.HasError()).To(BeFalse()) + // The number of ports has not changed, just the shopfloor bond was elevated to L3. + g.Expect(dpc.Ports).To(HaveLen(5)) + sortDPCPorts(&dpc) + port = dpc.Ports[0] + g.Expect(port.Logicallabel).To(Equal("adapter-shopfloor-untagged")) + g.Expect(port.Phylabel).To(BeEmpty()) + g.Expect(port.IsL3Port).To(BeTrue()) + g.Expect(port.IsMgmt).To(BeFalse()) + g.Expect(port.IfName).To(Equal("bond")) + g.Expect(port.NetworkUUID.String()).To(Equal(network1UUID)) + g.Expect(port.Cost).To(BeEquivalentTo(2)) + g.Expect(port.DhcpConfig.Type).To(BeEquivalentTo(types.NetworkTypeIPv4)) + g.Expect(port.DhcpConfig.Dhcp).To(Equal(types.DhcpTypeClient)) + g.Expect(port.L2LinkConfig.L2Type).To(Equal(types.L2LinkTypeBond)) + g.Expect(port.L2LinkConfig.Bond.AggregatedPorts).To(Equal([]string{"shopfloor1", "shopfloor0"})) + g.Expect(port.L2LinkConfig.Bond.Mode).To(Equal(types.BondModeActiveBackup)) + g.Expect(port.L2LinkConfig.Bond.MIIMonitor.Enabled).To(BeTrue()) + g.Expect(port.L2LinkConfig.Bond.MIIMonitor.Interval).To(BeEquivalentTo(400)) + g.Expect(port.L2LinkConfig.Bond.MIIMonitor.UpDelay).To(BeEquivalentTo(800)) + g.Expect(port.L2LinkConfig.Bond.MIIMonitor.DownDelay).To(BeEquivalentTo(1200)) + g.Expect(port.L2LinkConfig.Bond.ARPMonitor.Enabled).To(BeFalse()) + g.Expect(port.WirelessCfg.WType).To(Equal(types.WirelessTypeNone)) + port = dpc.Ports[1] + g.Expect(port.Logicallabel).To(Equal("adapter-shopfloor-vlan100")) + port = dpc.Ports[2] + g.Expect(port.Logicallabel).To(Equal("adapter-shopfloor-vlan200")) + port = dpc.Ports[3] + g.Expect(port.Logicallabel).To(Equal("shopfloor0")) + port = dpc.Ports[4] + g.Expect(port.Logicallabel).To(Equal("shopfloor1")) + + // It is not allowed to use physical port with IP if it is under a bond. + config.SystemAdapterList = append(config.SystemAdapterList, &zconfig.SystemAdapter{ + Name: "adapter-ethernet0", + Uplink: true, + LowerLayerName: "shopfloor0", + NetworkUUID: network1UUID, + }) + parseSystemAdapterConfig(getconfigCtx, config, fromController, true) + portConfig, err = getconfigCtx.pubDevicePortConfig.Get("zedagent") + g.Expect(err).To(BeNil()) + dpc = portConfig.(types.DevicePortConfig) + g.Expect(dpc.Ports).To(HaveLen(5)) + sortDPCPorts(&dpc) + port = dpc.Ports[0] + fmt.Printf("%+v\n", dpc) + g.Expect(port.Logicallabel).To(Equal("adapter-ethernet0")) + g.Expect(port.HasError()).To(BeTrue()) + g.Expect(port.LastError).To(Equal( + "Port adapter-ethernet0 aggregated by bond (adapter-shopfloor-untagged) cannot be used with IP configuration")) + } func TestInvalidLowerLayerReferences(t *testing.T) { @@ -799,9 +917,9 @@ func TestInvalidLowerLayerReferences(t *testing.T) { dpc := portConfig.(types.DevicePortConfig) g.Expect(dpc.HasError()).To(BeTrue()) g.Expect(getPortError(&dpc, "adapter1")). - To(ContainSubstring("Port collides with another port")) + To(ContainSubstring("Port collides with another port with the same interface name")) g.Expect(getPortError(&dpc, "adapter2")). - To(ContainSubstring("Port collides with another port")) + To(ContainSubstring("Port collides with another port with the same interface name")) g.Expect(dpc.Ports).To(HaveLen(2)) // fix: @@ -889,7 +1007,7 @@ func TestInvalidLowerLayerReferences(t *testing.T) { g.Expect(dpc.HasError()).To(BeFalse()) g.Expect(dpc.Ports).To(HaveLen(3)) - // Scenario 4: interface referenced by both a system adapter and a L2 object + // Scenario 4: interface referenced by both a system adapter and a bond config = &zconfig.EdgeDevConfig{ Bonds: []*zconfig.BondAdapter{ { @@ -921,10 +1039,8 @@ func TestInvalidLowerLayerReferences(t *testing.T) { dpc = portConfig.(types.DevicePortConfig) g.Expect(dpc.HasError()).To(BeTrue()) g.Expect(getPortError(&dpc, "adapter-warehouse")). - To(ContainSubstring("Port collides with another port")) - g.Expect(getPortError(&dpc, "adapter-bond-shopfloor")). - To(ContainSubstring("Port collides with another port")) - g.Expect(dpc.Ports).To(HaveLen(4)) + To(Equal("Port adapter-warehouse aggregated by bond (adapter-bond-shopfloor) cannot be used with IP configuration")) + g.Expect(dpc.Ports).To(HaveLen(3)) // fix: config.Bonds[0].LowerLayerNames = []string{"shopfloor"} @@ -1142,6 +1258,67 @@ func TestInvalidLowerLayerReferences(t *testing.T) { dpc = portConfig.(types.DevicePortConfig) g.Expect(dpc.HasError()).To(BeFalse()) g.Expect(dpc.Ports).To(HaveLen(2)) + + // Scenario 10: System adapters referencing the same underlying port by physical addresses + // Note that we allow only wwan ports to be defined without interface name. + config = &zconfig.EdgeDevConfig{ + DeviceIoList: []*zconfig.PhysicalIO{ + { + Ptype: zcommon.PhyIoType_PhyIoNetWWAN, + Phylabel: "ethernet0", + Logicallabel: "shopfloor", + Phyaddrs: map[string]string{ + "pcilong": "0000:f4:00.0", + }, + Usage: zcommon.PhyIoMemberUsage_PhyIoUsageMgmtAndApps, + }, + { + Ptype: zcommon.PhyIoType_PhyIoNetWWAN, + Phylabel: "ethernet1", + Logicallabel: "warehouse", + Phyaddrs: map[string]string{ + "pcilong": "0000:05:00.0", + }, + Usage: zcommon.PhyIoMemberUsage_PhyIoUsageMgmtAndApps, + }, + }, + SystemAdapterList: []*zconfig.SystemAdapter{ + { + Name: "adapter1", + Uplink: true, + NetworkUUID: network1UUID, + LowerLayerName: "shopfloor", + Cost: 10, + }, + { + Name: "adapter2", + Uplink: true, + NetworkUUID: network2UUID, + LowerLayerName: "shopfloor", + Cost: 20, + }, + }, + } + parseDeviceIoListConfig(getconfigCtx, config) + parseSystemAdapterConfig(getconfigCtx, config, fromController, true) + portConfig, err = getconfigCtx.pubDevicePortConfig.Get("zedagent") + g.Expect(err).To(BeNil()) + dpc = portConfig.(types.DevicePortConfig) + g.Expect(dpc.HasError()).To(BeTrue()) + g.Expect(getPortError(&dpc, "adapter1")). + To(ContainSubstring("Port collides with another port with the same physical address")) + g.Expect(getPortError(&dpc, "adapter2")). + To(ContainSubstring("Port collides with another port with the same physical address")) + g.Expect(dpc.Ports).To(HaveLen(2)) + + // fix: + config.SystemAdapterList[1].LowerLayerName = "warehouse" + parseSystemAdapterConfig(getconfigCtx, config, fromController, true) + portConfig, err = getconfigCtx.pubDevicePortConfig.Get("zedagent") + g.Expect(err).To(BeNil()) + dpc = portConfig.(types.DevicePortConfig) + g.Expect(dpc.HasError()).To(BeFalse()) + g.Expect(dpc.Ports).To(HaveLen(2)) } func TestParseSRIOV(t *testing.T) { diff --git a/pkg/pillar/cmd/zedrouter/networkinstance.go b/pkg/pillar/cmd/zedrouter/networkinstance.go index 5464b0f00c..cbfbc08276 100644 --- a/pkg/pillar/cmd/zedrouter/networkinstance.go +++ b/pkg/pillar/cmd/zedrouter/networkinstance.go @@ -167,6 +167,14 @@ func (z *zedrouter) updateNIPorts(niConfig types.NetworkInstanceConfig, port.Logicallabel)) continue } + if z.deviceNetworkStatus.IsPortUsedAsVlanParent(port.Logicallabel) { + // It is not supported/valid to bridge port which has VLAN + // sub-interfaces configured. + errorMsgs = append(errorMsgs, + fmt.Sprintf("port %s with VLAN sub-interfaces cannot be used "+ + "in Switch Network Instance", port.Logicallabel)) + continue + } if len(newPorts) > 1 && port.Dhcp != types.DhcpTypeNone { errorMsgs = append(errorMsgs, fmt.Sprintf( diff --git a/pkg/pillar/dpcreconciler/genericitems/netio.go b/pkg/pillar/dpcreconciler/genericitems/netio.go index 28e4172d33..40dafaaf13 100644 --- a/pkg/pillar/dpcreconciler/genericitems/netio.go +++ b/pkg/pillar/dpcreconciler/genericitems/netio.go @@ -19,6 +19,9 @@ const ( IOUsageL3Adapter // IOUsageVlanParent : network IO is used as VLAN parent interface. IOUsageVlanParent + // IOUsageVlanParentAndL3Adapter : network IO is used as a VLAN parent interface + // and at the same time as an L3 endpoint (for untagged traffic). + IOUsageVlanParentAndL3Adapter // IOUsageBondAggrIf : network IO is aggregated by Bond interface. IOUsageBondAggrIf ) diff --git a/pkg/pillar/dpcreconciler/linux.go b/pkg/pillar/dpcreconciler/linux.go index acf50567ac..43ce725e67 100644 --- a/pkg/pillar/dpcreconciler/linux.go +++ b/pkg/pillar/dpcreconciler/linux.go @@ -954,7 +954,7 @@ func (r *LinuxDpcReconciler) getIntendedPhysicalIfs(dpc types.DevicePortConfig) } switch port.L2Type { case types.L2LinkTypeNone: - if port.IsL3Port { + if port.IsL3Port && !dpc.IsPortUsedAsVlanParent(port.Logicallabel) { intendedIfs.PutItem(linux.PhysIf{ PhysIfLL: port.Logicallabel, PhysIfName: port.IfName, @@ -965,11 +965,15 @@ func (r *LinuxDpcReconciler) getIntendedPhysicalIfs(dpc types.DevicePortConfig) } case types.L2LinkTypeVLAN: parent := dpc.LookupPortByLogicallabel(port.VLAN.ParentPort) + usage := generic.IOUsageVlanParent + if parent.IsL3Port { + usage = generic.IOUsageVlanParentAndL3Adapter + } if parent != nil && parent.L2Type == types.L2LinkTypeNone { intendedIfs.PutItem(linux.PhysIf{ PhysIfLL: parent.Logicallabel, PhysIfName: parent.IfName, - Usage: generic.IOUsageVlanParent, + Usage: usage, WirelessType: port.WirelessCfg.WType, MTU: r.intfMTU[port.Logicallabel], }, nil) @@ -1007,13 +1011,14 @@ func (r *LinuxDpcReconciler) getIntendedLogicalIO(dpc types.DevicePortConfig) dg parent := dpc.LookupPortByLogicallabel(port.VLAN.ParentPort) if parent != nil { vlan := linux.Vlan{ - LogicalLabel: port.Logicallabel, - IfName: port.IfName, - ParentLL: port.VLAN.ParentPort, - ParentIfName: parent.IfName, - ParentL2Type: parent.L2Type, - ID: port.VLAN.ID, - MTU: r.intfMTU[port.Logicallabel], + LogicalLabel: port.Logicallabel, + IfName: port.IfName, + ParentLL: port.VLAN.ParentPort, + ParentIfName: parent.IfName, + ParentL2Type: parent.L2Type, + ParentIsL3Port: parent.IsL3Port, + ID: port.VLAN.ID, + MTU: r.intfMTU[port.Logicallabel], } intendedIO.PutItem(vlan, nil) } @@ -1026,13 +1031,14 @@ func (r *LinuxDpcReconciler) getIntendedLogicalIO(dpc types.DevicePortConfig) dg } } var usage generic.IOUsage - if port.IsL3Port { + if dpc.IsPortUsedAsVlanParent(port.Logicallabel) { + if port.IsL3Port { + usage = generic.IOUsageVlanParentAndL3Adapter + } else { + usage = generic.IOUsageVlanParent + } + } else if port.IsL3Port { usage = generic.IOUsageL3Adapter - } else { - // Nothing other than VLAN is supported at the higher-layer currently. - // It is also possible that the bond is not being used at all, but we - // do not need to treat that case differently. - usage = generic.IOUsageVlanParent } intendedIO.PutItem(linux.Bond{ BondConfig: port.Bond, @@ -1077,12 +1083,13 @@ func (r *LinuxDpcReconciler) getIntendedAdapters(dpc types.DevicePortConfig) dg. continue } adapter := linux.Adapter{ - LogicalLabel: port.Logicallabel, - IfName: port.IfName, - L2Type: port.L2Type, - WirelessType: port.WirelessCfg.WType, - DhcpType: port.Dhcp, - MTU: r.intfMTU[port.Logicallabel], + LogicalLabel: port.Logicallabel, + IfName: port.IfName, + L2Type: port.L2Type, + WirelessType: port.WirelessCfg.WType, + UsedAsVlanParent: dpc.IsPortUsedAsVlanParent(port.Logicallabel), + DhcpType: port.Dhcp, + MTU: r.intfMTU[port.Logicallabel], } intendedAdapters.PutItem(adapter, nil) if port.Dhcp != types.DhcpTypeNone && diff --git a/pkg/pillar/dpcreconciler/linux_test.go b/pkg/pillar/dpcreconciler/linux_test.go index 8f129cba9d..b6afb12e9f 100644 --- a/pkg/pillar/dpcreconciler/linux_test.go +++ b/pkg/pillar/dpcreconciler/linux_test.go @@ -857,6 +857,7 @@ func TestVlansAndBonds(test *testing.T) { t.Expect(itemIsCreatedWithLabel("dhcpcd for shopfloor-vlan100")).To(BeTrue()) t.Expect(itemIsCreatedWithLabel("dhcpcd for shopfloor-vlan200")).To(BeTrue()) + t.Expect(itemIsCreatedWithLabel("dhcpcd for bond-shopfloor")).To(BeFalse()) currentState, release := dpcReconciler.GetCurrentState() bondRef := dg.Reference(linux.Bond{IfName: "bond0"}) @@ -871,6 +872,7 @@ func TestVlansAndBonds(test *testing.T) { t.Expect(bond.MIIMonitor.Interval).To(BeEquivalentTo(400)) t.Expect(bond.MIIMonitor.UpDelay).To(BeEquivalentTo(800)) t.Expect(bond.MIIMonitor.DownDelay).To(BeEquivalentTo(1200)) + t.Expect(bond.Usage).To(Equal(generic.IOUsageVlanParent)) t.Expect(bond.MTU).To(BeEquivalentTo(3000)) // max of VLAN sub-interfaces vlan100Ref := dg.Reference(linux.Vlan{IfName: "shopfloor.100"}) @@ -881,6 +883,7 @@ func TestVlansAndBonds(test *testing.T) { t.Expect(vlan100.ID).To(BeEquivalentTo(100)) t.Expect(vlan100.ParentLL).To(BeEquivalentTo("bond-shopfloor")) t.Expect(vlan100.ParentIfName).To(BeEquivalentTo("bond0")) + t.Expect(vlan100.ParentIsL3Port).To(BeFalse()) t.Expect(vlan100.MTU).To(BeEquivalentTo(2000)) vlan200Ref := dg.Reference(linux.Vlan{IfName: "shopfloor.200"}) @@ -891,6 +894,7 @@ func TestVlansAndBonds(test *testing.T) { t.Expect(vlan200.ID).To(BeEquivalentTo(200)) t.Expect(vlan200.ParentLL).To(BeEquivalentTo("bond-shopfloor")) t.Expect(vlan200.ParentIfName).To(BeEquivalentTo("bond0")) + t.Expect(vlan200.ParentIsL3Port).To(BeFalse()) t.Expect(vlan200.MTU).To(BeEquivalentTo(3000)) vlan100AdapterRef := dg.Reference(linux.Adapter{IfName: "shopfloor.100"}) @@ -900,6 +904,7 @@ func TestVlansAndBonds(test *testing.T) { t.Expect(vlan100Adapter.IfName).To(Equal("shopfloor.100")) t.Expect(vlan100Adapter.L2Type).To(BeEquivalentTo(types.L2LinkTypeVLAN)) t.Expect(vlan100Adapter.MTU).To(BeEquivalentTo(2000)) + t.Expect(vlan100Adapter.UsedAsVlanParent).To(BeFalse()) vlan200AdapterRef := dg.Reference(linux.Adapter{IfName: "shopfloor.200"}) item, _, _, found = currentState.Item(vlan200AdapterRef) @@ -908,6 +913,7 @@ func TestVlansAndBonds(test *testing.T) { t.Expect(vlan200Adapter.IfName).To(Equal("shopfloor.200")) t.Expect(vlan200Adapter.L2Type).To(BeEquivalentTo(types.L2LinkTypeVLAN)) t.Expect(vlan200Adapter.MTU).To(BeEquivalentTo(3000)) + t.Expect(vlan200Adapter.UsedAsVlanParent).To(BeFalse()) eth0Ref := dg.Reference(linux.PhysIf{PhysIfName: "eth0"}) item, _, _, found = currentState.Item(eth0Ref) @@ -927,4 +933,103 @@ func TestVlansAndBonds(test *testing.T) { t.Expect(eth1If.MasterIfName).To(BeEquivalentTo("bond0")) t.Expect(eth1If.MTU).To(BeEquivalentTo(3000)) // max of all higher-layer ports release() + + // VLAN parent can be also used as an L3 endpoint for untagged traffic. + dpc.Ports[2].IsL3Port = true + dpc.Ports[2].IsMgmt = false + dpc.Ports[2].MTU = 4000 + dpc.Ports[2].DhcpConfig = types.DhcpConfig{ + Dhcp: types.DhcpTypeClient, + Type: types.NetworkTypeIPv4, + } + + ctx = reconciler.MockRun(context.Background()) + status = dpcReconciler.Reconcile(ctx, dpcrec.Args{GCP: *gcp, DPC: dpc, AA: aa}) + t.Expect(status.Error).To(BeNil()) + + t.Expect(itemCountWithType(generic.PhysIfTypename)).To(Equal(2)) + t.Expect(itemCountWithType(generic.BondTypename)).To(Equal(1)) + t.Expect(itemCountWithType(generic.VlanTypename)).To(Equal(2)) + t.Expect(itemCountWithType(generic.AdapterTypename)).To(Equal(3)) + + t.Expect(itemIsCreatedWithLabel("dhcpcd for shopfloor-vlan100")).To(BeTrue()) + t.Expect(itemIsCreatedWithLabel("dhcpcd for shopfloor-vlan200")).To(BeTrue()) + t.Expect(itemIsCreatedWithLabel("dhcpcd for bond-shopfloor")).To(BeTrue()) + + currentState, release = dpcReconciler.GetCurrentState() + item, _, _, found = currentState.Item(bondRef) + t.Expect(found).To(BeTrue()) + bond = item.(linux.Bond) + t.Expect(bond.IfName).To(Equal("bond0")) + t.Expect(bond.AggregatedPorts).To(Equal([]string{"shopfloor0", "shopfloor1"})) + t.Expect(bond.AggregatedIfNames).To(Equal([]string{"eth0", "eth1"})) + t.Expect(bond.ARPMonitor.Enabled).To(BeFalse()) + t.Expect(bond.MIIMonitor.Enabled).To(BeTrue()) + t.Expect(bond.MIIMonitor.Interval).To(BeEquivalentTo(400)) + t.Expect(bond.MIIMonitor.UpDelay).To(BeEquivalentTo(800)) + t.Expect(bond.MIIMonitor.DownDelay).To(BeEquivalentTo(1200)) + t.Expect(bond.Usage).To(Equal(generic.IOUsageVlanParentAndL3Adapter)) + t.Expect(bond.MTU).To(BeEquivalentTo(4000)) // from bond adapter + + item, _, _, found = currentState.Item(vlan100Ref) + t.Expect(found).To(BeTrue()) + vlan100 = item.(linux.Vlan) + t.Expect(vlan100.IfName).To(Equal("shopfloor.100")) + t.Expect(vlan100.ID).To(BeEquivalentTo(100)) + t.Expect(vlan100.ParentLL).To(BeEquivalentTo("bond-shopfloor")) + t.Expect(vlan100.ParentIfName).To(BeEquivalentTo("bond0")) + t.Expect(vlan100.ParentIsL3Port).To(BeTrue()) + t.Expect(vlan100.MTU).To(BeEquivalentTo(2000)) + + item, _, _, found = currentState.Item(vlan200Ref) + t.Expect(found).To(BeTrue()) + vlan200 = item.(linux.Vlan) + t.Expect(vlan200.IfName).To(Equal("shopfloor.200")) + t.Expect(vlan200.ID).To(BeEquivalentTo(200)) + t.Expect(vlan200.ParentLL).To(BeEquivalentTo("bond-shopfloor")) + t.Expect(vlan200.ParentIfName).To(BeEquivalentTo("bond0")) + t.Expect(vlan200.ParentIsL3Port).To(BeTrue()) + t.Expect(vlan200.MTU).To(BeEquivalentTo(3000)) + + item, _, _, found = currentState.Item(vlan100AdapterRef) + t.Expect(found).To(BeTrue()) + vlan100Adapter = item.(linux.Adapter) + t.Expect(vlan100Adapter.IfName).To(Equal("shopfloor.100")) + t.Expect(vlan100Adapter.L2Type).To(BeEquivalentTo(types.L2LinkTypeVLAN)) + t.Expect(vlan100Adapter.MTU).To(BeEquivalentTo(2000)) + t.Expect(vlan100Adapter.UsedAsVlanParent).To(BeFalse()) + + item, _, _, found = currentState.Item(vlan200AdapterRef) + t.Expect(found).To(BeTrue()) + vlan200Adapter = item.(linux.Adapter) + t.Expect(vlan200Adapter.IfName).To(Equal("shopfloor.200")) + t.Expect(vlan200Adapter.L2Type).To(BeEquivalentTo(types.L2LinkTypeVLAN)) + t.Expect(vlan200Adapter.MTU).To(BeEquivalentTo(3000)) + t.Expect(vlan200Adapter.UsedAsVlanParent).To(BeFalse()) + + bondAdapterRef := dg.Reference(linux.Adapter{IfName: "bond0"}) + item, _, _, found = currentState.Item(bondAdapterRef) + t.Expect(found).To(BeTrue()) + bondAdapter := item.(linux.Adapter) + t.Expect(bondAdapter.IfName).To(Equal("bond0")) + t.Expect(bondAdapter.L2Type).To(BeEquivalentTo(types.L2LinkTypeBond)) + t.Expect(bondAdapter.MTU).To(BeEquivalentTo(4000)) + t.Expect(bondAdapter.UsedAsVlanParent).To(BeTrue()) + + item, _, _, found = currentState.Item(eth0Ref) + t.Expect(found).To(BeTrue()) + eth0If = item.(linux.PhysIf) + t.Expect(eth0If.PhysIfName).To(Equal("eth0")) + t.Expect(eth0If.Usage).To(BeEquivalentTo(generic.IOUsageBondAggrIf)) + t.Expect(eth0If.MasterIfName).To(BeEquivalentTo("bond0")) + t.Expect(eth0If.MTU).To(BeEquivalentTo(4000)) // MTU from bond adapter + + item, _, _, found = currentState.Item(eth1Ref) + t.Expect(found).To(BeTrue()) + eth1If = item.(linux.PhysIf) + t.Expect(eth1If.PhysIfName).To(Equal("eth1")) + t.Expect(eth1If.Usage).To(BeEquivalentTo(generic.IOUsageBondAggrIf)) + t.Expect(eth1If.MasterIfName).To(BeEquivalentTo("bond0")) + t.Expect(eth1If.MTU).To(BeEquivalentTo(4000)) // MTU from bond adapter + release() } diff --git a/pkg/pillar/dpcreconciler/linuxitems/adapter.go b/pkg/pillar/dpcreconciler/linuxitems/adapter.go index bab56c5a36..9dfdd37396 100644 --- a/pkg/pillar/dpcreconciler/linuxitems/adapter.go +++ b/pkg/pillar/dpcreconciler/linuxitems/adapter.go @@ -29,6 +29,11 @@ type Adapter struct { L2Type types.L2LinkType // WirelessType is used to distinguish between Ethernet, WiFi and cellular port. WirelessType types.WirelessType + // UsedAsVlanParent is true if the network adapter is not only used as L3 endpoint + // but also as a parent interface for VLAN sub-interfaces. + // In such case the network adapter is not put under a bridge (and cannot be used + // with Switch NI). + UsedAsVlanParent bool // DhcpType is used to determine the method used to obtain IP address for the network // adapter. DhcpType types.DhcpType @@ -56,6 +61,7 @@ func (a Adapter) Equal(other depgraph.Item) bool { a2 := other.(Adapter) return a.L2Type == a2.L2Type && a.WirelessType == a2.WirelessType && + a.UsedAsVlanParent == a2.UsedAsVlanParent && a.DhcpType == a2.DhcpType && a.MTU == a2.MTU } @@ -75,6 +81,10 @@ func (a Adapter) String() string { func (a Adapter) Dependencies() (deps []depgraph.Dependency) { var depType string var mustSatisfy func(item depgraph.Item) bool + expectedParentUsage := genericitems.IOUsageL3Adapter + if a.UsedAsVlanParent { + expectedParentUsage = genericitems.IOUsageVlanParentAndL3Adapter + } switch a.L2Type { case types.L2LinkTypeNone: // Attached directly to a physical interface. @@ -82,12 +92,16 @@ func (a Adapter) Dependencies() (deps []depgraph.Dependency) { depType = genericitems.PhysIfTypename mustSatisfy = func(item depgraph.Item) bool { physIf := item.(PhysIf) - return physIf.Usage == genericitems.IOUsageL3Adapter + return physIf.Usage == expectedParentUsage } case types.L2LinkTypeVLAN: depType = genericitems.VlanTypename case types.L2LinkTypeBond: depType = genericitems.BondTypename + mustSatisfy = func(item depgraph.Item) bool { + bond := item.(Bond) + return bond.Usage == expectedParentUsage + } } return []depgraph.Dependency{ { @@ -225,12 +239,13 @@ func (c *AdapterConfigurator) Create(ctx context.Context, item depgraph.Item) er // Return true if NIM is responsible for creating a Linux bridge for the adapter. // Bridge is NOT created by NIM if: // - the adapter is wireless: it is not valid to put wireless adapter under a bridge, +// - or if the adapter has VLAN sub-interfaces and therefore cannot be bridged, // - or if the adapter is configured with DHCP passthrough: in that case, NIM does not // have to apply IP config or test connectivity, and it can leave it up to zedrouter // to bridge the port with applications (and possibly also with other ports) if requested // by the user func (c *AdapterConfigurator) isAdapterBridgedByNIM(adapter Adapter) bool { - return adapter.WirelessType == types.WirelessTypeNone && + return adapter.WirelessType == types.WirelessTypeNone && !adapter.UsedAsVlanParent && (adapter.DhcpType == types.DhcpTypeClient || adapter.DhcpType == types.DhcpTypeStatic) } @@ -357,5 +372,6 @@ func (c *AdapterConfigurator) NeedsRecreate(oldItem, newItem depgraph.Item) (rec } return oldCfg.L2Type != newCfg.L2Type || oldCfg.WirelessType != newCfg.WirelessType || + oldCfg.UsedAsVlanParent != newCfg.UsedAsVlanParent || oldCfg.DhcpType != newCfg.DhcpType } diff --git a/pkg/pillar/dpcreconciler/linuxitems/bond.go b/pkg/pillar/dpcreconciler/linuxitems/bond.go index de7556f887..5a63f426e0 100644 --- a/pkg/pillar/dpcreconciler/linuxitems/bond.go +++ b/pkg/pillar/dpcreconciler/linuxitems/bond.go @@ -6,8 +6,6 @@ package linuxitems import ( "context" "fmt" - "reflect" - "github.com/vishvananda/netlink" "github.com/lf-edge/eve-libs/depgraph" @@ -15,6 +13,7 @@ import ( "github.com/lf-edge/eve/pkg/pillar/dpcreconciler/genericitems" "github.com/lf-edge/eve/pkg/pillar/netmonitor" "github.com/lf-edge/eve/pkg/pillar/types" + "github.com/lf-edge/eve/pkg/pillar/utils/generics" ) // Bond : Bond interface. @@ -52,8 +51,12 @@ func (b Bond) Type() string { // Equal is a comparison method for two equally-named Bond instances. func (b Bond) Equal(other depgraph.Item) bool { b2 := other.(Bond) - return reflect.DeepEqual(b.BondConfig, b2.BondConfig) && - reflect.DeepEqual(b.AggregatedIfNames, b2.AggregatedIfNames) && + return b.LacpRate == b2.LacpRate && + b.MIIMonitor == b2.MIIMonitor && + b.Mode == b2.Mode && + b.ARPMonitor.Equal(b2.ARPMonitor) && + generics.EqualSets(b.AggregatedIfNames, b2.AggregatedIfNames) && + b.Usage == b2.Usage && b.MTU == b2.MTU } @@ -320,7 +323,10 @@ func (c *BondConfigurator) Delete(ctx context.Context, item depgraph.Item) error func (c *BondConfigurator) NeedsRecreate(oldItem, newItem depgraph.Item) (recreate bool) { oldBondCfg := oldItem.(Bond) newBondCfg := newItem.(Bond) - if !reflect.DeepEqual(oldBondCfg.BondConfig, newBondCfg.BondConfig) { + if oldBondCfg.LacpRate != newBondCfg.LacpRate || + oldBondCfg.MIIMonitor != newBondCfg.MIIMonitor || + oldBondCfg.Mode != newBondCfg.Mode || + !oldBondCfg.ARPMonitor.Equal(newBondCfg.ARPMonitor) { return true } if oldBondCfg.Usage != newBondCfg.Usage { diff --git a/pkg/pillar/dpcreconciler/linuxitems/vlan.go b/pkg/pillar/dpcreconciler/linuxitems/vlan.go index 2a4de9ae2e..ffd3f3f852 100644 --- a/pkg/pillar/dpcreconciler/linuxitems/vlan.go +++ b/pkg/pillar/dpcreconciler/linuxitems/vlan.go @@ -28,6 +28,9 @@ type Vlan struct { ParentIfName string // ParentL2Type : link type of the parent interface (bond or physical). ParentL2Type types.L2LinkType + // ParentIsL3Port is true when the parent port is used both as a VLAN parent + // and a L3 endpoint (for untagged traffic) at the same time. + ParentIsL3Port bool // VLAN ID. ID uint16 // MTU : Maximum transmission unit size. @@ -54,6 +57,7 @@ func (v Vlan) Equal(other depgraph.Item) bool { v2 := other.(Vlan) return v.ParentIfName == v2.ParentIfName && v.ParentL2Type == v2.ParentL2Type && + v.ParentIsL3Port == v2.ParentIsL3Port && v.ID == v2.ID && v.MTU == v2.MTU } @@ -72,6 +76,10 @@ func (v Vlan) String() string { func (v Vlan) Dependencies() (deps []depgraph.Dependency) { var depType string var mustSatisfy func(item depgraph.Item) bool + expectedParentUsage := genericitems.IOUsageVlanParent + if v.ParentIsL3Port { + expectedParentUsage = genericitems.IOUsageVlanParentAndL3Adapter + } switch v.ParentL2Type { case types.L2LinkTypeNone: // Attached directly to a physical interface. @@ -85,7 +93,7 @@ func (v Vlan) Dependencies() (deps []depgraph.Dependency) { return false } // The physical interface has to be "allocated" for use as a VLAN parent. - if physIf.Usage != genericitems.IOUsageVlanParent { + if physIf.Usage != expectedParentUsage { return false } // MTU of the parent interface must not be smaller. @@ -101,6 +109,10 @@ func (v Vlan) Dependencies() (deps []depgraph.Dependency) { // unreachable return false } + // The bond interface has to be "allocated" for use as a VLAN parent. + if bond.Usage != expectedParentUsage { + return false + } // MTU of the parent interface must not be smaller. return bond.GetMTU() >= v.GetMTU() } @@ -235,5 +247,6 @@ func (c *VlanConfigurator) NeedsRecreate(oldItem, newItem depgraph.Item) (recrea } return oldCfg.ParentIfName != newCfg.ParentIfName || oldCfg.ParentL2Type != newCfg.ParentL2Type || + oldCfg.ParentIsL3Port != newCfg.ParentIsL3Port || oldCfg.ID != newCfg.ID } diff --git a/pkg/pillar/types/dns.go b/pkg/pillar/types/dns.go index 3ca83159d6..e648e4d04d 100644 --- a/pkg/pillar/types/dns.go +++ b/pkg/pillar/types/dns.go @@ -362,6 +362,17 @@ func (status DeviceNetworkStatus) GetPortAddrInfo(ifname string, addr net.IP) *A return nil } +// IsPortUsedAsVlanParent - returns true if port with the given logical label +// is used as a VLAN parent interface. +func (status DeviceNetworkStatus) IsPortUsedAsVlanParent(portLabel string) bool { + for _, port2 := range status.Ports { + if port2.L2Type == L2LinkTypeVLAN && port2.VLAN.ParentPort == portLabel { + return true + } + } + return false +} + func rotate(arr []string, amount int) []string { if len(arr) == 0 { return []string{} diff --git a/pkg/pillar/types/dpc.go b/pkg/pillar/types/dpc.go index bf1dc3f1ea..82b18e085f 100644 --- a/pkg/pillar/types/dpc.go +++ b/pkg/pillar/types/dpc.go @@ -15,6 +15,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/lf-edge/eve/pkg/pillar/base" "github.com/lf-edge/eve/pkg/pillar/utils/generics" + "github.com/lf-edge/eve/pkg/pillar/utils/netutils" uuid "github.com/satori/go.uuid" ) @@ -306,6 +307,17 @@ func (config *DevicePortConfig) RecordPortFailure(ifname string, errStr string) } } +// IsPortUsedAsVlanParent - returns true if port with the given logical label +// is used as a VLAN parent interface. +func (config DevicePortConfig) IsPortUsedAsVlanParent(portLabel string) bool { + for _, port2 := range config.Ports { + if port2.L2Type == L2LinkTypeVLAN && port2.VLAN.ParentPort == portLabel { + return true + } + } + return false +} + // DPCSanitizeArgs : arguments for DevicePortConfig.DoSanitize(). type DPCSanitizeArgs struct { SanitizeTimePriority bool @@ -948,6 +960,13 @@ type BondArpMonitor struct { IPTargets []net.IP } +// Equal compares two BondArpMonitor configs for equality. +func (m BondArpMonitor) Equal(m2 BondArpMonitor) bool { + return m.Enabled == m2.Enabled && + m.Interval == m2.Interval && + generics.EqualSetsFn(m.IPTargets, m2.IPTargets, netutils.EqualIPs) +} + // DevicePortConfigList is an array in timestamp aka priority order; // first one is the most desired config to use // It includes test results hence is misnamed - should have a separate status