wwctl node args and completions

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Jonathon Anderson
2025-02-08 16:57:49 -07:00
parent 5494424c09
commit 08a8eb430c
16 changed files with 213 additions and 246 deletions

View File

@@ -7,6 +7,7 @@ import (
"github.com/warewulf/warewulf/internal/pkg/image"
"github.com/warewulf/warewulf/internal/pkg/kernel"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/overlay"
)
func NodeKernelVersion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -51,18 +52,37 @@ func ProfileKernelVersion(cmd *cobra.Command, args []string, toComplete string)
func Images(num int) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if num <= 0 || len(args) < num {
sources, _ := image.ListSources()
return sources, cobra.ShellCompDirectiveNoFileComp
} else {
return nil, cobra.ShellCompDirectiveNoFileComp
if sources, err := image.ListSources(); err != nil {
return sources, cobra.ShellCompDirectiveNoFileComp
}
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
func Profiles(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
func Nodes(num int) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if num <= 0 || len(args) < num {
if registry, err := node.New(); err == nil {
return registry.ListAllNodes(), cobra.ShellCompDirectiveNoFileComp
}
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
registry, _ := node.New()
return registry.ListAllProfiles(), cobra.ShellCompDirectiveNoFileComp
}
func Profiles(num int) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if num <= 0 || len(args) < num {
if registry, err := node.New(); err == nil {
return registry.ListAllProfiles(), cobra.ShellCompDirectiveNoFileComp
}
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
}
func Overlays(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list := overlay.FindOverlays()
return list, cobra.ShellCompDirectiveNoFileComp
}

View File

@@ -1,14 +1,10 @@
package add
import (
"log"
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/app/wwctl/completions"
"github.com/warewulf/warewulf/internal/app/wwctl/flags"
"github.com/warewulf/warewulf/internal/pkg/image"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/overlay"
)
// Holds the variables which are needed in CobraRunE
@@ -29,42 +25,26 @@ func GetCommand() *cobra.Command {
Aliases: []string{"new", "create"},
RunE: CobraRunE(&vars),
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: cobra.FixedCompletions(nil, cobra.ShellCompDirectiveNoFileComp),
}
vars.nodeConf.CreateFlags(baseCmd)
vars.nodeAdd.CreateAddFlags(baseCmd)
flags.AddContainer(baseCmd, &(vars.nodeConf.Profile.ImageName))
// register the command line completions
if err := baseCmd.RegisterFlagCompletionFunc("image", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, _ := image.ListSources()
return list, cobra.ShellCompDirectiveNoFileComp
}); err != nil {
log.Println(err)
if err := baseCmd.RegisterFlagCompletionFunc("image", completions.Images(0)); err != nil { // no limit
panic(err)
}
if err := baseCmd.RegisterFlagCompletionFunc("kernelversion", completions.NodeKernelVersion); err != nil {
log.Println(err)
panic(err)
}
if err := baseCmd.RegisterFlagCompletionFunc("runtime", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list := overlay.FindOverlays()
return list, cobra.ShellCompDirectiveNoFileComp
}); err != nil {
log.Println(err)
if err := baseCmd.RegisterFlagCompletionFunc("runtime", completions.Overlays); err != nil {
panic(err)
}
if err := baseCmd.RegisterFlagCompletionFunc("wwinit", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list := overlay.FindOverlays()
return list, cobra.ShellCompDirectiveNoFileComp
}); err != nil {
log.Println(err)
if err := baseCmd.RegisterFlagCompletionFunc("wwinit", completions.Overlays); err != nil {
panic(err)
}
if err := baseCmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var list []string
nodeDB, _ := node.New()
profiles, _ := nodeDB.FindAllProfiles()
for _, profile := range profiles {
list = append(list, profile.Id())
}
return list, cobra.ShellCompDirectiveNoFileComp
}); err != nil {
log.Println(err)
if err := baseCmd.RegisterFlagCompletionFunc("profile", completions.Profiles(0)); err != nil { // no limit
panic(err)
}
// GetRootCommand returns the root cobra.Command for the application.

View File

@@ -2,6 +2,7 @@ package console
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/app/wwctl/completions"
)
var (
@@ -12,6 +13,7 @@ var (
Long: "Start a new IPMI console for NODENAME.",
Args: cobra.MinimumNArgs(1),
RunE: CobraRunE,
ValidArgsFunction: completions.Nodes(0), // no limit
}
)

