Skip to content

Commit

Permalink
Add switch connected-machines command. (#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
Gerrit91 authored Mar 7, 2023
1 parent 56d4eaa commit 0991651
Show file tree
Hide file tree
Showing 7 changed files with 429 additions and 1 deletion.
71 changes: 70 additions & 1 deletion cmd/switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os/exec"
"strings"

"github.com/metal-stack/metal-go/api/client/machine"
"github.com/metal-stack/metal-go/api/client/switch_operations"
"github.com/metal-stack/metal-go/api/models"
"github.com/metal-stack/metal-lib/pkg/genericcli"
Expand Down Expand Up @@ -81,6 +82,37 @@ func newSwitchCmd(c *config) *cobra.Command {
must(switchDetailCmd.RegisterFlagCompletionFunc("partition", c.comp.PartitionListCompletion))
must(switchDetailCmd.RegisterFlagCompletionFunc("rack", c.comp.SwitchRackListCompletion))

switchMachinesCmd := &cobra.Command{
Use: "connected-machines",
Short: "shows switches with their connected machines",
RunE: func(cmd *cobra.Command, args []string) error {
return w.switchMachines()
},
Example: `The command will show the machines connected to the switch ports.
Can also be used with -o template in order to generate CSV-style output:
$ metalctl switch connected-machines -o template --template '{{ $machines := .machines }}{{ range .switches }}{{ $switch := . }}{{ range .connections }}{{ $switch.id }},{{ $switch.rack_id }},{{ .nic.name }},{{ .machine_id }},{{ (index $machines .machine_id).ipmi.fru.product_serial }}{{ printf "\n" }}{{ end }}{{ end }}'
r01leaf01,swp1,f78cc340-e5e8-48ed-8fe7-2336c1e2ded2,<a-serial>
r01leaf01,swp2,44e3a522-5f48-4f3c-9188-41025f9e401e,<b-serial>
...
`,
}

switchMachinesCmd.Flags().String("id", "", "ID of the switch.")
switchMachinesCmd.Flags().String("name", "", "Name of the switch.")
switchMachinesCmd.Flags().String("os-vendor", "", "OS vendor of this switch.")
switchMachinesCmd.Flags().String("os-version", "", "OS version of this switch.")
switchMachinesCmd.Flags().String("partition", "", "Partition of this switch.")
switchMachinesCmd.Flags().String("rack", "", "Rack of this switch.")
switchMachinesCmd.Flags().String("size", "", "Size of the connectedmachines.")

must(switchMachinesCmd.RegisterFlagCompletionFunc("id", c.comp.SwitchListCompletion))
must(switchMachinesCmd.RegisterFlagCompletionFunc("name", c.comp.SwitchNameListCompletion))
must(switchMachinesCmd.RegisterFlagCompletionFunc("partition", c.comp.PartitionListCompletion))
must(switchMachinesCmd.RegisterFlagCompletionFunc("rack", c.comp.SwitchRackListCompletion))
must(switchMachinesCmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion))

switchReplaceCmd := &cobra.Command{
Use: "replace <switchID>",
Short: "put a leaf switch into replace mode in preparation for physical replacement. For a description of the steps involved see the long help.",
Expand Down Expand Up @@ -120,7 +152,7 @@ Operational steps to replace a switch:
ValidArgsFunction: c.comp.SwitchListCompletion,
}

return genericcli.NewCmds(cmdsConfig, switchDetailCmd, switchReplaceCmd, switchSSHCmd, switchConsoleCmd)
return genericcli.NewCmds(cmdsConfig, switchDetailCmd, switchMachinesCmd, switchReplaceCmd, switchSSHCmd, switchConsoleCmd)
}

