feat: add TUI for configuring network interfaces settings

Allows configuring:
- cidr.
- dhcp enable/disable.
- MTU.
- Ignore.
- Dhcp metric.

Signed-off-by: Artem Chernyshev <artem.0xD2@gmail.com>
This commit is contained in:
Artem Chernyshev 2020-12-02 18:05:10 +03:00 committed by Andrew Rynhard
parent 7811589db8
commit 63e0d02aa9
11 changed files with 1414 additions and 671 deletions

View File

@ -721,8 +721,29 @@ message EtcdMemberListResponse { repeated EtcdMemberList messages = 1; }
// rpc generateConfiguration
message RouteConfig {
string network = 1;
string gateway = 2;
uint32 metric = 3;
}
message DHCPOptionsConfig {
uint32 route_metric = 1;
}
message NetworkDeviceConfig {
string interface = 1;
string cidr = 2;
int32 mtu = 3;
bool dhcp = 4;
bool ignore = 5;
DHCPOptionsConfig dhcp_options = 6;
repeated RouteConfig routes = 7;
}
message NetworkConfig {
string hostname = 1;
repeated NetworkDeviceConfig interfaces = 2;
}
message InstallConfig {

View File

@ -35,12 +35,38 @@ func Generate(ctx context.Context, in *machine.GenerateConfigurationRequest) (re
options := []generate.GenOption{}
if in.MachineConfig.NetworkConfig != nil && in.MachineConfig.NetworkConfig.Hostname != "" {
options = append(options, generate.WithNetworkConfig(
&v1alpha1.NetworkConfig{
NetworkHostname: in.MachineConfig.NetworkConfig.Hostname,
},
))
if in.MachineConfig.NetworkConfig != nil {
networkConfig := &v1alpha1.NetworkConfig{
NetworkHostname: in.MachineConfig.NetworkConfig.Hostname,
}
networkInterfaces := in.MachineConfig.NetworkConfig.Interfaces
if len(networkInterfaces) > 0 {
networkConfig.NetworkInterfaces = make([]*v1alpha1.Device, len(networkInterfaces))
for i, device := range networkInterfaces {
routes := make([]*v1alpha1.Route, len(device.Routes))
iface := &v1alpha1.Device{
DeviceInterface: device.Interface,
DeviceMTU: int(device.Mtu),
DeviceCIDR: device.Cidr,
DeviceDHCP: device.Dhcp,
DeviceIgnore: device.Ignore,
DeviceRoutes: routes,
}
if device.DhcpOptions != nil {
iface.DeviceDHCPOptions = &v1alpha1.DHCPOptions{
DHCPRouteMetric: device.DhcpOptions.RouteMetric,
}
}
networkConfig.NetworkInterfaces[i] = iface
}
}
options = append(options, generate.WithNetworkConfig(networkConfig))
}
if in.MachineConfig.InstallConfig != nil {

View File

@ -5,30 +5,251 @@
package components
import (
"fmt"
"reflect"
"strings"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"gopkg.in/yaml.v3"
)
var hline string
func init() {
for i := 0; i < 2000; i++ {
hline += string(tcell.RuneHLine)
}
}
// Separator hline with a description below.
type Separator struct {
*tview.TextView
}
// GetHeight implements Multiline interface.
func (s *Separator) GetHeight() int {
return 2
}
// NewSeparator creates new form special form item which can be used as a sections Separator.
func NewSeparator(description string) *Item {
return NewItem("", "", func(item *Item) tview.Primitive {
s := &Separator{
tview.NewTextView(),
}
s.SetText(hline + "\n" + description)
s.SetWrap(false)
return s
})
}
// Item represents a single form item.
type Item struct {
Name string
description string
dest interface{}
options []interface{}
}
// TableHeaders represents table headers list for item options which are using table representation.
type TableHeaders []interface{}
// NewTableHeaders creates TableHeaders object.
func NewTableHeaders(headers ...interface{}) TableHeaders {
return TableHeaders(headers)
}
// NewItem creates new form item.
func NewItem(name, description string, dest interface{}, options ...interface{}) *Item {
return &Item{
Name: name,
dest: dest,
description: description,
options: options,
}
}
func (item *Item) assign(value string) error {
// rely on yaml parser to decode value into the right type
return yaml.Unmarshal([]byte(value), item.dest)
}
// createFormItems dynamically creates tview.FormItem list based on the wrapped type.
// nolint:gocyclo
func (item *Item) createFormItems() ([]tview.Primitive, error) {
res := []tview.Primitive{}
v := reflect.ValueOf(item.dest)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
var formItem tview.Primitive
label := fmt.Sprintf("[::b]%s[::-]:", item.Name)
addDescription := true
// nolint:exhaustive
switch v.Kind() {
case reflect.Func:
if f, ok := item.dest.(func(*Item) tview.Primitive); ok {
formItem = f(item)
}
case reflect.Bool:
// use checkbox for boolean fields
checkbox := tview.NewCheckbox()
checkbox.SetChangedFunc(func(checked bool) {
v.Set(reflect.ValueOf(checked))
})
checkbox.SetChecked(v.Bool())
checkbox.SetLabel(label)
formItem = checkbox
default:
if len(item.options) > 0 {
tableHeaders, ok := item.options[0].(TableHeaders)
if ok {
table := NewTable()
table.SetHeader(tableHeaders...)
addDescription = false
data := item.options[1:]
numColumns := len(tableHeaders)
if len(data)%numColumns != 0 {
return nil, fmt.Errorf("incorrect amount of data provided for the table")
}
selected := -1
for i := 0; i < len(data); i += numColumns {
table.AddRow(data[i : i+numColumns]...)
if v.Interface() == data[i] {
selected = i / numColumns
}
}
if selected != -1 {
table.SelectRow(selected)
}
formItem = table
table.SetRowSelectedFunc(func(row int) {
v.Set(reflect.ValueOf(table.GetValue(row, 0))) // always pick the first column
})
} else {
dropdown := tview.NewDropDown()
if len(item.options)%2 != 0 {
return nil, fmt.Errorf("wrong amount of arguments for options: should be even amount of key, value pairs")
}
for i := 0; i < len(item.options); i += 2 {
if optionName, ok := item.options[i].(string); ok {
selected := -1
func(index int) {
dropdown.AddOption(optionName, func() {
v.Set(reflect.ValueOf(item.options[index]))
})
if v.Interface() == item.options[index] {
selected = i / 2
}
}(i + 1)
if selected != -1 {
dropdown.SetCurrentOption(selected)
}
} else {
return nil, fmt.Errorf("expected string option name, got %s", item.options[i])
}
}
dropdown.SetLabel(label)
formItem = dropdown
}
} else {
input := tview.NewInputField()
formItem = input
input.SetLabel(label)
text, err := yaml.Marshal(item.dest)
if err != nil {
return nil, err
}
input.SetText(string(text))
input.SetChangedFunc(func(text string) {
if err := item.assign(text); err != nil {
// TODO: highlight red
return
}
})
}
}
res = append(res, formItem)
if item.description != "" && addDescription {
parts := strings.Split(item.description, "\n")
for _, part := range parts {
desc := NewFormLabel(part)
res = append(res, desc)
}
}
res = append(res, NewFormLabel(""))
return res, nil
}
// NewForm creates a new form.
func NewForm() *Form {
return &Form{
func NewForm(app *tview.Application) *Form {
f := &Form{
Flex: tview.NewFlex().SetDirection(tview.FlexRow),
formItems: []tview.FormItem{},
form: tview.NewFlex().SetDirection(tview.FlexRow),
buttons: tview.NewFlex(),
group: NewGroup(app),
}
f.Flex.AddItem(f.form, 0, 1, false)
f.Flex.AddItem(f.buttons, 0, 0, false)
f.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
// nolint:exhaustive
switch e.Key() {
case tcell.KeyTAB:
f.group.NextFocus()
case tcell.KeyBacktab:
f.group.PrevFocus()
}
return e
})
return f
}
// Form is a more flexible form component for tview lib.
type Form struct {
*tview.Flex
formItems []tview.FormItem
maxLabelLen int
form *tview.Flex
buttons *tview.Flex
formItems []tview.FormItem
maxLabelLen int
hasMenuButton bool
group *Group
}
// AddFormItem adds a new item to the form.
func (f *Form) AddFormItem(item tview.Primitive) {
if formItem, ok := item.(tview.FormItem); ok {
f.formItems = append(f.formItems, formItem)
labelLen := len(formItem.GetLabel()) + 1
labelLen := tview.TaggedStringWidth(formItem.GetLabel()) + 1
if labelLen > f.maxLabelLen {
for _, item := range f.formItems[:len(f.formItems)-1] {
@ -62,7 +283,57 @@ func (f *Form) AddFormItem(item tview.Primitive) {
height = multiline.GetHeight()
}
f.AddItem(item, height+1, 1, false)
f.form.AddItem(item, height, 1, false)
switch item.(type) {
case *FormLabel:
case *Separator:
default:
f.group.AddElement(item)
}
}
// Focus overrides default focus behavior.
func (f *Form) Focus(delegate func(tview.Primitive)) {
f.group.FocusFirst()
}
// AddMenuButton adds a button to the menu at the bottom of the form.
func (f *Form) AddMenuButton(label string, alignRight bool) *tview.Button {
b := tview.NewButton(label)
if f.hasMenuButton || alignRight {
f.buttons.AddItem(
tview.NewBox().SetBackgroundColor(f.GetBackgroundColor()),
0,
1,
false,
)
}
f.ResizeItem(f.buttons, 3, 0)
f.buttons.AddItem(b, tview.TaggedStringWidth(label)+4, 0, false)
f.hasMenuButton = true
f.group.AddElement(b)
return b
}
// AddFormItems constructs form from data represented as a list of Item objects.
func (f *Form) AddFormItems(items []*Item) error {
for _, item := range items {
formItems, e := item.createFormItems()
if e != nil {
return e
}
for _, formItem := range formItems {
f.AddFormItem(formItem)
}
}
return nil
}
// Multiline interface represents elements that can occupy more than one line.

View File

@ -0,0 +1,75 @@
// 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 (
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
// NewFormModalButton creates a new FormModalButton.
func NewFormModalButton(formLabel, buttonLabel string) *FormModalButton {
res := &FormModalButton{
Flex: tview.NewFlex(),
button: tview.NewButton(buttonLabel),
label: tview.NewTextView(),
}
res.label.SetText(formLabel)
res.AddItem(res.label, 0, 1, false)
res.AddItem(res.button, len(buttonLabel)+2, 1, false)
return res
}
// FormModalButton the button that opens modal dialog with extended settings.
type FormModalButton struct {
*tview.Flex
label *tview.TextView
button *tview.Button
}
// SetSelectedFunc forwards that to underlying button component.
func (b *FormModalButton) SetSelectedFunc(handler func()) *FormModalButton {
b.button.SetSelectedFunc(handler)
return b
}
// Focus override default focus behavior.
func (b *FormModalButton) Focus(delegate func(tview.Primitive)) {
b.button.Focus(delegate)
}
// Blur override default blur behavior.
func (b *FormModalButton) Blur() {
b.button.Blur()
}
// SetFormAttributes sets form attributes.
func (b *FormModalButton) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem {
b.label.SetTextColor(labelColor)
b.label.SetBackgroundColor(bgColor)
b.SetBackgroundColor(bgColor)
b.ResizeItem(b.label, labelWidth, 1)
return b
}
// GetFieldWidth implements tview.FormItem.
func (b *FormModalButton) GetFieldWidth() int {
return 0
}
// SetFinishedFunc implements tview.FormItem.
func (b *FormModalButton) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem {
return b
}
// GetLabel implements tview.FormItem.
func (b *FormModalButton) GetLabel() string {
return b.label.GetText(true)
}

View File

@ -5,6 +5,9 @@
package components
import (
"strings"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
)
@ -21,3 +24,32 @@ func NewFormLabel(label string) *FormLabel {
type FormLabel struct {
*tview.TextView
}
// SetFormAttributes sets form attributes.
func (b *FormLabel) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) tview.FormItem {
b.SetTextColor(labelColor)
b.SetBackgroundColor(bgColor)
s := strings.TrimSpace(b.GetText(false))
for i := 0; i < labelWidth; i++ {
s = " " + s
}
b.SetText(s)
return b
}
// GetFieldWidth implements tview.FormItem.
func (b *FormLabel) GetFieldWidth() int {
return 0
}
// SetFinishedFunc implements tview.FormItem.
func (b *FormLabel) SetFinishedFunc(handler func(key tcell.Key)) tview.FormItem {
return b
}
// GetLabel implements tview.FormItem.
func (b *FormLabel) GetLabel() string {
return ""
}

View File

@ -30,6 +30,17 @@ func (eg *Group) AddElement(element tview.Primitive) tview.Primitive {
return element
}
// FocusFirst sets focus to the first element.
func (eg *Group) FocusFirst() {
if len(eg.elements) == 0 {
return
}
eg.current = 0
eg.focus = eg.elements[eg.current]
eg.app.SetFocus(eg.focus)
}
// NextFocus switch focus to the next element.
func (eg *Group) NextFocus() {
eg.detectFocus()

View File

@ -10,6 +10,7 @@ import (
"google.golang.org/grpc"
"github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/api/network"
"github.com/talos-systems/talos/pkg/machinery/api/storage"
"github.com/talos-systems/talos/pkg/machinery/client"
)
@ -61,6 +62,11 @@ func (c *Connection) Disks(callOptions ...grpc.CallOption) (*storage.DisksRespon
return c.nodeClient.Disks(c.nodeCtx, callOptions...)
}
// Interfaces get list of network interfaces.
func (c *Connection) Interfaces(callOptions ...grpc.CallOption) (*network.InterfacesResponse, error) {
return c.nodeClient.Interfaces(c.nodeCtx, callOptions...)
}
// ExpandingCluster check if bootstrap node is set.
func (c *Connection) ExpandingCluster() bool {
return c.bootstrapClient != nil

View File

@ -9,13 +9,11 @@ import (
"context"
"fmt"
"os"
"reflect"
"strings"
"sync"
"github.com/gdamore/tcell/v2"
"github.com/rivo/tview"
"gopkg.in/yaml.v3"
"github.com/talos-systems/talos/internal/pkg/tui/components"
machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine"
@ -24,7 +22,7 @@ import (
)
// NewPage creates a new installer page.
func NewPage(name string, items ...*Item) *Page {
func NewPage(name string, items ...*components.Item) *Page {
return &Page{
name: name,
items: items,
@ -34,156 +32,7 @@ func NewPage(name string, items ...*Item) *Page {
// Page represents a single installer page.
type Page struct {
name string
items []*Item
}
// Item represents a single form item.
type Item struct {
name string
description string
dest interface{}
options []interface{}
}
// TableHeaders represents table headers list for item options which are using table representation.
type TableHeaders []interface{}
// NewTableHeaders creates TableHeaders object.
func NewTableHeaders(headers ...interface{}) TableHeaders {
return TableHeaders(headers)
}
// NewItem creates new form item.
func NewItem(name, description string, dest interface{}, options ...interface{}) *Item {
return &Item{
dest: dest,
name: name,
description: description,
options: options,
}
}
func (item *Item) assign(value string) error {
// rely on yaml parser to decode value into the right type
return yaml.Unmarshal([]byte(value), item.dest)
}
// createFormItems dynamically creates tview.FormItem list based on the wrapped type.
// nolint:gocyclo
func (item *Item) createFormItems() ([]tview.Primitive, error) {
res := []tview.Primitive{}
v := reflect.ValueOf(item.dest)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if item.description != "" {
parts := strings.Split(item.description, "\n")
for _, part := range parts {
res = append(res, components.NewFormLabel(part))
}
}
var formItem tview.Primitive
// nolint:exhaustive
switch v.Kind() {
case reflect.Bool:
// use checkbox for boolean fields
checkbox := tview.NewCheckbox()
checkbox.SetChangedFunc(func(checked bool) {
reflect.ValueOf(item.dest).Set(reflect.ValueOf(checked))
})
checkbox.SetChecked(v.Bool())
checkbox.SetLabel(item.name)
formItem = checkbox
default:
if len(item.options) > 0 {
tableHeaders, ok := item.options[0].(TableHeaders)
if ok {
table := components.NewTable()
table.SetHeader(tableHeaders...)
data := item.options[1:]
numColumns := len(tableHeaders)
if len(data)%numColumns != 0 {
return nil, fmt.Errorf("incorrect amount of data provided for the table")
}
selected := -1
for i := 0; i < len(data); i += numColumns {
table.AddRow(data[i : i+numColumns]...)
if v.Interface() == data[i] {
selected = i / numColumns
}
}
if selected != -1 {
table.SelectRow(selected)
}
formItem = table
table.SetRowSelectedFunc(func(row int) {
v.Set(reflect.ValueOf(table.GetValue(row, 0))) // always pick the first column
})
} else {
dropdown := tview.NewDropDown()
if len(item.options)%2 != 0 {
return nil, fmt.Errorf("wrong amount of arguments for options: should be even amount of key, value pairs")
}
for i := 0; i < len(item.options); i += 2 {
if optionName, ok := item.options[i].(string); ok {
selected := -1
func(index int) {
dropdown.AddOption(optionName, func() {
v.Set(reflect.ValueOf(item.options[index]))
})
if v.Interface() == item.options[index] {
selected = i / 2
}
}(i + 1)
if selected != -1 {
dropdown.SetCurrentOption(selected)
}
} else {
return nil, fmt.Errorf("expected string option name, got %s", item.options[i])
}
}
dropdown.SetLabel(item.name)
formItem = dropdown
}
} else {
input := tview.NewInputField()
formItem = input
input.SetLabel(item.name)
text, err := yaml.Marshal(item.dest)
if err != nil {
return nil, err
}
input.SetText(string(text))
input.SetChangedFunc(func(text string) {
if err := item.assign(text); err != nil {
// TODO: highlight red
return
}
})
}
}
res = append(res, formItem)
return res, nil
items []*components.Item
}
// Installer interactive installer text based UI.
@ -315,6 +164,7 @@ func (installer *Installer) init(conn *Connection) (err error) {
installer.state, err = NewState(
installer.ctx,
installer,
conn,
)
@ -330,11 +180,10 @@ func (installer *Installer) init(conn *Connection) (err error) {
// nolint:gocyclo
func (installer *Installer) configure() error {
var (
currentGroup *components.Group
err error
err error
forms []*components.Form
)
groups := []*components.Group{}
currentPage := 0
menuButtons := []*components.MenuButton{}
@ -351,21 +200,13 @@ func (installer *Installer) configure() error {
menuButtons[currentPage].SetActive(true)
installer.pages.SwitchToPage(state.pages[currentPage].name)
currentGroup = groups[currentPage]
installer.app.SetFocus(forms[currentPage])
}
capture := installer.app.GetInputCapture()
installer.app.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey {
if currentGroup == nil {
return e
}
//nolint:exhaustive
switch e.Key() {
case tcell.KeyTAB:
currentGroup.NextFocus()
case tcell.KeyBacktab:
currentGroup.PrevFocus()
case tcell.KeyCtrlN:
setPage(currentPage + 1)
case tcell.KeyCtrlB:
@ -376,6 +217,8 @@ func (installer *Installer) configure() error {
if e.Rune() >= '1' && e.Rune() < '9' {
if e.Modifiers()&(tcell.ModAlt|tcell.ModCtrl) != 0 {
setPage(int(e.Rune()) - 49)
return nil
}
}
@ -410,70 +253,43 @@ func (installer *Installer) configure() error {
}
}
forms = make([]*components.Form, len(state.pages))
for i, p := range state.pages {
eg := components.NewGroup(installer.app)
groups = append(groups, eg)
err = func(index int) error {
form := components.NewForm()
form := components.NewForm(installer.app)
form.SetBackgroundColor(color)
forms[i] = form
for _, item := range p.items {
formItems, e := item.createFormItems()
if e != nil {
return e
}
for _, formItem := range formItems {
if _, ok := formItem.(*components.FormLabel); !ok {
eg.AddElement(formItem)
}
form.AddFormItem(formItem)
}
if e := form.AddFormItems(p.items); e != nil {
return e
}
flex := tview.NewFlex().SetDirection(tview.FlexRow)
flex.AddItem(form, 0, 1, false)
content := tview.NewFlex()
if index > 0 {
back := tview.NewButton("[::u]B[::-]ack")
back := form.AddMenuButton("[::u]B[::-]ack", false)
back.SetSelectedFunc(func() {
setPage(index - 1)
})
content.AddItem(eg.AddElement(back), 10, 1, false)
}
addMenuItem(p.name, index)
content.AddItem(tview.NewBox().SetBackgroundColor(color), 0, 1, false)
flex.SetBackgroundColor(color)
form.SetBackgroundColor(color)
content.SetBackgroundColor(color)
if index < len(state.pages)-1 {
next := tview.NewButton("[::u]N[::-]ext")
next := form.AddMenuButton("[::u]N[::-]ext", index == 0)
next.SetSelectedFunc(func() {
setPage(index + 1)
})
content.AddItem(eg.AddElement(next), 10, 1, false)
} else {
install := tview.NewButton("Install")
install := form.AddMenuButton("Install", false)
install.SetBackgroundColor(tcell.ColorGreen)
install.SetTitleAlign(tview.AlignCenter)
install.SetSelectedFunc(func() {
close(done)
})
content.AddItem(eg.AddElement(install), 11, 1, false)
}
flex.AddItem(content, 1, 1, false)
installer.addPage(p.name, flex, index == 0, menu)
installer.addPage(p.name, form, index == 0, menu)
return nil
}(i)
@ -698,16 +514,22 @@ func (installer *Installer) addPage(name string, primitive tview.Primitive, swit
frame := tview.NewFrame(content).SetBorders(1, 1, 1, 1, 2, 2).
AddText(name, true, tview.AlignLeft, tcell.ColorWhite).
AddText("Talos Interactive Installer", true, tview.AlignCenter, tcell.ColorWhite).
AddText(version.Tag, true, tview.AlignRight, tcell.ColorIvory)
AddText(version.Tag, true, tview.AlignRight, tcell.ColorIvory).
AddText("<CTRL>+B/<CTRL>+N to switch tabs", false, tview.AlignLeft, tcell.ColorIvory).
AddText("<TAB> for navigation", false, tview.AlignLeft, tcell.ColorIvory).
AddText("[::b]Key Bindings[::-]", false, tview.AlignLeft, tcell.ColorIvory)
frame.SetBackgroundColor(frameBGColor)
installer.pages.AddPage(name,
frame, true, false)
installer.app.Draw()
}
if switchToPage {
if switchToPage {
installer.pages.AddAndSwitchToPage(name, frame, true)
} else {
installer.pages.AddPage(name,
frame, true, false)
}
} else if switchToPage {
installer.pages.SwitchToPage(name)
}
installer.app.ForceDraw()
}

View File

@ -8,13 +8,17 @@ package installer
import (
"context"
"fmt"
"net"
"time"
"github.com/dustin/go-humanize"
"github.com/rivo/tview"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/talos-systems/talos/internal/pkg/tui/components"
"github.com/talos-systems/talos/pkg/images"
machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/api/network"
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1"
"github.com/talos-systems/talos/pkg/machinery/config/types/v1alpha1/machine"
"github.com/talos-systems/talos/pkg/machinery/constants"
@ -31,7 +35,8 @@ var cniPresets = map[string]*machineapi.CNIConfig{
}
// NewState creates new installer state.
func NewState(ctx context.Context, conn *Connection) (*State, error) {
// nolint:gocyclo
func NewState(ctx context.Context, installer *Installer, conn *Connection) (*State, error) {
opts := &machineapi.GenerateConfigurationRequest{
ConfigVersion: "v1alpha1",
MachineConfig: &machineapi.MachineConfig{
@ -58,7 +63,7 @@ func NewState(ctx context.Context, conn *Connection) (*State, error) {
}
installDiskOptions := []interface{}{
NewTableHeaders("device name", "model name", "size"),
components.NewTableHeaders("DEVICE NAME", "MODEL NAME", "SIZE"),
}
disks, err := conn.Disks()
@ -78,13 +83,13 @@ func NewState(ctx context.Context, conn *Connection) (*State, error) {
if conn.ExpandingCluster() {
machineTypes = []interface{}{
"worker", machineapi.MachineConfig_MachineType(machine.TypeJoin),
"control plane", machineapi.MachineConfig_MachineType(machine.TypeControlPlane),
" worker ", machineapi.MachineConfig_MachineType(machine.TypeJoin),
" control plane ", machineapi.MachineConfig_MachineType(machine.TypeControlPlane),
}
opts.MachineConfig.Type = machineapi.MachineConfig_MachineType(machine.TypeControlPlane)
} else {
machineTypes = []interface{}{
"control plane", machineapi.MachineConfig_MachineType(machine.TypeInit),
" control plane ", machineapi.MachineConfig_MachineType(machine.TypeInit),
}
}
@ -94,64 +99,96 @@ func NewState(ctx context.Context, conn *Connection) (*State, error) {
opts: opts,
}
networkConfigItems := []*Item{
NewItem(
"hostname",
networkConfigItems := []*components.Item{
components.NewItem(
"Hostname",
v1alpha1.NetworkConfigDoc.Describe("hostname", true),
&opts.MachineConfig.NetworkConfig.Hostname,
),
NewItem(
"dns domain",
components.NewItem(
"DNS Domain",
v1alpha1.ClusterNetworkConfigDoc.Describe("dnsDomain", true),
&opts.ClusterConfig.ClusterNetwork.DnsDomain,
),
}
if !conn.ExpandingCluster() {
networkConfigItems = append(networkConfigItems, NewItem(
"type",
v1alpha1.ClusterNetworkConfigDoc.Describe("cni", true),
&state.cni,
NewTableHeaders("CNI", "description"),
constants.DefaultCNI, "CNI used by Talos by default",
"cilium", "Cillium 1.8 installed through quick-install.yaml",
interfaces, err := conn.Interfaces()
if err != nil {
return nil, err
}
addedInterfaces := false
opts.MachineConfig.NetworkConfig.Interfaces = []*machineapi.NetworkDeviceConfig{}
for _, iface := range interfaces.Messages[0].Interfaces {
status := ""
if (net.Flags(iface.Flags) & net.FlagUp) != 0 {
status = " (UP)"
}
if !addedInterfaces {
networkConfigItems = append(networkConfigItems, components.NewSeparator("Network Interfaces Configuration"))
addedInterfaces = true
}
networkConfigItems = append(networkConfigItems, components.NewItem(
fmt.Sprintf("%s, %s%s", iface.Name, iface.Hardwareaddr, status),
"",
configureAdapter(installer, opts, iface),
))
}
if !conn.ExpandingCluster() {
networkConfigItems = append(networkConfigItems,
components.NewSeparator(v1alpha1.ClusterNetworkConfigDoc.Describe("cni", true)),
components.NewItem(
"Type",
v1alpha1.ClusterNetworkConfigDoc.Describe("cni", true),
&state.cni,
components.NewTableHeaders("CNI", "description"),
constants.DefaultCNI, "CNI used by Talos by default",
"cilium", "Cillium 1.8 installed through quick-install.yaml",
))
}
state.pages = []*Page{
NewPage("Installer Params",
NewItem(
"image",
components.NewItem(
"Image",
v1alpha1.InstallConfigDoc.Describe("image", true),
&opts.MachineConfig.InstallConfig.InstallImage,
),
NewItem(
"install disk",
components.NewSeparator(
v1alpha1.InstallConfigDoc.Describe("disk", true),
),
components.NewItem(
"Install Disk",
"",
&opts.MachineConfig.InstallConfig.InstallDisk,
installDiskOptions...,
),
),
NewPage("Machine Config",
NewItem(
"machine type",
components.NewItem(
"Machine Type",
v1alpha1.MachineConfigDoc.Describe("type", true),
&opts.MachineConfig.Type,
machineTypes...,
),
NewItem(
"cluster name",
components.NewItem(
"Cluster Name",
v1alpha1.ClusterConfigDoc.Describe("clusterName", true),
&opts.ClusterConfig.Name,
),
NewItem(
"control plane endpoint",
components.NewItem(
"Control Plane Endpoint",
v1alpha1.ControlPlaneConfigDoc.Describe("endpoint", true),
&opts.ClusterConfig.ControlPlane.Endpoint,
),
NewItem(
"kubernetes version",
"Kubernetes version to install.",
components.NewItem(
"Kubernetes Version",
"",
&opts.MachineConfig.KubernetesVersion,
),
),
@ -182,3 +219,104 @@ func (s *State) GenConfig() (*machineapi.GenerateConfigurationResponse, error) {
return s.conn.GenerateConfiguration(s.opts)
}
func configureAdapter(installer *Installer, opts *machineapi.GenerateConfigurationRequest, adapter *network.Interface) func(item *components.Item) tview.Primitive {
return func(item *components.Item) tview.Primitive {
return components.NewFormModalButton(item.Name, "configure").
SetSelectedFunc(func() {
deviceIndex := -1
var adapterSettings *machineapi.NetworkDeviceConfig
for i, iface := range opts.MachineConfig.NetworkConfig.Interfaces {
if iface.Interface == adapter.Name {
deviceIndex = i
adapterSettings = iface
break
}
}
if adapterSettings == nil {
adapterSettings = &machineapi.NetworkDeviceConfig{
Interface: adapter.Name,
Dhcp: true,
Mtu: int32(adapter.Mtu),
Ignore: false,
DhcpOptions: &machineapi.DHCPOptionsConfig{},
}
if len(adapter.Ipaddress) > 0 {
adapterSettings.Cidr = adapter.Ipaddress[0]
}
}
items := []*components.Item{
components.NewItem(
"Use DHCP",
v1alpha1.DeviceDoc.Describe("dhcp", true),
&adapterSettings.Dhcp,
),
components.NewItem(
"Ignore",
v1alpha1.DeviceDoc.Describe("ignore", true),
&adapterSettings.Ignore,
),
components.NewItem(
"CIDR",
v1alpha1.DeviceDoc.Describe("cidr", true),
&adapterSettings.Cidr,
),
components.NewItem(
"MTU",
v1alpha1.DeviceDoc.Describe("mtu", true),
&adapterSettings.Mtu,
),
components.NewItem(
"Route Metric",
v1alpha1.DeviceDoc.Describe("dhcpOptions", true),
&adapterSettings.DhcpOptions.RouteMetric,
),
}
adapterConfiguration := components.NewForm(installer.app)
if err := adapterConfiguration.AddFormItems(items); err != nil {
panic(err)
}
focused := installer.app.GetFocus()
page, _ := installer.pages.GetFrontPage()
goBack := func() {
installer.pages.SwitchToPage(page)
installer.app.SetFocus(focused)
}
adapterConfiguration.AddMenuButton("Cancel", false).SetSelectedFunc(func() {
goBack()
})
adapterConfiguration.AddMenuButton("Apply", false).SetSelectedFunc(func() {
goBack()
if deviceIndex == -1 {
opts.MachineConfig.NetworkConfig.Interfaces = append(
opts.MachineConfig.NetworkConfig.Interfaces,
adapterSettings,
)
}
})
flex := tview.NewFlex().SetDirection(tview.FlexRow)
flex.AddItem(tview.NewBox().SetBackgroundColor(color), 1, 0, false)
flex.AddItem(adapterConfiguration, 0, 1, false)
installer.addPage(
fmt.Sprintf("Adapter %s Configuration", adapter.Name),
flex,
true,
nil,
)
installer.app.SetFocus(adapterConfiguration)
})
}
}

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,7 @@ title: API
- [ContainersResponse](#machine.ContainersResponse)
- [ControlPlaneConfig](#machine.ControlPlaneConfig)
- [CopyRequest](#machine.CopyRequest)
- [DHCPOptionsConfig](#machine.DHCPOptionsConfig)
- [DiskStat](#machine.DiskStat)
- [DiskStats](#machine.DiskStats)
- [DiskStatsResponse](#machine.DiskStatsResponse)
@ -72,6 +73,7 @@ title: API
- [MountsResponse](#machine.MountsResponse)
- [NetDev](#machine.NetDev)
- [NetworkConfig](#machine.NetworkConfig)
- [NetworkDeviceConfig](#machine.NetworkDeviceConfig)
- [NetworkDeviceStats](#machine.NetworkDeviceStats)
- [NetworkDeviceStatsResponse](#machine.NetworkDeviceStatsResponse)
- [PhaseEvent](#machine.PhaseEvent)
@ -95,6 +97,7 @@ title: API
- [Rollback](#machine.Rollback)
- [RollbackRequest](#machine.RollbackRequest)
- [RollbackResponse](#machine.RollbackResponse)
- [RouteConfig](#machine.RouteConfig)
- [SequenceEvent](#machine.SequenceEvent)
- [ServiceEvent](#machine.ServiceEvent)
- [ServiceEvents](#machine.ServiceEvents)
@ -651,6 +654,21 @@ Copy produces .tar.gz archive which is streamed back to the caller
<a name="machine.DHCPOptionsConfig"></a>
### DHCPOptionsConfig
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| route_metric | [uint32](#uint32) | | |
<a name="machine.DiskStat"></a>
### DiskStat
@ -1300,6 +1318,28 @@ The messages message containing the requested df stats.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| hostname | [string](#string) | | |
| interfaces | [NetworkDeviceConfig](#machine.NetworkDeviceConfig) | repeated | |
<a name="machine.NetworkDeviceConfig"></a>
### NetworkDeviceConfig
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| interface | [string](#string) | | |
| cidr | [string](#string) | | |
| mtu | [int32](#int32) | | |
| dhcp | [bool](#bool) | | |
| ignore | [bool](#bool) | | |
| dhcp_options | [DHCPOptionsConfig](#machine.DHCPOptionsConfig) | | |
| routes | [RouteConfig](#machine.RouteConfig) | repeated | |
@ -1660,6 +1700,23 @@ rpc rollback
<a name="machine.RouteConfig"></a>
### RouteConfig
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| network | [string](#string) | | |
| gateway | [string](#string) | | |
| metric | [uint32](#uint32) | | |
<a name="machine.SequenceEvent"></a>
### SequenceEvent