Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/admin/v2/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func AddCmds(cmd *cobra.Command, c *config.Config) {
adminCmd.AddCommand(newTenantCmd(c))
adminCmd.AddCommand(newTokenCmd(c))
adminCmd.AddCommand(newProjectCmd(c))
adminCmd.AddCommand(newSwitchCmd(c))

cmd.AddCommand(adminCmd)
}
84 changes: 84 additions & 0 deletions cmd/admin/v2/switch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package v2

import (
adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2"
apiv2 "github.com/metal-stack/api/go/metalstack/api/v2"
"github.com/metal-stack/cli/cmd/config"
"github.com/metal-stack/metal-lib/pkg/genericcli"
"github.com/metal-stack/metal-lib/pkg/genericcli/printers"
"github.com/metal-stack/metal-lib/pkg/multisort"
"github.com/spf13/cobra"
)

type switchCmd struct {
c *config.Config
}

func newSwitchCmd(c *config.Config) *cobra.Command {
sw := &switchCmd{
c: c,
}

cmdsConfig := &genericcli.CmdsConfig[any, *adminv2.SwitchServiceUpdateRequest, *apiv2.Switch]{
GenericCLI: genericcli.NewGenericCLI(sw).WithFS(c.Fs),
OnlyCmds: genericcli.OnlyCmds(
genericcli.DescribeCmd,
genericcli.ListCmd,
genericcli.UpdateCmd,
genericcli.DeleteCmd,
genericcli.EditCmd,
),
BinaryName: config.BinaryName,
Singular: "switch",
Plural: "switches",
Description: "view and manage network switches",
Aliases: []string{"sw"},
DescribePrinter: func() printers.Printer { return c.DescribePrinter },
ListPrinter: func() printers.Printer { return c.ListPrinter },
Sorter: &multisort.Sorter[*apiv2.Switch]{},
ListCmdMutateFn: func(cmd *cobra.Command) {
cmd.Flags().String("id", "", "ID of the switch.")
cmd.Flags().String("name", "", "Name of the switch.")
cmd.Flags().String("os-vendor", "", "OS vendor of this switch.")
cmd.Flags().String("os-version", "", "OS version of this switch.")
cmd.Flags().String("partition", "", "Partition of this switch.")
cmd.Flags().String("rack", "", "Rack of this switch.")

genericcli.Must(cmd.RegisterFlagCompletionFunc("id", c.Completion.SwitchListCompletion))
genericcli.Must(cmd.RegisterFlagCompletionFunc("name", c.Completion.SwitchNameListCompletion))
genericcli.Must(cmd.RegisterFlagCompletionFunc("partition", c.Completion.PartitionListCompletion))
genericcli.Must(cmd.RegisterFlagCompletionFunc("rack", c.Completion.SwitchRackListCompletion))
genericcli.Must(cmd.RegisterFlagCompletionFunc("os-vendor", c.Completion.SwitchOSVendorListCompletion))
genericcli.Must(cmd.RegisterFlagCompletionFunc("os-version", c.Completion.SwitchOSVersionListCompletion))
},
DeleteCmdMutateFn: func(cmd *cobra.Command) {
cmd.Flags().Bool("force", false, "forcefully delete the switch accepting the risk that it still has machines connected to it")
},
}

return genericcli.NewCmds(cmdsConfig)
}

func (c *switchCmd) Get(id string) (*apiv2.Switch, error) {
panic("unimplemented")
}

func (c *switchCmd) List() ([]*apiv2.Switch, error) {
panic("unimplemented")
}

func (c *switchCmd) Create(rq any) (*apiv2.Switch, error) {
panic("unimplemented")
}

func (c *switchCmd) Delete(id string) (*apiv2.Switch, error) {
panic("unimplemented")
}

func (c *switchCmd) Convert(sw *apiv2.Switch) (string, any, *adminv2.SwitchServiceUpdateRequest, error) {
panic("unimplemented")
}

func (c *switchCmd) Update(rq *adminv2.SwitchServiceUpdateRequest) (*apiv2.Switch, error) {
panic("unimplemented")
}
29 changes: 29 additions & 0 deletions cmd/completion/switch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package completion

import (
"github.com/spf13/cobra"
)

func (c *Completion) SwitchListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
panic("unimplemented")
}

func (c *Completion) SwitchNameListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
panic("unimplemented")
}

