// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package components import ( "net/netip" "sort" "strings" "github.com/cosi-project/runtime/pkg/resource" "github.com/rivo/tview" "github.com/siderolabs/gen/maps" "github.com/siderolabs/gen/slices" "github.com/siderolabs/talos/internal/pkg/dashboard/resourcedata" "github.com/siderolabs/talos/pkg/machinery/resources/k8s" "github.com/siderolabs/talos/pkg/machinery/resources/network" ) var ( zeroPrefix = netip.Prefix{} routedNoK8sID = network.FilteredNodeAddressID(network.NodeAddressRoutedID, k8s.NodeAddressFilterNoK8s) ) type networkInfoData struct { hostname string gateway string outbound string resolvers string timeservers string addresses string nodeAddressRouted *network.NodeAddress nodeAddressRoutedNoK8s *network.NodeAddress routeStatusMap map[resource.ID]*network.RouteStatus } // NetworkInfo represents the network info widget. type NetworkInfo struct { tview.TextView selectedNode string nodeMap map[string]*networkInfoData } // NewNetworkInfo initializes NetworkInfo. func NewNetworkInfo() *NetworkInfo { component := &NetworkInfo{ TextView: *tview.NewTextView(), nodeMap: make(map[string]*networkInfoData), } component.SetDynamicColors(true). SetText(noData). SetBorderPadding(1, 0, 1, 0) return component } // OnNodeSelect implements the NodeSelectListener interface. func (widget *NetworkInfo) OnNodeSelect(node string) { if node != widget.selectedNode { widget.selectedNode = node widget.redraw() } } // OnResourceDataChange implements the ResourceDataListener interface. func (widget *NetworkInfo) OnResourceDataChange(data resourcedata.Data) { widget.updateNodeData(data) if data.Node == widget.selectedNode { widget.redraw() } } //nolint:gocyclo func (widget *NetworkInfo) updateNodeData(data resourcedata.Data) { nodeData := widget.getOrCreateNodeData(data.Node) switch res := data.Resource.(type) { case *network.ResolverStatus: if data.Deleted { nodeData.resolvers = notAvailable } else { nodeData.resolvers = widget.resolvers(res) } case *network.TimeServerStatus: if data.Deleted { nodeData.timeservers = notAvailable } else { nodeData.timeservers = widget.timeservers(res) } case *network.Status: if data.Deleted { nodeData.outbound = notAvailable } else { nodeData.outbound = widget.outbound(res) } case *network.HostnameStatus: if data.Deleted { nodeData.hostname = notAvailable } else { nodeData.hostname = res.TypedSpec().Hostname } case *network.RouteStatus: if data.Deleted { delete(nodeData.routeStatusMap, res.Metadata().ID()) } else { nodeData.routeStatusMap[res.Metadata().ID()] = res } nodeData.gateway = widget.gateway(maps.Values(nodeData.routeStatusMap)) case *network.NodeAddress: widget.setAddresses(data, res) } } func (widget *NetworkInfo) getOrCreateNodeData(node string) *networkInfoData { data, ok := widget.nodeMap[node] if !ok { data = &networkInfoData{ hostname: notAvailable, addresses: notAvailable, gateway: notAvailable, outbound: notAvailable, resolvers: notAvailable, timeservers: notAvailable, routeStatusMap: make(map[resource.ID]*network.RouteStatus), } widget.nodeMap[node] = data } return data } func (widget *NetworkInfo) redraw() { data := widget.getOrCreateNodeData(widget.selectedNode) fields := fieldGroup{ fields: []field{ { Name: "HOST", Value: data.hostname, }, { Name: "IP", Value: data.addresses, }, { Name: "GW", Value: data.gateway, }, { Name: "OUTBOUND", Value: data.outbound, }, { Name: "DNS", Value: data.resolvers, }, { Name: "NTP", Value: data.timeservers, }, }, } widget.SetText(fields.String()) } func (widget *NetworkInfo) setAddresses(data resourcedata.Data, nodeAddress *network.NodeAddress) { nodeData := widget.getOrCreateNodeData(data.Node) switch nodeAddress.Metadata().ID() { case network.NodeAddressRoutedID: if data.Deleted { nodeData.nodeAddressRouted = nil } else { nodeData.nodeAddressRouted = nodeAddress } case routedNoK8sID: if data.Deleted { nodeData.nodeAddressRoutedNoK8s = nil } else { nodeData.nodeAddressRoutedNoK8s = nodeAddress } } formatIPs := func(res *network.NodeAddress) string { if res == nil { return notAvailable } strs := slices.Map(res.TypedSpec().Addresses, func(prefix netip.Prefix) string { return prefix.String() }) sort.Strings(strs) return strings.Join(strs, ", ") } // if "routed-no-k8s" is available, use it if nodeData.nodeAddressRoutedNoK8s != nil { nodeData.addresses = formatIPs(nodeData.nodeAddressRoutedNoK8s) return } // fallback to "routed" nodeData.addresses = formatIPs(nodeData.nodeAddressRouted) } func (widget *NetworkInfo) gateway(statuses []*network.RouteStatus) string { var gatewaysV4, gatewaysV6 []string for _, status := range statuses { gateway := status.TypedSpec().Gateway if !gateway.IsValid() || status.TypedSpec().Destination != zeroPrefix { continue } if gateway.Is4() { gatewaysV4 = append(gatewaysV4, gateway.String()) } else { gatewaysV6 = append(gatewaysV6, gateway.String()) } } if len(gatewaysV4) == 0 && len(gatewaysV6) == 0 { return notAvailable } sort.Strings(gatewaysV4) sort.Strings(gatewaysV6) return strings.Join(append(gatewaysV4, gatewaysV6...), ", ") } func (widget *NetworkInfo) resolvers(status *network.ResolverStatus) string { strs := slices.Map(status.TypedSpec().DNSServers, func(t netip.Addr) string { return t.String() }) if len(strs) == 0 { return none } return strings.Join(strs, ", ") } func (widget *NetworkInfo) timeservers(status *network.TimeServerStatus) string { if len(status.TypedSpec().NTPServers) == 0 { return none } return strings.Join(status.TypedSpec().NTPServers, ", ") } func (widget *NetworkInfo) outbound(status *network.Status) string { if status.TypedSpec().ConnectivityReady { return "[green]OK[-]" } return "[red]FAILED[-]" }