View File

@@ -2,7 +2,7 @@ package delete
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/app/wwctl/completions"
)
var (
@@ -14,15 +14,7 @@ var (
Args: cobra.MinimumNArgs(1),
RunE: CobraRunE,
Aliases: []string{"rm", "del", "remove"},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
nodeDB, _ := node.New()
nodes := nodeDB.ListAllNodes()
return nodes, cobra.ShellCompDirectiveNoFileComp
},
ValidArgsFunction: completions.Nodes(0), // no limit
}
SetYes bool
SetForce bool // no hash checking, so always using force

View File

@@ -7,6 +7,7 @@ import (
"strings"
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/hostlist"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/util"
"github.com/warewulf/warewulf/internal/pkg/warewulfd"
@@ -34,6 +35,8 @@ func CobraRunE(cmd *cobra.Command, args []string) error {
for nodeID := range registry.Nodes {
args = append(args, nodeID)
}
} else {
args = hostlist.Expand(args)
}
wwlog.Debug("node list: %v", args)

View File

@@ -2,7 +2,7 @@ package edit
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/app/wwctl/completions"
)
var (
@@ -12,20 +12,7 @@ var (
Short: "Edit node(s) with editor",
Long: "This command opens an editor for the given nodes.",
RunE: CobraRunE,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
nodeDB, _ := node.New()
nodes, _ := nodeDB.FindAllNodes()
var node_names []string
for _, node := range nodes {
node_names = append(node_names, node.Id())
}
return node_names, cobra.ShellCompDirectiveNoFileComp
},
ValidArgsFunction: completions.Nodes(0), // no limit
}
NoHeader bool
)

View File

@@ -8,18 +8,10 @@ import (
)
func CobraRunE(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
args = append(args, ".*")
}
filterList := wwapiv1.NodeList{
Output: args,
}
nodeListMsg := apinode.FilteredNodes(&filterList)
/*
nodeMap := make(map[string]*node.NodeConf)
// got proper yaml back
_ = yaml.Unmarshal([]byte(nodeListMsg.NodeConfMapYaml), nodeMap)
*/
wwlog.Info(nodeListMsg.NodeConfMapYaml)
return nil
}

View File

@@ -2,7 +2,7 @@ package export
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/app/wwctl/completions"
)
var (
@@ -12,19 +12,7 @@ var (
Short: "Export nodes as yaml to stdout",
Long: "This command exports the given nodes as yaml to stdout.",
RunE: CobraRunE,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
nodeDB, _ := node.New()
nodes, _ := nodeDB.FindAllNodes()
var node_names []string
for _, node := range nodes {
node_names = append(node_names, node.Id())
}
return node_names, cobra.ShellCompDirectiveNoFileComp
},
ValidArgsFunction: completions.Nodes(0), // no limit
}
NoHeader bool
)

View File

@@ -13,7 +13,7 @@ var (
Short: "Import node(s) from yaml file",
Long: "This command imports all the nodes defined in a file. It will overwrite nodes with same name.",
RunE: CobraRunE,
Args: cobra.MinimumNArgs(1),
Args: cobra.ExactArgs(1),
Aliases: []string{"import"},
}
ImportCVS bool

View File