func (c *Completion) PartitionListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
panic("unimplemented")
}

func (c *Completion) SwitchRackListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
panic("unimplemented")
}

func (c *Completion) SwitchOSVendorListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
panic("unimplemented")
}

func (c *Completion) SwitchOSVersionListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
panic("unimplemented")
}
5 changes: 5 additions & 0 deletions cmd/tableprinters/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ import (
"github.com/metal-stack/metal-lib/pkg/pointer"
)

const (
dot = "●"
nbr = " "
)

type TablePrinter struct {
t *printers.TablePrinter
}
Expand Down
156 changes: 156 additions & 0 deletions cmd/tableprinters/switch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package tableprinters

import (
"fmt"
"strings"
"time"

"github.com/fatih/color"
apiv2 "github.com/metal-stack/api/go/metalstack/api/v2"
"github.com/metal-stack/metal-lib/pkg/pointer"
)

func (t *TablePrinter) SwitchTable(switches []*apiv2.Switch, wide bool) ([]string, [][]string, error) {
var (
rows [][]string
)

header := []string{"ID", "Partition", "Rack", "OS", "Status", "Last Sync"}
if wide {
header = []string{"ID", "Partition", "Rack", "OS", "Metalcore", "IP", "Mode", "Last Sync", "Sync Duration", "Last Error"}

t.t.DisableAutoWrap(true)
}

for _, s := range switches {
var (
id = s.Id
partition = s.Partition
rack = pointer.SafeDeref(s.Rack)

syncTime time.Time
syncLast = ""
syncDurStr = ""
lastError = ""
shortStatus = nbr
allUp = true
)

for _, c := range s.MachineConnections {
if c.Nic == nil {
continue
}

if c.Nic.State == nil {
allUp = false
lastError = fmt.Sprintf("port status of %q is unknown", c.Nic.Name)
break
}

desired := c.Nic.State.Desired
actual := c.Nic.State.Actual
allUp = allUp && actual == apiv2.SwitchPortStatus_SWITCH_PORT_STATUS_UP

if desired != nil && actual != *desired {
lastError = fmt.Sprintf("%q is %s but should be %s", c.Nic.Name, c.Nic.State.Actual, desired)
break
}

if !allUp {
lastError = fmt.Sprintf("%q is %s", c.Nic.Name, c.Nic.State.Actual)
break
}
}

if s.LastSync != nil {
var (
syncAge time.Duration
syncDur time.Duration
)

if s.LastSync.Time != nil && !s.LastSync.Time.AsTime().IsZero() {
syncTime = s.LastSync.Time.AsTime()
syncAge = time.Since(syncTime)
}
if s.LastSync.Duration != nil {
syncDur = s.LastSync.Duration.AsDuration().Round(time.Millisecond)
}

switch {
case syncAge >= 10*time.Minute, syncDur >= 30*time.Second:
shortStatus = color.RedString(dot)
case syncAge >= time.Minute, syncDur >= 20*time.Second, !allUp:
shortStatus = color.YellowString(dot)
default:
shortStatus = color.GreenString(dot)
}

if syncAge > 0 {
syncLast = humanizeDuration(syncAge) + " ago"
}
if syncDur > 0 {
syncDurStr = fmt.Sprintf("%v", syncDur)
}
}

if s.LastSyncError != nil {
var (
errorTime time.Time
error string
)

if s.LastSyncError.Time != nil {
errorTime = s.LastSyncError.Time.AsTime()
}
if s.LastSyncError.Error != nil {
error = *s.LastSyncError.Error
}
// after 7 days we do not show sync errors anymore
if !errorTime.IsZero() && time.Since(errorTime) < 7*24*time.Hour {
lastError = fmt.Sprintf("%s ago: %s", humanizeDuration(time.Since(errorTime)), error)

if errorTime.After(syncTime) {
shortStatus = color.RedString(dot)
}
}
}

var mode string
switch s.ReplaceMode {
case apiv2.SwitchReplaceMode_SWITCH_REPLACE_MODE_REPLACE:
shortStatus = nbr + color.RedString(dot)
mode = "replace"
default:
mode = "operational"
}

os := ""
osIcon := ""
metalCore := ""
if s.Os != nil {
switch s.Os.Vendor {
case apiv2.SwitchOSVendor_SWITCH_OS_VENDOR_CUMULUS:
osIcon = "🐢"
case apiv2.SwitchOSVendor_SWITCH_OS_VENDOR_SONIC:
osIcon = "🦔"
default:
osIcon = s.Os.Vendor.String()
}

os = s.Os.Vendor.String()
if s.Os.Version != "" {
os = fmt.Sprintf("%s (%s)", os, s.Os.Version)
}
// metal core version is very long: v0.9.1 (1d5e42ea), tags/v0.9.1-0-g1d5e42e, go1.20.5
metalCore = strings.Split(s.Os.MetalCoreVersion, ",")[0]
}

if wide {
rows = append(rows, []string{id, partition, rack, os, metalCore, s.ManagementIp, mode, syncLast, syncDurStr, lastError})
} else {
rows = append(rows, []string{id, partition, rack, osIcon, shortStatus, syncLast})
}
}

return header, rows, nil
}
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/dustin/go-humanize v1.0.1
github.com/fatih/color v1.18.0
github.com/google/go-cmp v0.7.0
github.com/metal-stack/api v0.0.35
github.com/metal-stack/api v0.0.38-0.20260114100931-81fb1b7d4b93
github.com/metal-stack/metal-lib v0.23.5
github.com/metal-stack/v v1.0.3
github.com/spf13/afero v1.15.0
Expand All @@ -15,12 +15,12 @@ require (
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
golang.org/x/net v0.46.0
google.golang.org/protobuf v1.36.10
google.golang.org/protobuf v1.36.11
sigs.k8s.io/yaml v1.6.0
)

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.10-20250912141014-52f32327d4b0.1 // indirect
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect
connectrpc.com/connect v1.19.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
Expand All @@ -35,7 +35,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/connect-compress/v2 v2.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand All @@ -58,7 +58,7 @@ require (
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apimachinery v0.34.1 // indirect
Expand Down
22 changes: 10 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.10-20250912141014-52f32327d4b0.1 h1:31on4W/yPcV4nZHL4+UCiCvLPsMqe/vJcNg8Rci0scc=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.10-20250912141014-52f32327d4b0.1/go.mod h1:fUl8CEN/6ZAMk6bP8ahBJPUJw7rbp+j4x+wCcYi2IG4=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
Expand Down Expand Up @@ -39,8 +39,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/connect-compress/v2 v2.1.0 h1:8fM8QrVeHT69e5VVSh4yjDaQASYIvOp2uMZq7nVLj2U=
github.com/klauspost/connect-compress/v2 v2.1.0/go.mod h1:Ayurh2wscMMx3AwdGGVL+ylSR5316WfApREDgsqHyH8=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand All @@ -53,10 +53,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
github.com/metal-stack/api v0.0.35-0.20251124101516-a3076941d0f8 h1:PGBiFwASqhtmI6dGzVo0IEVQiBTIsQ2eo3pMriPViNY=
github.com/metal-stack/api v0.0.35-0.20251124101516-a3076941d0f8/go.mod h1:EBwS/oZr5tIcnV6hM7iK4aBQrw4wlU7vF5p+O1p3YIU=
github.com/metal-stack/api v0.0.35 h1:XxxYKTscSeYJg/ftL519nY3FAZ01atPeyD7+Zz/amQQ=
github.com/metal-stack/api v0.0.35/go.mod h1:EBwS/oZr5tIcnV6hM7iK4aBQrw4wlU7vF5p+O1p3YIU=
github.com/metal-stack/api v0.0.38-0.20260114100931-81fb1b7d4b93 h1:GfDGUyn3KA7LI/NyO6smxFs2xkW6bKQxce77cDylgJ4=
github.com/metal-stack/api v0.0.38-0.20260114100931-81fb1b7d4b93/go.mod h1:lVDIha/gViLpYuJi+OhQIQCeh6XYdzGxrtbtJTJ94eI=
github.com/metal-stack/metal-lib v0.23.5 h1:ozrkB3DNr3Cqn8nkBvmzc/KKpYqC1j1mv2OVOj8i7Ac=
github.com/metal-stack/metal-lib v0.23.5/go.mod h1:7uyHIrE19dkLwCZyeh2jmd7IEq5pEpzrzUGLoMN1eqY=
github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs=
Expand Down Expand Up @@ -113,10 +111,10 @@ golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
Expand Down