Utku Ozdemir aa662ff635
fix: apply small fixes on dashboard
* 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>
2023-03-30 09:39:14 +02:00

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[-]"
}