mirror of
https://github.com/siderolabs/talos.git
synced 2025-10-08 14:11:13 +02:00
* Clear the input form and switch to summary tab after the network config is saved. * Use nodeaddress resource for detecting and displaying IPs. Improve the IP filtering logic. * Fix the logic of gateway detection. Display all gateways instead of a single one. * Use hostnamestatus resource to detect the hostname instead of an API call. * Add hostname entry to the network info section on summary tab (as `HOST`). * Enable `OUTBOUND` entry in network info section on summary tab. * Display only the physical network interfaces in the interface dropdown on network config tab. * Improve form input handling. * Additional minor fixes & improvements. Closes siderolabs/talos#6992. Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
274 lines
6.2 KiB
Go
274 lines
6.2 KiB
Go
// 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[-]"
|
|
}
|