@@ -13,9 +13,6 @@ import (
func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) {
return func(cmd *cobra.Command, args []string) (err error) {
if len(args) > 0 && strings.Contains(args[0], ",") {
args = strings.FieldsFunc(args[0], func(r rune) bool { return r == ',' })
}
req := wwapiv1.GetNodeList{
Nodes: args,
Type: wwapiv1.GetNodeList_Simple,

View File

@@ -2,7 +2,7 @@ package list
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/app/wwctl/completions"
)
type variables struct {
@@ -22,21 +22,9 @@ func GetCommand() *cobra.Command {
Short: "List nodes",
Long: "This command lists all configured nodes. Optionally, it will list only\n" +
"nodes matching a PATTERN.",
RunE: CobraRunE(&vars),
Aliases: []string{"ls"},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
nodeDB, _ := node.New()
nodes, _ := nodeDB.FindAllNodes()
var node_names []string
for _, node := range nodes {
node_names = append(node_names, node.Id())
}
return node_names, cobra.ShellCompDirectiveNoFileComp
},
RunE: CobraRunE(&vars),
Aliases: []string{"ls"},
ValidArgsFunction: completions.Nodes(0), // no limit
}
baseCmd.PersistentFlags().BoolVarP(&vars.showNet, "net", "n", false, "Show node network configurations")
baseCmd.PersistentFlags().BoolVarP(&vars.showIpmi, "ipmi", "i", false, "Show node IPMI configurations")

View File

@@ -2,7 +2,7 @@ package sensors
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/app/wwctl/completions"
)
type variables struct {
@@ -21,19 +21,7 @@ func GetCommand() *cobra.Command {
Long: "Show IPMI sensor information for nodes matching PATTERN.",
Args: cobra.MinimumNArgs(1),
RunE: CobraRunE(&vars),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
nodeDB, _ := node.New()
nodes, _ := nodeDB.FindAllNodes()
var node_names []string
for _, node := range nodes {
node_names = append(node_names, node.Id())
}
return node_names, cobra.ShellCompDirectiveNoFileComp
},
ValidArgsFunction: completions.Nodes(0), // no limit
}
powerCmd.PersistentFlags().BoolVarP(&vars.Full, "full", "F", false, "show detailed output.")
powerCmd.PersistentFlags().BoolVarP(&vars.Showcmd, "show", "s", false, "only show command which will be executed")

View File

@@ -1,14 +1,10 @@
package set
import (
"log"
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/app/wwctl/completions"
"github.com/warewulf/warewulf/internal/app/wwctl/flags"
"github.com/warewulf/warewulf/internal/pkg/image"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/overlay"
)
type variables struct {
@@ -31,15 +27,7 @@ func GetCommand() *cobra.Command {
Aliases: []string{"modify"},
Args: cobra.MinimumNArgs(1), // require pattern as a mandatory arg
RunE: CobraRunE(&vars),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}
nodeDB, _ := node.New()
nodes := nodeDB.ListAllNodes()
return nodes, cobra.ShellCompDirectiveNoFileComp
},
ValidArgsFunction: completions.Nodes(0), // no limit
}
vars.nodeConf.CreateFlags(baseCmd)
@@ -50,37 +38,20 @@ func GetCommand() *cobra.Command {
baseCmd.PersistentFlags().BoolVarP(&vars.setYes, "yes", "y", false, "Set 'yes' to all questions asked")
baseCmd.PersistentFlags().BoolVarP(&vars.setForce, "force", "f", false, "Force configuration (even on error)")
// register the command line completions
if err := baseCmd.RegisterFlagCompletionFunc("image", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list, _ := image.ListSources()
return list, cobra.ShellCompDirectiveNoFileComp
}); err != nil {
log.Println(err)
if err := baseCmd.RegisterFlagCompletionFunc("image", completions.Images(0)); err != nil { // no limit
panic(err)
}
if err := baseCmd.RegisterFlagCompletionFunc("kernelversion", completions.NodeKernelVersion); err != nil {
log.Println(err)
panic(err)
}
if err := baseCmd.RegisterFlagCompletionFunc("runtime", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list := overlay.FindOverlays()
return list, cobra.ShellCompDirectiveNoFileComp
}); err != nil {
log.Println(err)
if err := baseCmd.RegisterFlagCompletionFunc("runtime", completions.Overlays); err != nil {
panic(err)
}
if err := baseCmd.RegisterFlagCompletionFunc("wwinit", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
list := overlay.FindOverlays()
return list, cobra.ShellCompDirectiveNoFileComp
}); err != nil {
log.Println(err)
if err := baseCmd.RegisterFlagCompletionFunc("wwinit", completions.Overlays); err != nil {
panic(err)
}
if err := baseCmd.RegisterFlagCompletionFunc("profile", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var list []string
nodeDB, _ := node.New()
profiles, _ := nodeDB.FindAllProfiles()
for _, profile := range profiles {
list = append(list, profile.Id())
}
return list, cobra.ShellCompDirectiveNoFileComp
}); err != nil {
log.Println(err)
if err := baseCmd.RegisterFlagCompletionFunc("profile", completions.Profiles(0)); err != nil { // no limit
panic(err)
}
return baseCmd

View File

@@ -1,6 +1,9 @@
package nodestatus
import "github.com/spf13/cobra"
import (
"github.com/spf13/cobra"
"github.com/warewulf/warewulf/internal/app/wwctl/completions"
)
var (
baseCmd = &cobra.Command{
@@ -9,7 +12,7 @@ var (
Short: "View the provisioning status of nodes",
Long: "View and monitor the status of nodes as they are provisioned and check in.",
RunE: CobraRunE,
Args: cobra.MinimumNArgs(0),
ValidArgsFunction: completions.Nodes(0), // no limit
}
SetWatch bool
SetUpdate int

View File

@@ -4,77 +4,113 @@ import (
"fmt"
"strconv"
"strings"
"unicode"
)
func toInt(i string) int {
ret, _ := strconv.Atoi(i)
return ret
}
func isDigit(s string) bool {
r := []rune(s)
for i := 0; i < len(r); i++ {
if !unicode.IsDigit(r[i]) {
return false
}
}
return true
}
func expand_iterate(list []string) ([]string, int) {
var ret []string
var count int
for _, i := range list {
bracketIndex1 := strings.Index(i, "[")
bracketIndex2 := strings.Index(i, "]")
if bracketIndex1 >= 0 && bracketIndex1 < bracketIndex2 {
prefix := i[:bracketIndex1]
suffix := i[bracketIndex2+1:]
ranges := strings.Split(i[bracketIndex1+1:bracketIndex2], ",")
count++
for _, r := range ranges {
iterate := strings.Split(r, "-")
if len(iterate) == 1 {
if !isDigit(iterate[0]) {
return ret, 0
} else {
ret = append(ret, prefix+iterate[0]+suffix)
}
} else if len(iterate) == 2 {
if !isDigit(iterate[0]) || !isDigit(iterate[1]) {
return ret, 0
} else {
sigfigures := len(iterate[0])
for i := toInt(iterate[0]); i <= toInt(iterate[1]); i++ {
ret = append(ret, fmt.Sprintf(`%s%.`+fmt.Sprintf("%d", sigfigures)+`d%s`, prefix, i, suffix))
}
}
}
}
}
}
return ret, count
}
// Expand takes a slice of host strings, possibly containing comma-separated
// values and bracketed ranges (e.g. "node[01-03]") and returns a fully expanded
// slice of host names.
func Expand(list []string) []string {
ret := list
// First, split each input string on commas that occur outside brackets.
var preList []string
for _, s := range list {
parts := splitTopLevel(s)
preList = append(preList, parts...)
}
expanded := preList
for {
loop, count := expand_iterate(ret)
onceExpanded, count := expandOnce(expanded)
if count == 0 {
break
}
ret = loop
expanded = onceExpanded
}
return ret
return expanded
}
// expandOnce performs a single round of bracket expansion.
func expandOnce(hosts []string) ([]string, int) {
var result []string
var expansionCount int
for _, host := range hosts {
bracketStart := strings.Index(host, "[")
bracketEnd := strings.Index(host, "]")
if bracketStart >= 0 && bracketStart < bracketEnd {
prefix := host[:bracketStart]
suffix := host[bracketEnd+1:]
// Extract the content between brackets and split on commas.
ranges := strings.Split(host[bracketStart+1:bracketEnd], ",")
expansionCount++
for _, rng := range ranges {
parts := strings.Split(rng, "-")
if len(parts) == 1 {
if !isDigit(parts[0]) {
// Abort expansion on invalid input.
return result, 0
}
result = append(result, prefix+parts[0]+suffix)
} else if len(parts) == 2 {
if !isDigit(parts[0]) || !isDigit(parts[1]) {
return result, 0
}
sigFigures := len(parts[0])
startNum := toInt(parts[0])
endNum := toInt(parts[1])
for num := startNum; num <= endNum; num++ {
result = append(result, fmt.Sprintf("%s%0*d%s", prefix, sigFigures, num, suffix))
}
}
}
} else {
// No brackets; keep the string as is.
result = append(result, host)
}
}
return result, expansionCount
}
// splitTopLevel splits s on commas that are not inside square brackets.
func splitTopLevel(s string) []string {
var parts []string
depth := 0
start := 0
for i, ch := range s {
switch ch {
case '[':
depth++
case ']':
if depth > 0 {
depth--
}
case ',':
if depth == 0 {
parts = append(parts, s[start:i])
start = i + 1
}
}
}
parts = append(parts, s[start:])
return parts
}
// toInt converts a numeric string to an integer. Assumes valid input.
func toInt(s string) int {
num, _ := strconv.Atoi(s)
return num
}
// isDigit returns true if s consists solely of ASCII digits.
func isDigit(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return true
}

View File

@@ -6,28 +6,48 @@ import (
"github.com/stretchr/testify/assert"
)
func Test_Single(t *testing.T) {
assert.Equal(t, []string{"node1"}, Expand([]string{"node1"}))
}
func TestHostList(t *testing.T) {
tests := map[string]struct {
input []string
output []string
}{
"single": {
input: []string{"node1"},
output: []string{"node1"},
},
"multiple": {
input: []string{"node1", "node2"},
output: []string{"node1", "node2"},
},
"range": {
input: []string{"node[1-2]"},
output: []string{"node1", "node2"},
},
"internal comma": {
input: []string{"node[1,2]"},
output: []string{"node1", "node2"},
},
"mixed range comma": {
input: []string{"node[1,2-3]"},
output: []string{"node1", "node2", "node3"},
},
"external comma": {
input: []string{"node1,node2"},
output: []string{"node1", "node2"},
},
"mixed external comma with range": {
input: []string{"n[1-3],n5,n[7-8,10]"},
output: []string{"n1", "n2", "n3", "n5", "n7", "n8", "n10"},
},
"leading zeroes": {
input: []string{"n[01-03]"},
output: []string{"n01", "n02", "n03"},
},
}
func Test_Multiple(t *testing.T) {
assert.Equal(t, []string{"node1", "node2"}, Expand([]string{"node1", "node2"}))
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
assert.Equal(t, tt.output, Expand(tt.input))
})
}
}
func Test_Range(t *testing.T) {
assert.Equal(t, []string{"node1", "node2"}, Expand([]string{"node[1-2]"}))
}
func Test_Internal_Comma(t *testing.T) {
assert.Equal(t, []string{"node1", "node2"}, Expand([]string{"node[1,2]"}))
}
func Test_Mixed_Range_Comma(t *testing.T) {
assert.Equal(t, []string{"node1", "node2", "node3"}, Expand([]string{"node[1,2-3]"}))
}
// not currently supported
//
// func Test_External_Comma(t *testing.T) {
// assert.Equal(t, []string{"node1", "node2"}, Expand([]string{"node1,node2"}))
// }