func (c switchCmd) Get(id string) (*models.V1SwitchResponse, error) {
Expand Down Expand Up @@ -214,6 +246,43 @@ func (c *switchCmd) switchDetail() error {
return c.listPrinter.Print(result)
}

func (c *switchCmd) switchMachines() error {
switches, err := c.List()
if err != nil {
return err
}

err = sorters.SwitchSorter().SortBy(switches)
if err != nil {
return err
}

resp, err := c.client.Machine().FindIPMIMachines(machine.NewFindIPMIMachinesParams().WithBody(&models.V1MachineFindRequest{
PartitionID: viper.GetString("partition"),
Rackid: viper.GetString("rack"),
Sizeid: viper.GetString("size"),
}), nil)
if err != nil {
return err
}

machines := map[string]*models.V1MachineIPMIResponse{}
for _, m := range resp.Payload {
m := m

if m.ID == nil {
continue
}

machines[*m.ID] = m
}

return c.listPrinter.Print(&tableprinters.SwitchesWithMachines{
SS: switches,
MS: machines,
})
}

func (c *switchCmd) switchReplace(args []string) error {
id, err := genericcli.GetExactlyOneArg(args)
if err != nil {
Expand Down
89 changes: 89 additions & 0 deletions cmd/switch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"time"

"github.com/go-openapi/strfmt"
"github.com/metal-stack/metal-go/api/client/machine"
"github.com/metal-stack/metal-go/api/client/switch_operations"
"github.com/metal-stack/metal-go/api/models"
"github.com/metal-stack/metal-go/test/client"
"github.com/metal-stack/metal-lib/pkg/pointer"
"github.com/metal-stack/metal-lib/pkg/testcommon"
"github.com/metal-stack/metalctl/cmd/tableprinters"
"github.com/spf13/afero"

"github.com/stretchr/testify/mock"
Expand Down Expand Up @@ -208,6 +210,93 @@ ID PARTITION RACK OS IP MODE LAST SYNC SYNC DUR
}
}

func Test_SwitchCmd_ConnectedMachinesResult(t *testing.T) {
tests := []*test[*tableprinters.SwitchesWithMachines]{
{
name: "connected-machines",
cmd: func(want *tableprinters.SwitchesWithMachines) []string {
return []string{"switch", "connected-machines"}
},
mocks: &client.MetalMockFns{
Machine: func(mock *mock.Mock) {
mock.On("FindIPMIMachines", testcommon.MatchIgnoreContext(t, machine.NewFindIPMIMachinesParams().WithBody(&models.V1MachineFindRequest{})), nil).Return(&machine.FindIPMIMachinesOK{
Payload: []*models.V1MachineIPMIResponse{
{
ID: pointer.Pointer("machine-1"),
Rackid: "rack-1",
Partition: &models.V1PartitionResponse{
ID: pointer.Pointer("1"),
},
Size: &models.V1SizeResponse{
ID: pointer.Pointer("n1-medium-x86"),
},
Ipmi: &models.V1MachineIPMI{
Fru: &models.V1MachineFru{
ProductSerial: "123",
},
},
},
},
}, nil)
},
SwitchOperations: func(mock *mock.Mock) {
mock.On("FindSwitches", testcommon.MatchIgnoreContext(t, switch_operations.NewFindSwitchesParams().WithBody(&models.V1SwitchFindRequest{})), nil).Return(&switch_operations.FindSwitchesOK{
Payload: []*models.V1SwitchResponse{
switch2,
switch1,
},
}, nil)
},
},
want: &tableprinters.SwitchesWithMachines{
SS: []*models.V1SwitchResponse{
switch1,
switch2,
},
MS: map[string]*models.V1MachineIPMIResponse{
"machine-1": {
ID: pointer.Pointer("machine-1"),
Rackid: "rack-1",
Partition: &models.V1PartitionResponse{
ID: pointer.Pointer("1"),
},
Size: &models.V1SizeResponse{
ID: pointer.Pointer("n1-medium-x86"),
},
Ipmi: &models.V1MachineIPMI{
Fru: &models.V1MachineFru{
ProductSerial: "123",
},
},
},
},
},
wantTable: pointer.Pointer(`
ID NIC NAME IDENTIFIER PARTITION RACK SIZE PRODUCT SERIAL
1 1 rack-1
└─╴machine-1 a-name a-mac 1 rack-1 n1-medium-x86 123
2 1 rack-1
└─╴machine-1 a-name a-mac 1 rack-1 n1-medium-x86 123
`),
wantWideTable: pointer.Pointer(`
ID NIC NAME IDENTIFIER PARTITION RACK SIZE PRODUCT SERIAL
1 1 rack-1
└─╴machine-1 a-name a-mac 1 rack-1 n1-medium-x86 123
2 1 rack-1
└─╴machine-1 a-name a-mac 1 rack-1 n1-medium-x86 123
`),
template: pointer.Pointer(`{{ $machines := .machines }}{{ range .switches }}{{ $switch := . }}{{ range .connections }}{{ $switch.id }},{{ $switch.rack_id }},{{ .nic.name }},{{ .machine_id }},{{ (index $machines .machine_id).ipmi.fru.product_serial }}{{ printf "\n" }}{{ end }}{{ end }}`),
wantTemplate: pointer.Pointer(`
1,rack-1,a-name,machine-1,123
2,rack-1,a-name,machine-1,123
`),
},
}
for _, tt := range tests {
tt.testCmd(t)
}
}

func Test_SwitchCmd_SingleResult(t *testing.T) {
tests := []*test[*models.V1SwitchResponse]{
{
Expand Down
2 changes: 2 additions & 0 deletions cmd/tableprinters/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin
return t.SwitchTable(pointer.WrapInSlice(d), wide)
case []*SwitchDetail:
return t.SwitchDetailTable(d, wide)
case *SwitchesWithMachines:
return t.SwitchWithConnectedMachinesTable(d, wide)
case *models.V1NetworkResponse:
return t.NetworkTable(pointer.WrapInSlice(d), wide)
case []*models.V1NetworkResponse:
Expand Down
108 changes: 108 additions & 0 deletions cmd/tableprinters/switch.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package tableprinters

import (
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"time"

"github.com/fatih/color"
"github.com/metal-stack/metal-go/api/models"
"github.com/metal-stack/metal-lib/pkg/pointer"
"github.com/spf13/viper"
)

func (t *TablePrinter) SwitchTable(data []*models.V1SwitchResponse, wide bool) ([]string, [][]string, error) {
Expand Down Expand Up @@ -88,6 +92,110 @@ func (t *TablePrinter) SwitchTable(data []*models.V1SwitchResponse, wide bool) (
return header, rows, nil
}

type SwitchesWithMachines struct {
SS []*models.V1SwitchResponse `json:"switches" yaml:"switches"`
MS map[string]*models.V1MachineIPMIResponse `json:"machines" yaml:"machines"`
}

func (t *TablePrinter) SwitchWithConnectedMachinesTable(data *SwitchesWithMachines, wide bool) ([]string, [][]string, error) {
var (
rows [][]string
)

header := []string{"ID", "NIC Name", "Identifier", "Partition", "Rack", "Size", "Product Serial"}

for _, s := range data.SS {
id := pointer.SafeDeref(s.ID)
partition := pointer.SafeDeref(pointer.SafeDeref(s.Partition).ID)
rack := pointer.SafeDeref(s.RackID)

rows = append(rows, []string{id, "", "", partition, rack})

conns := s.Connections
if viper.IsSet("size") {
conns = []*models.V1SwitchConnection{}
for _, conn := range s.Connections {
conn := conn

m, ok := data.MS[conn.MachineID]
if !ok {
continue
}

if pointer.SafeDeref(m.Size.ID) == viper.GetString("size") {
conns = append(conns, conn)
}
}
}

sort.Slice(conns, switchInterfaceNameLessFunc(conns))

for i, conn := range conns {
prefix := "├"
if i == len(conns)-1 {
prefix = "└"
}
prefix += "─╴"

m, ok := data.MS[conn.MachineID]
if !ok {
return nil, nil, fmt.Errorf("switch port %s is connected to a machine which does not exist: %q", pointer.SafeDeref(pointer.SafeDeref(conn.Nic).Name), conn.MachineID)
}

identifier := pointer.SafeDeref(conn.Nic.Identifier)
if identifier == "" {
identifier = pointer.SafeDeref(conn.Nic.Mac)
}

rows = append(rows, []string{
fmt.Sprintf("%s%s", prefix, pointer.SafeDeref(m.ID)),
pointer.SafeDeref(pointer.SafeDeref(conn.Nic).Name),
identifier,
pointer.SafeDeref(pointer.SafeDeref(m.Partition).ID),
m.Rackid,
pointer.SafeDeref(pointer.SafeDeref(m.Size).ID),
pointer.SafeDeref(pointer.SafeDeref(m.Ipmi).Fru).ProductSerial,
})
}
}

return header, rows, nil
}

var numberRegex = regexp.MustCompile("([0-9]+)")

func switchInterfaceNameLessFunc(conns []*models.V1SwitchConnection) func(i, j int) bool {
return func(i, j int) bool {
var (
a = pointer.SafeDeref(pointer.SafeDeref(conns[i]).Nic.Name)
b = pointer.SafeDeref(pointer.SafeDeref(conns[j]).Nic.Name)

aMatch = numberRegex.FindAllStringSubmatch(a, -1)
bMatch = numberRegex.FindAllStringSubmatch(b, -1)
)

for i := range aMatch {
if i >= len(bMatch) {
return true
}

interfaceNumberA, aErr := strconv.Atoi(aMatch[i][0])
interfaceNumberB, bErr := strconv.Atoi(bMatch[i][0])

if aErr == nil && bErr == nil {
if interfaceNumberA < interfaceNumberB {
return true
}
if interfaceNumberA != interfaceNumberB {
return false
}
}
}

return a < b
}
}

type SwitchDetail struct {
*models.V1SwitchResponse
}
Expand Down
Loading

0 comments on commit 0991651

Please sign in to comment.