mirror of
https://github.com/siderolabs/talos.git
synced 2025-10-08 14:11:13 +02:00
Updated documentation, what's new, etc. Also fix some minor UI issues in the dashboard. Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
455 lines
13 KiB
Go
455 lines
13 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 dashboard
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/gdamore/tcell/v2"
|
|
"github.com/rivo/tview"
|
|
"github.com/siderolabs/gen/maps"
|
|
"github.com/siderolabs/go-pointer"
|
|
"gopkg.in/yaml.v3"
|
|
|
|
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime"
|
|
"github.com/siderolabs/talos/internal/pkg/dashboard/resourcedata"
|
|
"github.com/siderolabs/talos/internal/pkg/meta"
|
|
"github.com/siderolabs/talos/pkg/machinery/resources/network"
|
|
runtimeres "github.com/siderolabs/talos/pkg/machinery/resources/runtime"
|
|
)
|
|
|
|
const (
|
|
formItemHostname = "Hostname"
|
|
formItemDNSServers = "DNS Servers"
|
|
formItemTimeServers = "Time Servers"
|
|
formItemInterface = "Interface"
|
|
formItemMode = "Mode"
|
|
formItemAddresses = "Addresses"
|
|
formItemGateway = "Gateway"
|
|
)
|
|
|
|
type networkConfigData struct {
|
|
existingConfig *runtime.PlatformNetworkConfig
|
|
newConfig *runtime.PlatformNetworkConfig
|
|
newConfigError error
|
|
linkSet map[string]struct{}
|
|
}
|
|
|
|
// NetworkConfigGrid represents the network configuration widget.
|
|
type NetworkConfigGrid struct {
|
|
tview.Grid
|
|
|
|
dashboard *Dashboard
|
|
|
|
configForm *tview.Form
|
|
hostnameField *tview.InputField
|
|
dnsServersField *tview.InputField
|
|
timeServersField *tview.InputField
|
|
interfaceDropdown *tview.DropDown
|
|
modeDropdown *tview.DropDown
|
|
addressesField *tview.InputField
|
|
gatewayField *tview.InputField
|
|
|
|
infoView *tview.TextView
|
|
existingConfigView *tview.TextView
|
|
newConfigView *tview.TextView
|
|
|
|
selectedNode string
|
|
nodeMap map[string]*networkConfigData
|
|
}
|
|
|
|
// NewNetworkConfigGrid initializes NetworkConfigGrid.
|
|
//
|
|
//nolint:gocyclo
|
|
func NewNetworkConfigGrid(ctx context.Context, dashboard *Dashboard) *NetworkConfigGrid {
|
|
widget := &NetworkConfigGrid{
|
|
Grid: *tview.NewGrid(),
|
|
configForm: tview.NewForm(),
|
|
infoView: tview.NewTextView(),
|
|
existingConfigView: tview.NewTextView(),
|
|
newConfigView: tview.NewTextView(),
|
|
nodeMap: make(map[string]*networkConfigData),
|
|
dashboard: dashboard,
|
|
}
|
|
|
|
widget.configForm.SetBorder(true).SetTitle("Configure (Ctrl+Q)")
|
|
widget.SetRows(0, 3).SetColumns(0, 0, 0)
|
|
|
|
widget.infoView.
|
|
SetDynamicColors(true).
|
|
SetScrollable(true).
|
|
SetWrap(true)
|
|
widget.existingConfigView.
|
|
SetDynamicColors(true).
|
|
SetScrollable(true).
|
|
SetBorderPadding(0, 0, 1, 0).
|
|
SetBorder(true).
|
|
SetTitle("Existing Config (Ctrl+W)")
|
|
widget.newConfigView.
|
|
SetDynamicColors(true).
|
|
SetScrollable(true).
|
|
SetBorderPadding(0, 0, 1, 0).
|
|
SetBorder(true).
|
|
SetTitle("New Config (Ctrl+E)")
|
|
|
|
widget.AddItem(widget.configForm, 0, 0, 1, 1, 0, 0, false)
|
|
widget.AddItem(widget.infoView, 1, 0, 1, 1, 0, 0, false)
|
|
widget.AddItem(widget.existingConfigView, 0, 1, 2, 1, 0, 0, false)
|
|
widget.AddItem(widget.newConfigView, 0, 2, 2, 1, 0, 0, false)
|
|
|
|
widget.hostnameField = tview.NewInputField().SetLabel(formItemHostname)
|
|
widget.hostnameField.SetBlurFunc(widget.formEdited)
|
|
|
|
widget.dnsServersField = tview.NewInputField().SetLabel(formItemDNSServers)
|
|
widget.dnsServersField.SetBlurFunc(widget.formEdited)
|
|
|
|
widget.timeServersField = tview.NewInputField().SetLabel(formItemTimeServers)
|
|
widget.timeServersField.SetBlurFunc(widget.formEdited)
|
|
|
|
widget.interfaceDropdown = tview.NewDropDown().SetLabel(formItemInterface)
|
|
widget.interfaceDropdown.SetBlurFunc(widget.formEdited)
|
|
widget.interfaceDropdown.SetOptions([]string{interfaceNone}, func(_ string, _ int) {
|
|
widget.formEdited()
|
|
})
|
|
widget.interfaceDropdown.SetListStyles(
|
|
tcell.StyleDefault.Foreground(tview.Styles.PrimitiveBackgroundColor).Background(tview.Styles.MoreContrastBackgroundColor),
|
|
tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tview.Styles.PrimaryTextColor),
|
|
)
|
|
|
|
widget.modeDropdown = tview.NewDropDown().SetLabel(formItemMode)
|
|
widget.modeDropdown.SetBlurFunc(widget.formEdited)
|
|
widget.modeDropdown.SetOptions([]string{ModeDHCP, ModeStatic}, func(_ string, _ int) {
|
|
widget.formEdited()
|
|
})
|
|
widget.modeDropdown.SetListStyles(
|
|
tcell.StyleDefault.Foreground(tview.Styles.PrimitiveBackgroundColor).Background(tview.Styles.MoreContrastBackgroundColor),
|
|
tcell.StyleDefault.Foreground(tcell.ColorBlack).Background(tview.Styles.PrimaryTextColor),
|
|
)
|
|
|
|
widget.addressesField = tview.NewInputField().SetLabel(formItemAddresses)
|
|
widget.addressesField.SetBlurFunc(widget.formEdited)
|
|
|
|
widget.gatewayField = tview.NewInputField().SetLabel(formItemGateway)
|
|
widget.gatewayField.SetBlurFunc(widget.formEdited)
|
|
|
|
widget.configForm.AddFormItem(widget.hostnameField)
|
|
widget.configForm.AddFormItem(widget.dnsServersField)
|
|
widget.configForm.AddFormItem(widget.timeServersField)
|
|
widget.configForm.AddFormItem(widget.interfaceDropdown)
|
|
widget.configForm.AddFormItem(widget.modeDropdown)
|
|
widget.configForm.AddFormItem(widget.addressesField)
|
|
widget.configForm.AddFormItem(widget.gatewayField)
|
|
|
|
widget.configForm.AddButton("Save", func() {
|
|
widget.save(ctx)
|
|
})
|
|
|
|
saveButton := widget.configForm.GetButton(0)
|
|
saveButton.SetBlurFunc(widget.formEdited)
|
|
|
|
inputCapture := func(event *tcell.EventKey) *tcell.EventKey {
|
|
if widget.handleFocusSwitch(event) {
|
|
return nil
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
widget.configForm.SetInputCapture(inputCapture)
|
|
widget.existingConfigView.SetInputCapture(inputCapture)
|
|
widget.newConfigView.SetInputCapture(inputCapture)
|
|
|
|
widget.interfaceDropdown.SetCurrentOption(0)
|
|
widget.modeDropdown.SetCurrentOption(0)
|
|
|
|
return widget
|
|
}
|
|
|
|
// OnNodeSelect implements the NodeSelectListener interface.
|
|
func (widget *NetworkConfigGrid) OnNodeSelect(node string) {
|
|
if node != widget.selectedNode {
|
|
widget.selectedNode = node
|
|
|
|
widget.clearForm()
|
|
widget.formEdited()
|
|
|
|
widget.redraw()
|
|
}
|
|
}
|
|
|
|
// OnResourceDataChange implements the ResourceDataListener interface.
|
|
func (widget *NetworkConfigGrid) OnResourceDataChange(data resourcedata.Data) {
|
|
widget.updateNodeData(data)
|
|
|
|
if data.Node == widget.selectedNode {
|
|
widget.redraw()
|
|
}
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func (widget *NetworkConfigGrid) formEdited() {
|
|
widget.infoView.SetText("")
|
|
|
|
resetInputField := func(field *tview.InputField) {
|
|
// avoid triggering another form edit if there is nothing to change
|
|
if field.GetText() != "" {
|
|
field.SetText("")
|
|
}
|
|
}
|
|
|
|
resetDropdown := func(dropdown *tview.DropDown) {
|
|
// avoid triggering another form edit if there is nothing to change
|
|
if currentIndex, _ := dropdown.GetCurrentOption(); currentIndex != 0 {
|
|
dropdown.SetCurrentOption(0)
|
|
}
|
|
}
|
|
|
|
_, currentInterface := widget.interfaceDropdown.GetCurrentOption()
|
|
_, currentMode := widget.modeDropdown.GetCurrentOption()
|
|
|
|
ifaceSelected := currentInterface != "" && currentInterface != interfaceNone
|
|
if ifaceSelected {
|
|
if itemIndex := widget.configForm.GetFormItemIndex(formItemMode); itemIndex == -1 {
|
|
widget.configForm.AddFormItem(widget.modeDropdown)
|
|
}
|
|
|
|
switch currentMode {
|
|
case ModeDHCP:
|
|
resetInputField(widget.addressesField)
|
|
resetInputField(widget.gatewayField)
|
|
|
|
if itemIndex := widget.configForm.GetFormItemIndex(formItemAddresses); itemIndex != -1 {
|
|
widget.configForm.RemoveFormItem(itemIndex)
|
|
}
|
|
|
|
if itemIndex := widget.configForm.GetFormItemIndex(formItemGateway); itemIndex != -1 {
|
|
widget.configForm.RemoveFormItem(itemIndex)
|
|
}
|
|
case ModeStatic:
|
|
if itemIndex := widget.configForm.GetFormItemIndex(formItemAddresses); itemIndex == -1 {
|
|
widget.configForm.AddFormItem(widget.addressesField)
|
|
}
|
|
|
|
if itemIndex := widget.configForm.GetFormItemIndex(formItemGateway); itemIndex == -1 {
|
|
widget.configForm.AddFormItem(widget.gatewayField)
|
|
}
|
|
}
|
|
} else {
|
|
resetDropdown(widget.modeDropdown)
|
|
resetInputField(widget.addressesField)
|
|
resetInputField(widget.gatewayField)
|
|
|
|
if itemIndex := widget.configForm.GetFormItemIndex(formItemMode); itemIndex != -1 {
|
|
widget.configForm.RemoveFormItem(itemIndex)
|
|
}
|
|
|
|
if itemIndex := widget.configForm.GetFormItemIndex(formItemAddresses); itemIndex != -1 {
|
|
widget.configForm.RemoveFormItem(itemIndex)
|
|
}
|
|
|
|
if itemIndex := widget.configForm.GetFormItemIndex(formItemGateway); itemIndex != -1 {
|
|
widget.configForm.RemoveFormItem(itemIndex)
|
|
}
|
|
}
|
|
|
|
data := widget.getOrCreateNodeData(widget.selectedNode)
|
|
|
|
formData := NetworkConfigFormData{
|
|
Base: pointer.SafeDeref(data.existingConfig),
|
|
Hostname: widget.hostnameField.GetText(),
|
|
DNSServers: widget.dnsServersField.GetText(),
|
|
TimeServers: widget.timeServersField.GetText(),
|
|
Iface: currentInterface,
|
|
Mode: currentMode,
|
|
Addresses: widget.addressesField.GetText(),
|
|
Gateway: widget.gatewayField.GetText(),
|
|
}
|
|
|
|
config, err := formData.ToPlatformNetworkConfig()
|
|
if err != nil {
|
|
data.newConfig = nil
|
|
data.newConfigError = err
|
|
} else {
|
|
data.newConfig = config
|
|
data.newConfigError = nil
|
|
}
|
|
|
|
widget.redraw()
|
|
}
|
|
|
|
func (widget *NetworkConfigGrid) redraw() {
|
|
data := widget.getOrCreateNodeData(widget.selectedNode)
|
|
|
|
if data.existingConfig != nil {
|
|
var buf strings.Builder
|
|
|
|
encoder := yaml.NewEncoder(&buf)
|
|
encoder.SetIndent(2)
|
|
|
|
err := encoder.Encode(data.existingConfig)
|
|
if err != nil {
|
|
widget.existingConfigView.SetText(fmt.Sprintf("[red]error: %v[-]", err))
|
|
}
|
|
|
|
widget.existingConfigView.SetText(fmt.Sprintf("[lightblue]%s[-]", tview.Escape(buf.String())))
|
|
} else {
|
|
widget.existingConfigView.SetText("[gray]No Config[-]")
|
|
}
|
|
|
|
if data.newConfigError != nil {
|
|
widget.newConfigView.SetText(fmt.Sprintf("[red]error: %v[-]", data.newConfigError))
|
|
} else if data.newConfig != nil {
|
|
var buf strings.Builder
|
|
|
|
encoder := yaml.NewEncoder(&buf)
|
|
encoder.SetIndent(2)
|
|
|
|
err := encoder.Encode(data.newConfig)
|
|
if err != nil {
|
|
widget.newConfigView.SetText(fmt.Sprintf("[red]error: %v[-]", err))
|
|
}
|
|
|
|
widget.newConfigView.SetText(fmt.Sprintf("[green]%s[-]", tview.Escape(buf.String())))
|
|
}
|
|
}
|
|
|
|
func (widget *NetworkConfigGrid) clearForm() {
|
|
widget.hostnameField.SetText("")
|
|
widget.dnsServersField.SetText("")
|
|
widget.timeServersField.SetText("")
|
|
widget.interfaceDropdown.SetCurrentOption(0)
|
|
widget.modeDropdown.SetCurrentOption(0)
|
|
widget.addressesField.SetText("")
|
|
widget.gatewayField.SetText("")
|
|
widget.infoView.SetText("")
|
|
|
|
widget.configForm.SetFocus(0)
|
|
|
|
widget.formEdited()
|
|
}
|
|
|
|
func (widget *NetworkConfigGrid) updateNodeData(data resourcedata.Data) {
|
|
nodeData := widget.getOrCreateNodeData(data.Node)
|
|
|
|
//nolint:gocritic
|
|
switch res := data.Resource.(type) {
|
|
case *network.LinkStatus:
|
|
if data.Deleted {
|
|
delete(nodeData.linkSet, res.Metadata().ID())
|
|
} else {
|
|
if !res.TypedSpec().Physical() {
|
|
return
|
|
}
|
|
|
|
nodeData.linkSet[res.Metadata().ID()] = struct{}{}
|
|
}
|
|
|
|
links := maps.Keys(nodeData.linkSet)
|
|
|
|
sort.Strings(links)
|
|
|
|
allLinks := append([]string{interfaceNone}, links...)
|
|
|
|
widget.interfaceDropdown.SetOptions(allLinks, func(_ string, _ int) {
|
|
widget.formEdited()
|
|
})
|
|
case *runtimeres.MetaKey:
|
|
if res.Metadata().ID() == runtimeres.MetaKeyTagToID(meta.MetalNetworkPlatformConfig) {
|
|
if data.Deleted {
|
|
nodeData.existingConfig = nil
|
|
} else {
|
|
cfg := runtime.PlatformNetworkConfig{}
|
|
|
|
if err := yaml.Unmarshal([]byte(res.TypedSpec().Value), &cfg); err != nil {
|
|
widget.existingConfigView.SetText(fmt.Sprintf("[red]error: %v[-]", err))
|
|
|
|
return
|
|
}
|
|
|
|
nodeData.existingConfig = &cfg
|
|
|
|
widget.formEdited()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (widget *NetworkConfigGrid) getOrCreateNodeData(node string) *networkConfigData {
|
|
nodeData, ok := widget.nodeMap[node]
|
|
if !ok {
|
|
nodeData = &networkConfigData{
|
|
linkSet: make(map[string]struct{}),
|
|
}
|
|
|
|
widget.nodeMap[node] = nodeData
|
|
}
|
|
|
|
return nodeData
|
|
}
|
|
|
|
// OnScreenSelect implements the screenSelectListener interface.
|
|
func (widget *NetworkConfigGrid) onScreenSelect(active bool) {
|
|
if active {
|
|
widget.dashboard.app.SetFocus(widget.configForm)
|
|
}
|
|
}
|
|
|
|
func (widget *NetworkConfigGrid) save(ctx context.Context) {
|
|
nodeData := widget.getOrCreateNodeData(widget.selectedNode)
|
|
|
|
if nodeData.newConfig == nil {
|
|
widget.infoView.SetText("[red]Error: nothing to save[-]")
|
|
|
|
return
|
|
}
|
|
|
|
if nodeData.newConfigError != nil {
|
|
widget.infoView.SetText("[red]Error: cannot save, fix the errors and try again[-]")
|
|
|
|
return
|
|
}
|
|
|
|
configBytes, err := yaml.Marshal(nodeData.newConfig)
|
|
if err != nil {
|
|
widget.infoView.SetText(fmt.Sprintf("[red]Error: %v[-]", err))
|
|
|
|
return
|
|
}
|
|
|
|
ctx = nodeContext(ctx, widget.selectedNode)
|
|
|
|
if err = widget.dashboard.cli.MetaWrite(ctx, meta.MetalNetworkPlatformConfig, configBytes); err != nil {
|
|
widget.infoView.SetText(fmt.Sprintf("[red]Error: %v[-]", err))
|
|
|
|
return
|
|
}
|
|
|
|
widget.infoView.SetText("[green]Network config saved successfully[-]")
|
|
widget.clearForm()
|
|
widget.dashboard.selectScreen(ScreenSummary)
|
|
}
|
|
|
|
func (widget *NetworkConfigGrid) handleFocusSwitch(event *tcell.EventKey) bool {
|
|
switch event.Key() { //nolint:exhaustive
|
|
case tcell.KeyCtrlQ:
|
|
widget.dashboard.app.SetFocus(widget.configForm)
|
|
|
|
return true
|
|
case tcell.KeyCtrlW:
|
|
widget.dashboard.app.SetFocus(widget.existingConfigView)
|
|
|
|
return true
|
|
case tcell.KeyCtrlE:
|
|
widget.dashboard.app.SetFocus(widget.newConfigView)
|
|
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|