diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fbc2315..a5ff074c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,6 +167,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - `wwctl container list` only lists names by default. (`--long` shows all attributes.) #1117 +- use templating mechanism for power commands ## v4.5.5, 2024-07-05 diff --git a/Makefile b/Makefile index 0f970b57..b567eb38 100644 --- a/Makefile +++ b/Makefile @@ -116,10 +116,12 @@ install: build docs install -d -m 0755 $(DESTDIR)$(IPXESOURCE) install -d -m 0755 $(DESTDIR)$(DATADIR)/warewulf # wwctl genconfig to get the compiled in paths to warewulf.conf + install -d -m 0755 $(DESTDIR)$(WWDATADIR)/bmc test -f $(DESTDIR)$(WWCONFIGDIR)/warewulf.conf || ./wwctl --warewulfconf etc/warewulf.conf genconfig warewulfconf print> $(DESTDIR)$(WWCONFIGDIR)/warewulf.conf test -f $(DESTDIR)$(WWCONFIGDIR)/nodes.conf || install -m 0644 etc/nodes.conf $(DESTDIR)$(WWCONFIGDIR) for f in etc/examples/*.ww; do install -m 0644 $$f $(DESTDIR)$(WWCONFIGDIR)/examples/; done for f in etc/ipxe/*.ipxe; do install -m 0644 $$f $(DESTDIR)$(WWCONFIGDIR)/ipxe/; done + for f in lib/warewulf/bmc/*.tmpl; do install -m 0644 $$f $(DESTDIR)$(WWDATADIR)/bmc; done install -m 0644 etc/grub/grub.cfg.ww $(DESTDIR)$(WWCONFIGDIR)/grub/grub.cfg.ww install -m 0644 etc/grub/chainload.ww $(DESTDIR)$(WWOVERLAYDIR)/host/rootfs$(TFTPDIR)/warewulf/grub.cfg.ww install -m 0644 etc/logrotate.d/warewulfd.conf $(DESTDIR)$(LOGROTATEDIR)/warewulfd.conf diff --git a/etc/nodes.conf b/etc/nodes.conf index 22ab4335..62d5b496 100644 --- a/etc/nodes.conf +++ b/etc/nodes.conf @@ -26,4 +26,6 @@ nodeprofiles: init: /sbin/init root: initramfs ipxe template: default + ipmi: + template: ipmitool.tmpl nodes: {} diff --git a/internal/app/wwctl/node/console/main.go b/internal/app/wwctl/node/console/main.go index bbc2d319..0f150e31 100644 --- a/internal/app/wwctl/node/console/main.go +++ b/internal/app/wwctl/node/console/main.go @@ -44,20 +44,8 @@ func CobraRunE(cmd *cobra.Command, args []string) error { wwlog.Error("%s: No IPMI IP address", node.Id()) continue } - - ipmiCmd := power.IPMI{ - NodeName: node.Id(), - HostName: node.Ipmi.Ipaddr.String(), - Port: node.Ipmi.Port, - User: node.Ipmi.UserName, - Password: node.Ipmi.Password, - AuthType: "MD5", - Interface: node.Ipmi.Interface, - EscapeChar: node.Ipmi.EscapeChar, - } - + ipmiCmd := power.IPMI{IpmiConf: *node.Ipmi} err := ipmiCmd.Console() - if err != nil { wwlog.Error("%s: Console problem", node.Id()) returnErr = err diff --git a/internal/app/wwctl/node/sensors/main.go b/internal/app/wwctl/node/sensors/main.go index 00e382d4..c565f0aa 100644 --- a/internal/app/wwctl/node/sensors/main.go +++ b/internal/app/wwctl/node/sensors/main.go @@ -3,6 +3,7 @@ package sensors import ( "fmt" "os" + "strings" "github.com/spf13/cobra" "github.com/warewulf/warewulf/internal/pkg/batch" @@ -12,91 +13,81 @@ import ( "github.com/warewulf/warewulf/internal/pkg/wwlog" ) -func CobraRunE(cmd *cobra.Command, args []string) error { - var returnErr error = nil - - nodeDB, err := node.New() - if err != nil { - return fmt.Errorf("could not open node configuration: %s", err) - } - - nodes, err := nodeDB.FindAllNodes() - if err != nil { - return fmt.Errorf("could not get node list: %s", err) - } - - args = hostlist.Expand(args) - - if len(args) > 0 { - nodes = node.FilterNodeListByName(nodes, args) - } else { - //nolint:errcheck - cmd.Usage() - os.Exit(1) - } - - if len(nodes) == 0 { - wwlog.Info("No nodes found") - os.Exit(1) - } - - batchpool := batch.New(50) - jobcount := len(nodes) - results := make(chan power.IPMI, jobcount) - - for _, node := range nodes { - if node.Ipmi.Ipaddr.IsUnspecified() { - wwlog.Error("%s: No IPMI IP address", node.Id()) - continue - } - var ipmiInterface = "lan" - if node.Ipmi.Interface != "" { - ipmiInterface = node.Ipmi.Interface - } - var ipmiPort = "623" - if node.Ipmi.Port != "" { - ipmiPort = node.Ipmi.Port - } - ipmiCmd := power.IPMI{ - NodeName: node.Id(), - HostName: node.Ipmi.Ipaddr.String(), - Port: ipmiPort, - User: node.Ipmi.UserName, - Password: node.Ipmi.Password, - Interface: ipmiInterface, - AuthType: "MD5", - } - - fullFlag := full - - batchpool.Submit(func() { - if fullFlag { - //nolint:errcheck - ipmiCmd.SensorList() - } else { - //nolint:errcheck - ipmiCmd.SDRList() - } - results <- ipmiCmd - }) - - } - - batchpool.Run() - - close(results) - - for result := range results { - out, err := result.Result() +func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { + return func(cmd *cobra.Command, args []string) error { + var returnErr error = nil + nodeDB, err := node.New() if err != nil { - wwlog.Error("%s: %s", result.NodeName, out) - returnErr = err - continue + return fmt.Errorf("could not open node configuration: %s", err) } - fmt.Printf("%s:\n%s\n", result.NodeName, out) - } + nodes, err := nodeDB.FindAllNodes() + if err != nil { + return fmt.Errorf("could not get node list: %s", err) + } - return returnErr + args = hostlist.Expand(args) + + if len(args) > 0 { + nodes = node.FilterNodeListByName(nodes, args) + } else { + //nolint:errcheck + cmd.Usage() + os.Exit(1) + } + + if len(nodes) == 0 { + wwlog.Info("No nodes found") + os.Exit(1) + } + + batchpool := batch.New(50) + jobcount := len(nodes) + results := make(chan power.IPMI, jobcount) + + for _, node := range nodes { + if node.Ipmi.Ipaddr.IsUnspecified() { + wwlog.Error("%s: No IPMI IP address", node.Id()) + continue + } + ipmiCmd := power.IPMI{ + IpmiConf: *node.Ipmi, + ShowOnly: vars.Showcmd, + } + batchpool.Submit(func() { + if vars.Full { + //nolint:errcheck + ipmiCmd.SensorList() + } else { + //nolint:errcheck + ipmiCmd.SDRList() + } + results <- ipmiCmd + }) + + } + + batchpool.Run() + + close(results) + + for result := range results { + out, err := result.Result() + + if err != nil { + for _, line := range strings.Split(out, "\n") { + wwlog.Error("%s: %s", result.Ipaddr, line) + } + returnErr = err + continue + } + + for _, line := range strings.Split(out, "\n") { + wwlog.Info("%s: %s", result.Ipaddr, line) + } + } + + return returnErr + } } diff --git a/internal/app/wwctl/node/sensors/root.go b/internal/app/wwctl/node/sensors/root.go index e67f48eb..f367caeb 100644 --- a/internal/app/wwctl/node/sensors/root.go +++ b/internal/app/wwctl/node/sensors/root.go @@ -5,14 +5,22 @@ import ( "github.com/warewulf/warewulf/internal/pkg/node" ) -var ( - powerCmd = &cobra.Command{ +type variables struct { + Showcmd bool + Full bool + Fanout int +} + +func GetCommand() *cobra.Command { + vars := variables{} + + powerCmd := &cobra.Command{ DisableFlagsInUseLine: true, Use: "sensors [OPTIONS] PATTERN", Short: "Show node IPMI sensor information", Long: "Show IPMI sensor information for nodes matching PATTERN.", Args: cobra.MinimumNArgs(1), - RunE: CobraRunE, + RunE: CobraRunE(&vars), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 0 { return nil, cobra.ShellCompDirectiveNoFileComp @@ -27,14 +35,8 @@ var ( return node_names, cobra.ShellCompDirectiveNoFileComp }, } - full bool -) - -func init() { - powerCmd.PersistentFlags().BoolVarP(&full, "full", "F", false, "show detailed output.") -} - -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { + 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") + powerCmd.PersistentFlags().IntVar(&vars.Fanout, "fanout", 50, "how many command should be executed in parallel") return powerCmd } diff --git a/internal/app/wwctl/node/sensors/root_test.go b/internal/app/wwctl/node/sensors/root_test.go new file mode 100644 index 00000000..3fff5f6a --- /dev/null +++ b/internal/app/wwctl/node/sensors/root_test.go @@ -0,0 +1,56 @@ +package sensors + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_Sensors(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll(t) + env.WriteFile(t, "etc/warewulf/nodes.conf", `WW_INTERNAL: 43 +nodeprofiles: + default: + ipmi: + template: ipmitool.tmpl + username: admin + password: admin +nodes: + n01: + profiles: + - default + ipmi: + ipaddr: 10.10.10.10`) + env.ImportFile(t, "share/warewulf/bmc/ipmitool.tmpl", "../../../../../lib/warewulf/bmc/ipmitool.tmpl") + + tests := map[string]struct { + args []string + expected string + }{ + "sensors": { + args: []string{"--show", "n01"}, + expected: "10.10.10.10: ipmitool -I lan -H 10.10.10.10 -p 623 -U admin -P admin -e ~ sdr list", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + baseCmd := GetCommand() + buf := new(bytes.Buffer) + baseCmd.SetOut(buf) + baseCmd.SetErr(buf) + wwlog.SetLogWriter(buf) + baseCmd.SetArgs(tt.args) + err := baseCmd.Execute() + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(tt.expected), strings.TrimSpace(buf.String())) + }) + } +} diff --git a/internal/app/wwctl/power/cycle/main.go b/internal/app/wwctl/power/cycle/main.go index 0018f857..1ed8e57a 100644 --- a/internal/app/wwctl/power/cycle/main.go +++ b/internal/app/wwctl/power/cycle/main.go @@ -12,84 +12,70 @@ import ( "github.com/warewulf/warewulf/internal/pkg/wwlog" ) -func CobraRunE(cmd *cobra.Command, args []string) error { - var returnErr error = nil - - nodeDB, err := node.New() - if err != nil { - return fmt.Errorf("could not open node configuration: %s", err) - } - - nodes, err := nodeDB.FindAllNodes() - if err != nil { - return fmt.Errorf("could not get node list: %s", err) - } - - if len(args) > 0 { - nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) - } else { - //nolint:errcheck - cmd.Usage() - os.Exit(1) - } - - if len(nodes) == 0 { - return fmt.Errorf("no nodes found") - } - - batchpool := batch.New(50) - jobcount := len(nodes) - results := make(chan power.IPMI, jobcount) - - for _, node := range nodes { - - if node.Ipmi.Ipaddr.IsUnspecified() { - wwlog.Error("%s: No IPMI IP address", node.Id()) - continue - } - var ipmiInterface = "lan" - if node.Ipmi.Interface != "" { - ipmiInterface = node.Ipmi.Interface - } - var ipmiPort = "623" - if node.Ipmi.Port != "" { - ipmiPort = node.Ipmi.Port - } - ipmiCmd := power.IPMI{ - NodeName: node.Id(), - HostName: node.Ipmi.Ipaddr.String(), - Port: ipmiPort, - User: node.Ipmi.UserName, - Password: node.Ipmi.Password, - Interface: ipmiInterface, - AuthType: "MD5", - } - - batchpool.Submit(func() { - //nolint:errcheck - ipmiCmd.PowerCycle() - results <- ipmiCmd - }) - - } - - batchpool.Run() - - close(results) - - for result := range results { - - out, err := result.Result() +func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { + return func(cmd *cobra.Command, args []string) error { + var returnErr error = nil + nodeDB, err := node.New() if err != nil { - wwlog.Error("%s: %s", result.NodeName, out) - returnErr = err - continue + return fmt.Errorf("could not open node configuration: %s", err) } - fmt.Printf("%s: %s\n", result.NodeName, out) + nodes, err := nodeDB.FindAllNodes() + if err != nil { + return fmt.Errorf("could not get node list: %s", err) + } + if len(args) > 0 { + nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) + } else { + //nolint:errcheck + cmd.Usage() + os.Exit(1) + } + + if len(nodes) == 0 { + return fmt.Errorf("no nodes found") + } + + batchpool := batch.New(vars.Fanout) + jobcount := len(nodes) + results := make(chan power.IPMI, jobcount) + + for _, node := range nodes { + + if node.Ipmi.Ipaddr.IsUnspecified() { + wwlog.Error("%s: No IPMI IP address", node.Id()) + continue + } + ipmiCmd := power.IPMI{ + IpmiConf: *node.Ipmi, + ShowOnly: vars.Showcmd, + } + batchpool.Submit(func() { + //nolint:errcheck + ipmiCmd.PowerCycle() + results <- ipmiCmd + }) + + } + + batchpool.Run() + + close(results) + + for result := range results { + + out, err := result.Result() + + if err != nil { + wwlog.Error("%s: %s", result.Ipaddr, out) + returnErr = err + continue + } + wwlog.Info("%s: %s\n", result.Ipaddr, out) + } + + return returnErr } - - return returnErr } diff --git a/internal/app/wwctl/power/cycle/root.go b/internal/app/wwctl/power/cycle/root.go index 803ba0da..8b450b8c 100644 --- a/internal/app/wwctl/power/cycle/root.go +++ b/internal/app/wwctl/power/cycle/root.go @@ -2,19 +2,38 @@ package cycle import ( "github.com/spf13/cobra" + "github.com/warewulf/warewulf/internal/pkg/node" ) -var ( - powerCmd = &cobra.Command{ +type variables struct { + Showcmd bool + Fanout int +} + +// GetRootCommand returns the root cobra.Command for the application. +func GetCommand() *cobra.Command { + vars := variables{} + powerCmd := &cobra.Command{ DisableFlagsInUseLine: true, Use: "cycle [OPTIONS] [PATTERN ...]", Short: "Power cycle the given node(s)", Long: "This command cycles power for a set of nodes specified by PATTERN.", - RunE: CobraRunE, - } -) + RunE: CobraRunE(&vars), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return nil, cobra.ShellCompDirectiveNoFileComp + } -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { + 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 + }, + } + powerCmd.PersistentFlags().BoolVarP(&vars.Showcmd, "show", "s", false, "only show command which will be executed") + powerCmd.PersistentFlags().IntVar(&vars.Fanout, "fanout", 50, "how many command should be executed in parallel") return powerCmd } diff --git a/internal/app/wwctl/power/cycle/root_test.go b/internal/app/wwctl/power/cycle/root_test.go new file mode 100644 index 00000000..89ba118c --- /dev/null +++ b/internal/app/wwctl/power/cycle/root_test.go @@ -0,0 +1,56 @@ +package cycle + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_PowerCycle(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll(t) + env.WriteFile(t, "etc/warewulf/nodes.conf", `WW_INTERNAL: 43 +nodeprofiles: + default: + ipmi: + template: ipmitool.tmpl + username: admin + password: admin +nodes: + n01: + profiles: + - default + ipmi: + ipaddr: 10.10.10.10`) + env.ImportFile(t, "share/warewulf/bmc/ipmitool.tmpl", "../../../../../lib/warewulf/bmc/ipmitool.tmpl") + + tests := map[string]struct { + args []string + expected string + }{ + "power cycle": { + args: []string{"--show", "n01"}, + expected: "10.10.10.10: ipmitool -I lan -H 10.10.10.10 -p 623 -U admin -P admin -e ~ chassis power cycle", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + baseCmd := GetCommand() + buf := new(bytes.Buffer) + baseCmd.SetOut(buf) + baseCmd.SetErr(buf) + wwlog.SetLogWriter(buf) + baseCmd.SetArgs(tt.args) + err := baseCmd.Execute() + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(tt.expected), strings.TrimSpace(buf.String())) + }) + } +} diff --git a/internal/app/wwctl/power/off/main.go b/internal/app/wwctl/power/off/main.go index 278c08f2..9426ff0e 100644 --- a/internal/app/wwctl/power/off/main.go +++ b/internal/app/wwctl/power/off/main.go @@ -12,84 +12,72 @@ import ( "github.com/warewulf/warewulf/internal/pkg/wwlog" ) -func CobraRunE(cmd *cobra.Command, args []string) error { - var returnErr error = nil - - nodeDB, err := node.New() - if err != nil { - return fmt.Errorf("could not open node configuration: %s", err) - } - - nodes, err := nodeDB.FindAllNodes() - if err != nil { - return fmt.Errorf("could not get node list: %s", err) - } - - if len(args) > 0 { - nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) - } else { - //nolint:errcheck - cmd.Usage() - os.Exit(1) - } - - if len(nodes) == 0 { - return fmt.Errorf("no nodes found") - } - - batchpool := batch.New(50) - jobcount := len(nodes) - results := make(chan power.IPMI, jobcount) - - for _, node := range nodes { - - if node.Ipmi.Ipaddr.IsUnspecified() { - wwlog.Error("%s: No IPMI IP address", node.Id()) - continue - } - var ipmiInterface = "lan" - if node.Ipmi.Interface != "" { - ipmiInterface = node.Ipmi.Interface - } - var ipmiPort = "623" - if node.Ipmi.Port != "" { - ipmiPort = node.Ipmi.Port - } - ipmiCmd := power.IPMI{ - NodeName: node.Id(), - HostName: node.Ipmi.Ipaddr.String(), - Port: ipmiPort, - User: node.Ipmi.UserName, - Password: node.Ipmi.Password, - Interface: ipmiInterface, - AuthType: "MD5", - } - - batchpool.Submit(func() { - //nolint:errcheck - ipmiCmd.PowerOff() - results <- ipmiCmd - }) - - } - - batchpool.Run() - - close(results) - - for result := range results { - - out, err := result.Result() +func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { + return func(cmd *cobra.Command, args []string) error { + var returnErr error = nil + nodeDB, err := node.New() if err != nil { - wwlog.Error("%s: %s", result.NodeName, out) - returnErr = err - continue + return fmt.Errorf("could not open node configuration: %s", err) } - fmt.Printf("%s: %s\n", result.NodeName, out) + nodes, err := nodeDB.FindAllNodes() + if err != nil { + return fmt.Errorf("could not get node list: %s", err) + } + if len(args) > 0 { + nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) + } else { + //nolint:errcheck + cmd.Usage() + os.Exit(1) + } + + if len(nodes) == 0 { + return fmt.Errorf("no nodes found") + } + + batchpool := batch.New(vars.Fanout) + jobcount := len(nodes) + results := make(chan power.IPMI, jobcount) + + for _, node := range nodes { + + if node.Ipmi.Ipaddr.IsUnspecified() { + wwlog.Error("%s: No IPMI IP address", node.Id()) + continue + } + ipmiCmd := power.IPMI{ + IpmiConf: *node.Ipmi, + ShowOnly: vars.Showcmd, + } + batchpool.Submit(func() { + //nolint:errcheck + ipmiCmd.PowerOff() + results <- ipmiCmd + }) + + } + + batchpool.Run() + + close(results) + + for result := range results { + + out, err := result.Result() + + if err != nil { + wwlog.Error("%s: %s", result.Ipaddr, out) + returnErr = err + continue + } + + wwlog.Info("%s: %s\n", result.Ipaddr, out) + + } + + return returnErr } - - return returnErr } diff --git a/internal/app/wwctl/power/off/root.go b/internal/app/wwctl/power/off/root.go index 004ad427..43670e29 100644 --- a/internal/app/wwctl/power/off/root.go +++ b/internal/app/wwctl/power/off/root.go @@ -5,13 +5,20 @@ import ( "github.com/warewulf/warewulf/internal/pkg/node" ) -var ( - powerCmd = &cobra.Command{ +type variables struct { + Showcmd bool + Fanout int +} + +// GetRootCommand returns the root cobra.Command for the application. +func GetCommand() *cobra.Command { + vars := variables{} + powerCmd := &cobra.Command{ DisableFlagsInUseLine: true, Use: "off [OPTIONS] [PATTERN ...]", Short: "Power off the given node(s)", Long: "This command will shutdown power to a set of nodes specified by PATTERN.", - RunE: CobraRunE, + RunE: CobraRunE(&vars), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 0 { return nil, cobra.ShellCompDirectiveNoFileComp @@ -26,9 +33,8 @@ var ( return node_names, cobra.ShellCompDirectiveNoFileComp }, } -) + powerCmd.PersistentFlags().BoolVarP(&vars.Showcmd, "show", "s", false, "only show command which will be executed") + powerCmd.PersistentFlags().IntVar(&vars.Fanout, "fanout", 50, "how many command should be executed in parallel") -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { return powerCmd } diff --git a/internal/app/wwctl/power/off/root_test.go b/internal/app/wwctl/power/off/root_test.go new file mode 100644 index 00000000..4dd31141 --- /dev/null +++ b/internal/app/wwctl/power/off/root_test.go @@ -0,0 +1,56 @@ +package off + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_Power_Status(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll(t) + env.WriteFile(t, "etc/warewulf/nodes.conf", `WW_INTERNAL: 43 +nodeprofiles: + default: + ipmi: + template: ipmitool.tmpl + username: admin + password: admin +nodes: + n01: + profiles: + - default + ipmi: + ipaddr: 10.10.10.10`) + env.ImportFile(t, "share/warewulf/bmc/ipmitool.tmpl", "../../../../../lib/warewulf/bmc/ipmitool.tmpl") + + tests := map[string]struct { + args []string + expected string + }{ + "power off": { + args: []string{"--show", "n01"}, + expected: "10.10.10.10: ipmitool -I lan -H 10.10.10.10 -p 623 -U admin -P admin -e ~ chassis power off", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + baseCmd := GetCommand() + buf := new(bytes.Buffer) + baseCmd.SetOut(buf) + baseCmd.SetErr(buf) + wwlog.SetLogWriter(buf) + baseCmd.SetArgs(tt.args) + err := baseCmd.Execute() + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(tt.expected), strings.TrimSpace(buf.String())) + }) + } +} diff --git a/internal/app/wwctl/power/on/main.go b/internal/app/wwctl/power/on/main.go index 9bf926d5..eed53bb9 100644 --- a/internal/app/wwctl/power/on/main.go +++ b/internal/app/wwctl/power/on/main.go @@ -12,84 +12,73 @@ import ( "github.com/warewulf/warewulf/internal/pkg/wwlog" ) -func CobraRunE(cmd *cobra.Command, args []string) error { - var returnErr error = nil +func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { + return func(cmd *cobra.Command, args []string) error { - nodeDB, err := node.New() - if err != nil { - return fmt.Errorf("could not open node configuration: %s", err) - } - - nodes, err := nodeDB.FindAllNodes() - if err != nil { - return fmt.Errorf("could not get node list: %s", err) - } - - if len(args) > 0 { - nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) - } else { - //nolint:errcheck - cmd.Usage() - os.Exit(1) - } - - if len(nodes) == 0 { - return fmt.Errorf("no nodes found") - } - - batchpool := batch.New(50) - jobcount := len(nodes) - results := make(chan power.IPMI, jobcount) - - for _, node := range nodes { - - if node.Ipmi.Ipaddr.IsUnspecified() { - wwlog.Error("%s: No IPMI IP address", node.Id()) - continue - } - var ipmiInterface = "lan" - if node.Ipmi.Interface != "" { - ipmiInterface = node.Ipmi.Interface - } - var ipmiPort = "623" - if node.Ipmi.Port != "" { - ipmiPort = node.Ipmi.Port - } - ipmiCmd := power.IPMI{ - NodeName: node.Id(), - HostName: node.Ipmi.Ipaddr.String(), - Port: ipmiPort, - User: node.Ipmi.UserName, - Password: node.Ipmi.Password, - Interface: ipmiInterface, - AuthType: "MD5", - } - - batchpool.Submit(func() { - //nolint:errcheck - ipmiCmd.PowerOn() - results <- ipmiCmd - }) - - } - - batchpool.Run() - - close(results) - - for result := range results { - - out, err := result.Result() + var returnErr error = nil + nodeDB, err := node.New() if err != nil { - wwlog.Error("%s: %s", result.NodeName, out) - returnErr = err - continue + return fmt.Errorf("could not open node configuration: %s", err) } - fmt.Printf("%s: %s\n", result.NodeName, out) + nodes, err := nodeDB.FindAllNodes() + if err != nil { + return fmt.Errorf("could not get node list: %s", err) + } + if len(args) > 0 { + nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) + } else { + //nolint:errcheck + cmd.Usage() + os.Exit(1) + } + + if len(nodes) == 0 { + return fmt.Errorf("no nodes found") + } + + batchpool := batch.New(vars.Fanout) + jobcount := len(nodes) + results := make(chan power.IPMI, jobcount) + + for _, node := range nodes { + + if node.Ipmi.Ipaddr.IsUnspecified() { + wwlog.Error("%s: No IPMI IP address", node.Id()) + continue + } + ipmiCmd := power.IPMI{ + IpmiConf: *node.Ipmi, + ShowOnly: vars.Showcmd, + } + batchpool.Submit(func() { + //nolint:errcheck + ipmiCmd.PowerOn() + results <- ipmiCmd + }) + + } + + batchpool.Run() + + close(results) + + for result := range results { + + out, err := result.Result() + + if err != nil { + wwlog.Error("%s: %s", result.Ipaddr, out) + returnErr = err + continue + } + + wwlog.Info("%s: %s\n", result.Ipaddr, out) + + } + + return returnErr } - - return returnErr } diff --git a/internal/app/wwctl/power/on/root.go b/internal/app/wwctl/power/on/root.go index 42c0ebf4..f5841222 100644 --- a/internal/app/wwctl/power/on/root.go +++ b/internal/app/wwctl/power/on/root.go @@ -5,12 +5,19 @@ import ( "github.com/warewulf/warewulf/internal/pkg/node" ) -var ( - powerCmd = &cobra.Command{ +type variables struct { + Showcmd bool + Fanout int +} + +// GetRootCommand returns the root cobra.Command for the application. +func GetCommand() *cobra.Command { + vars := variables{} + powerCmd := &cobra.Command{ Use: "on [OPTIONS] [PATTERN ...]", Short: "Power on the given node(s)", Long: "This command will power on a set of nodes specified by PATTERN.", - RunE: CobraRunE, + RunE: CobraRunE(&vars), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 0 { return nil, cobra.ShellCompDirectiveNoFileComp @@ -25,9 +32,8 @@ var ( return node_names, cobra.ShellCompDirectiveNoFileComp }, } -) + powerCmd.PersistentFlags().BoolVarP(&vars.Showcmd, "show", "s", false, "only show command which will be executed") + powerCmd.PersistentFlags().IntVar(&vars.Fanout, "fanout", 50, "how many command should be executed in parallel") -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { return powerCmd } diff --git a/internal/app/wwctl/power/on/root_test.go b/internal/app/wwctl/power/on/root_test.go new file mode 100644 index 00000000..82485f10 --- /dev/null +++ b/internal/app/wwctl/power/on/root_test.go @@ -0,0 +1,55 @@ +package on + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_Power_Status(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll(t) + env.WriteFile(t, "etc/warewulf/nodes.conf", `WW_INTERNAL: 43 +nodeprofiles: + default: + ipmi: + template: ipmitool.tmpl + username: admin + password: admin +nodes: + n01: + profiles: + - default + ipmi: + ipaddr: 10.10.10.10`) + env.ImportFile(t, "share/warewulf/bmc/ipmitool.tmpl", "../../../../../lib/warewulf/bmc/ipmitool.tmpl") + + tests := map[string]struct { + args []string + expected string + }{ + "power on": { + args: []string{"--show", "n01"}, + expected: "10.10.10.10: ipmitool -I lan -H 10.10.10.10 -p 623 -U admin -P admin -e ~ chassis power on", + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + baseCmd := GetCommand() + buf := new(bytes.Buffer) + baseCmd.SetOut(buf) + baseCmd.SetErr(buf) + wwlog.SetLogWriter(buf) + baseCmd.SetArgs(tt.args) + err := baseCmd.Execute() + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(tt.expected), strings.TrimSpace(buf.String())) + }) + } +} diff --git a/internal/app/wwctl/power/reset/main.go b/internal/app/wwctl/power/reset/main.go index b3550944..aad82f42 100644 --- a/internal/app/wwctl/power/reset/main.go +++ b/internal/app/wwctl/power/reset/main.go @@ -12,84 +12,73 @@ import ( "github.com/warewulf/warewulf/internal/pkg/wwlog" ) -func CobraRunE(cmd *cobra.Command, args []string) error { - var returnErr error = nil +func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { + return func(cmd *cobra.Command, args []string) error { - nodeDB, err := node.New() - if err != nil { - return fmt.Errorf("could not open node configuration: %s", err) - } - - nodes, err := nodeDB.FindAllNodes() - if err != nil { - return fmt.Errorf("cloud not get nodeList: %s", err) - } - - if len(args) > 0 { - nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) - } else { - //nolint:errcheck - cmd.Usage() - os.Exit(1) - } - - if len(nodes) == 0 { - return fmt.Errorf("no nodes found") - } - - batchpool := batch.New(50) - jobcount := len(nodes) - results := make(chan power.IPMI, jobcount) - - for _, node := range nodes { - - if node.Ipmi.Ipaddr.IsUnspecified() { - wwlog.Error("%s: No IPMI IP address", node.Id()) - continue - } - var ipmiInterface = "lan" - if node.Ipmi.Interface != "" { - ipmiInterface = node.Ipmi.Interface - } - var ipmiPort = "623" - if node.Ipmi.Port != "" { - ipmiPort = node.Ipmi.Port - } - ipmiCmd := power.IPMI{ - NodeName: node.Id(), - HostName: node.Ipmi.Ipaddr.String(), - Port: ipmiPort, - User: node.Ipmi.UserName, - Password: node.Ipmi.Password, - Interface: ipmiInterface, - AuthType: "MD5", - } - - batchpool.Submit(func() { - //nolint:errcheck - ipmiCmd.PowerReset() - results <- ipmiCmd - }) - - } - - batchpool.Run() - - close(results) - - for result := range results { - - out, err := result.Result() + var returnErr error = nil + nodeDB, err := node.New() if err != nil { - wwlog.Error("%s: %s", result.NodeName, out) - returnErr = err - continue + return fmt.Errorf("could not open node configuration: %s", err) } - fmt.Printf("%s: %s\n", result.NodeName, out) + nodes, err := nodeDB.FindAllNodes() + if err != nil { + return fmt.Errorf("cloud not get nodeList: %s", err) + } + if len(args) > 0 { + nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) + } else { + //nolint:errcheck + cmd.Usage() + os.Exit(1) + } + + if len(nodes) == 0 { + return fmt.Errorf("no nodes found") + } + + batchpool := batch.New(vars.Fanout) + jobcount := len(nodes) + results := make(chan power.IPMI, jobcount) + + for _, node := range nodes { + + if node.Ipmi.Ipaddr.IsUnspecified() { + wwlog.Error("%s: No IPMI IP address", node.Id()) + continue + } + ipmiCmd := power.IPMI{ + IpmiConf: *node.Ipmi, + ShowOnly: vars.Showcmd, + } + batchpool.Submit(func() { + //nolint:errcheck + ipmiCmd.PowerReset() + results <- ipmiCmd + }) + + } + + batchpool.Run() + + close(results) + + for result := range results { + + out, err := result.Result() + + if err != nil { + wwlog.Error("%s: %s", result.Ipaddr, out) + returnErr = err + continue + } + + wwlog.Info("%s: %s\n", result.Ipaddr, out) + + } + + return returnErr } - - return returnErr } diff --git a/internal/app/wwctl/power/reset/root.go b/internal/app/wwctl/power/reset/root.go index 7be3abdf..c7346135 100644 --- a/internal/app/wwctl/power/reset/root.go +++ b/internal/app/wwctl/power/reset/root.go @@ -5,13 +5,20 @@ import ( "github.com/warewulf/warewulf/internal/pkg/node" ) -var ( - powerCmd = &cobra.Command{ +type variables struct { + Showcmd bool + Fanout int +} + +// GetRootCommand returns the root cobra.Command for the application. +func GetCommand() *cobra.Command { + vars := variables{} + powerCmd := &cobra.Command{ DisableFlagsInUseLine: true, Use: "reset [OPTIONS] [PATTERN ...]", Short: "Issue a reset to node(s)", Long: "This command will issue a reset to a set of nodes specified by PATTERN.", - RunE: CobraRunE, + RunE: CobraRunE(&vars), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 0 { return nil, cobra.ShellCompDirectiveNoFileComp @@ -26,9 +33,7 @@ var ( return node_names, cobra.ShellCompDirectiveNoFileComp }, } -) - -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { + powerCmd.PersistentFlags().BoolVarP(&vars.Showcmd, "show", "s", false, "only show command which will be executed") + powerCmd.PersistentFlags().IntVar(&vars.Fanout, "fanout", 50, "how many command should be executed in parallel") return powerCmd } diff --git a/internal/app/wwctl/power/reset/root_test.go b/internal/app/wwctl/power/reset/root_test.go new file mode 100644 index 00000000..131f831a --- /dev/null +++ b/internal/app/wwctl/power/reset/root_test.go @@ -0,0 +1,55 @@ +package reset + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_Power_Status(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll(t) + env.WriteFile(t, "etc/warewulf/nodes.conf", `WW_INTERNAL: 43 +nodeprofiles: + default: + ipmi: + template: ipmitool.tmpl + username: admin + password: admin +nodes: + n01: + profiles: + - default + ipmi: + ipaddr: 10.10.10.10`) + env.ImportFile(t, "share/warewulf/bmc/ipmitool.tmpl", "../../../../../lib/warewulf/bmc/ipmitool.tmpl") + + tests := map[string]struct { + args []string + expected string + }{ + "power reset": { + args: []string{"--show", "n01"}, + expected: "10.10.10.10: ipmitool -I lan -H 10.10.10.10 -p 623 -U admin -P admin -e ~ chassis power reset", + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + baseCmd := GetCommand() + buf := new(bytes.Buffer) + baseCmd.SetOut(buf) + baseCmd.SetErr(buf) + wwlog.SetLogWriter(buf) + baseCmd.SetArgs(tt.args) + err := baseCmd.Execute() + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(tt.expected), strings.TrimSpace(buf.String())) + }) + } +} diff --git a/internal/app/wwctl/power/soft/main.go b/internal/app/wwctl/power/soft/main.go index 9eede24a..4a9359dd 100644 --- a/internal/app/wwctl/power/soft/main.go +++ b/internal/app/wwctl/power/soft/main.go @@ -12,84 +12,73 @@ import ( "github.com/warewulf/warewulf/internal/pkg/wwlog" ) -func CobraRunE(cmd *cobra.Command, args []string) error { - var returnErr error = nil +func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { + return func(cmd *cobra.Command, args []string) error { - nodeDB, err := node.New() - if err != nil { - return fmt.Errorf("could not open node configuration: %s", err) - } - - nodes, err := nodeDB.FindAllNodes() - if err != nil { - return fmt.Errorf("could not get nodeList: %s", err) - } - - if len(args) > 0 { - nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) - } else { - //nolint:errcheck - cmd.Usage() - os.Exit(1) - } - - if len(nodes) == 0 { - return fmt.Errorf("no nodes found") - } - - batchpool := batch.New(50) - jobcount := len(nodes) - results := make(chan power.IPMI, jobcount) - - for _, node := range nodes { - - if node.Ipmi.Ipaddr.IsUnspecified() { - wwlog.Error("%s: No IPMI IP address", node.Id()) - continue - } - var ipmiInterface = "lan" - if node.Ipmi.Interface != "" { - ipmiInterface = node.Ipmi.Interface - } - var ipmiPort = "623" - if node.Ipmi.Port != "" { - ipmiPort = node.Ipmi.Port - } - ipmiCmd := power.IPMI{ - NodeName: node.Id(), - HostName: node.Ipmi.Ipaddr.String(), - Port: ipmiPort, - User: node.Ipmi.UserName, - Password: node.Ipmi.Password, - Interface: ipmiInterface, - AuthType: "MD5", - } - - batchpool.Submit(func() { - //nolint:errcheck - ipmiCmd.PowerSoft() - results <- ipmiCmd - }) - - } - - batchpool.Run() - - close(results) - - for result := range results { - - out, err := result.Result() + var returnErr error = nil + nodeDB, err := node.New() if err != nil { - wwlog.Error("%s: %s", result.NodeName, out) - returnErr = err - continue + return fmt.Errorf("could not open node configuration: %s", err) } - fmt.Printf("%s: %s\n", result.NodeName, out) + nodes, err := nodeDB.FindAllNodes() + if err != nil { + return fmt.Errorf("could not get nodeList: %s", err) + } + if len(args) > 0 { + nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) + } else { + //nolint:errcheck + cmd.Usage() + os.Exit(1) + } + + if len(nodes) == 0 { + return fmt.Errorf("no nodes found") + } + + batchpool := batch.New(vars.Fanout) + jobcount := len(nodes) + results := make(chan power.IPMI, jobcount) + + for _, node := range nodes { + + if node.Ipmi.Ipaddr.IsUnspecified() { + wwlog.Error("%s: No IPMI IP address", node.Id()) + continue + } + ipmiCmd := power.IPMI{ + IpmiConf: *node.Ipmi, + ShowOnly: vars.Showcmd, + } + batchpool.Submit(func() { + //nolint:errcheck + ipmiCmd.PowerSoft() + results <- ipmiCmd + }) + + } + + batchpool.Run() + + close(results) + + for result := range results { + + out, err := result.Result() + + if err != nil { + wwlog.Error("%s: %s", result.Ipaddr, out) + returnErr = err + continue + } + + wwlog.Info("%s: %s\n", result.Ipaddr, out) + + } + + return returnErr } - - return returnErr } diff --git a/internal/app/wwctl/power/soft/root.go b/internal/app/wwctl/power/soft/root.go index 791aa0e0..10093211 100644 --- a/internal/app/wwctl/power/soft/root.go +++ b/internal/app/wwctl/power/soft/root.go @@ -5,13 +5,20 @@ import ( "github.com/warewulf/warewulf/internal/pkg/node" ) -var ( - powerCmd = &cobra.Command{ +type variables struct { + Showcmd bool + Fanout int +} + +// GetRootCommand returns the root cobra.Command for the application. +func GetCommand() *cobra.Command { + vars := variables{} + powerCmd := &cobra.Command{ DisableFlagsInUseLine: true, Use: "soft", Short: "Gracefully shuts down the given node(s)", Long: "This command uses the operating system to shut down the set of nodes specified by PATTERN.", - RunE: CobraRunE, + RunE: CobraRunE(&vars), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 0 { return nil, cobra.ShellCompDirectiveNoFileComp @@ -26,9 +33,7 @@ var ( return node_names, cobra.ShellCompDirectiveNoFileComp }, } -) - -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { + powerCmd.PersistentFlags().BoolVarP(&vars.Showcmd, "show", "s", false, "only show command which will be executed") + powerCmd.PersistentFlags().IntVar(&vars.Fanout, "fanout", 50, "how many command should be executed in parallel") return powerCmd } diff --git a/internal/app/wwctl/power/soft/root_test.go b/internal/app/wwctl/power/soft/root_test.go new file mode 100644 index 00000000..14728692 --- /dev/null +++ b/internal/app/wwctl/power/soft/root_test.go @@ -0,0 +1,56 @@ +package soft + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_Power_Status(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll(t) + env.WriteFile(t, "etc/warewulf/nodes.conf", `WW_INTERNAL: 43 +nodeprofiles: + default: + ipmi: + template: ipmitool.tmpl + username: admin + password: admin +nodes: + n01: + profiles: + - default + ipmi: + ipaddr: 10.10.10.10`) + env.ImportFile(t, "share/warewulf/bmc/ipmitool.tmpl", "../../../../../lib/warewulf/bmc/ipmitool.tmpl") + + tests := map[string]struct { + args []string + expected string + }{ + "power soft": { + args: []string{"--show", "n01"}, + expected: "10.10.10.10: ipmitool -I lan -H 10.10.10.10 -p 623 -U admin -P admin -e ~ chassis power soft", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + baseCmd := GetCommand() + buf := new(bytes.Buffer) + baseCmd.SetOut(buf) + baseCmd.SetErr(buf) + wwlog.SetLogWriter(buf) + baseCmd.SetArgs(tt.args) + err := baseCmd.Execute() + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(tt.expected), strings.TrimSpace(buf.String())) + }) + } +} diff --git a/internal/app/wwctl/power/status/main.go b/internal/app/wwctl/power/status/main.go index 6bfe4d15..d2d15fe9 100644 --- a/internal/app/wwctl/power/status/main.go +++ b/internal/app/wwctl/power/status/main.go @@ -12,84 +12,72 @@ import ( "github.com/warewulf/warewulf/internal/pkg/wwlog" ) -func CobraRunE(cmd *cobra.Command, args []string) error { - var returnErr error = nil - - nodeDB, err := node.New() - if err != nil { - return fmt.Errorf("could not open node configuration: %s", err) - } - - nodes, err := nodeDB.FindAllNodes() - if err != nil { - return fmt.Errorf("could not get node list: %s", err) - } - - if len(args) > 0 { - nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) - } else { - //nolint:errcheck - cmd.Usage() - os.Exit(1) - } - - if len(nodes) == 0 { - return fmt.Errorf("no nodes found") - } - - batchpool := batch.New(50) - jobcount := len(nodes) - results := make(chan power.IPMI, jobcount) - - for _, node := range nodes { - - if node.Ipmi.Ipaddr.IsUnspecified() { - wwlog.Error("%s: No IPMI IP address", node.Id()) - continue - } - var ipmiInterface = "lan" - if node.Ipmi.Interface != "" { - ipmiInterface = node.Ipmi.Interface - } - var ipmiPort = "623" - if node.Ipmi.Port != "" { - ipmiPort = node.Ipmi.Port - } - ipmiCmd := power.IPMI{ - NodeName: node.Id(), - HostName: node.Ipmi.Ipaddr.String(), - Port: ipmiPort, - User: node.Ipmi.UserName, - Password: node.Ipmi.Password, - Interface: ipmiInterface, - AuthType: "MD5", - } - - batchpool.Submit(func() { - //nolint:errcheck - ipmiCmd.PowerStatus() - results <- ipmiCmd - }) - - } - - batchpool.Run() - - close(results) - - for result := range results { - - out, err := result.Result() +func CobraRunE(vars *variables) func(cmd *cobra.Command, args []string) (err error) { + return func(cmd *cobra.Command, args []string) error { + var returnErr error = nil + nodeDB, err := node.New() if err != nil { - wwlog.Error("%s: %s", result.NodeName, out) - returnErr = err - continue + return err } - fmt.Printf("%s: %s\n", result.NodeName, out) + nodes, err := nodeDB.FindAllNodes() + if err != nil { + return err + } + if len(args) > 0 { + nodes = node.FilterNodeListByName(nodes, hostlist.Expand(args)) + } else { + //nolint:errcheck + cmd.Usage() + os.Exit(1) + } + + if len(nodes) == 0 { + return fmt.Errorf("no nodes found") + } + + batchpool := batch.New(50) + jobcount := len(nodes) + results := make(chan power.IPMI, jobcount) + + for _, node := range nodes { + + if node.Ipmi.Ipaddr.IsUnspecified() { + wwlog.Error("%s: No IPMI IP address", node.Id()) + continue + } + ipmiCmd := power.IPMI{ + IpmiConf: *node.Ipmi, + ShowOnly: vars.Showcmd, + } + batchpool.Submit(func() { + //nolint:errcheck + ipmiCmd.PowerStatus() + results <- ipmiCmd + }) + + } + + batchpool.Run() + + close(results) + + for result := range results { + + out, err := result.Result() + + if err != nil { + wwlog.Error("%s: %s", result.Ipaddr, out) + returnErr = err + continue + } + + wwlog.Info("%s: %s\n", result.Ipaddr, out) + + } + + return returnErr } - - return returnErr } diff --git a/internal/app/wwctl/power/status/root.go b/internal/app/wwctl/power/status/root.go index 4fda726c..1176792d 100644 --- a/internal/app/wwctl/power/status/root.go +++ b/internal/app/wwctl/power/status/root.go @@ -5,13 +5,20 @@ import ( "github.com/warewulf/warewulf/internal/pkg/node" ) -var ( - powerCmd = &cobra.Command{ +type variables struct { + Showcmd bool + Fanout int +} + +// GetRootCommand returns the root cobra.Command for the application. +func GetCommand() *cobra.Command { + vars := variables{} + powerCmd := &cobra.Command{ DisableFlagsInUseLine: true, Use: "status [OPTIONS] [PATTERN ...]", Short: "Show power status for the given node(s)", Long: "This command displays the power status of a set of nodes specified by PATTERN.", - RunE: CobraRunE, + RunE: CobraRunE(&vars), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 0 { return nil, cobra.ShellCompDirectiveNoFileComp @@ -26,9 +33,7 @@ var ( return node_names, cobra.ShellCompDirectiveNoFileComp }, } -) - -// GetRootCommand returns the root cobra.Command for the application. -func GetCommand() *cobra.Command { + powerCmd.PersistentFlags().BoolVarP(&vars.Showcmd, "show", "s", false, "only show command which will be executed") + powerCmd.PersistentFlags().IntVar(&vars.Fanout, "fanout", 50, "how many command should be executed in parallel") return powerCmd } diff --git a/internal/app/wwctl/power/status/root_test.go b/internal/app/wwctl/power/status/root_test.go new file mode 100644 index 00000000..e7160e3e --- /dev/null +++ b/internal/app/wwctl/power/status/root_test.go @@ -0,0 +1,56 @@ +package status + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func Test_Power_Status(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll(t) + env.WriteFile(t, "etc/warewulf/nodes.conf", `WW_INTERNAL: 43 +nodeprofiles: + default: + ipmi: + template: ipmitool.tmpl + username: admin + password: admin +nodes: + n01: + profiles: + - default + ipmi: + ipaddr: 10.10.10.10`) + env.ImportFile(t, "share/warewulf/bmc/ipmitool.tmpl", "../../../../../lib/warewulf/bmc/ipmitool.tmpl") + + tests := map[string]struct { + args []string + expected string + }{ + "sensors": { + args: []string{"--show", "n01"}, + expected: "10.10.10.10: ipmitool -I lan -H 10.10.10.10 -p 623 -U admin -P admin -e ~ chassis power status", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + baseCmd := GetCommand() + buf := new(bytes.Buffer) + baseCmd.SetOut(buf) + baseCmd.SetErr(buf) + wwlog.SetLogWriter(buf) + baseCmd.SetArgs(tt.args) + err := baseCmd.Execute() + assert.NoError(t, err) + assert.Equal(t, strings.TrimSpace(tt.expected), strings.TrimSpace(buf.String())) + }) + } +} diff --git a/internal/pkg/node/datastructure.go b/internal/pkg/node/datastructure.go index 44597ad2..bf44236b 100644 --- a/internal/pkg/node/datastructure.go +++ b/internal/pkg/node/datastructure.go @@ -68,6 +68,7 @@ type IpmiConf struct { Interface string `yaml:"interface,omitempty" lopt:"ipmiinterface" comment:"Set the node's IPMI interface (defaults: 'lan')"` EscapeChar string `yaml:"escapechar,omitempty" lopt:"ipmiescapechar" comment:"Set the IPMI escape character (defaults: '~')"` Write wwtype.WWbool `yaml:"write,omitempty" lopt:"ipmiwrite" comment:"Enable the write of impi configuration (true/false)"` + Template string `yaml:"template,omitempty" lopt:"ipmitemplate" comment:"template used for ipmi command"` Tags map[string]string `yaml:"tags,omitempty"` } diff --git a/internal/pkg/power/ipmitool.go b/internal/pkg/power/ipmitool.go index 73a5b913..dc543769 100644 --- a/internal/pkg/power/ipmitool.go +++ b/internal/pkg/power/ipmitool.go @@ -1,9 +1,18 @@ package power import ( + "bytes" + "fmt" "os" "os/exec" + "path" + "regexp" "strings" + "text/template" + + warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" + "github.com/warewulf/warewulf/internal/pkg/node" + "github.com/warewulf/warewulf/internal/pkg/wwlog" ) type IPMIResult struct { @@ -12,117 +21,118 @@ type IPMIResult struct { } type IPMI struct { - NodeName string - HostName string - Port string - User string - Password string - AuthType string - Interface string - EscapeChar string - result IPMIResult + node.IpmiConf + ShowOnly bool + Cmd string + result IPMIResult } func (ipmi *IPMI) Result() (string, error) { return ipmi.result.out, ipmi.result.err } -func (ipmi *IPMI) Command(ipmiArgs []string) ([]byte, error) { +func (ipmi *IPMI) getStr() (cmdStr string, err error) { + if ipmi.Template == "" { + return "", fmt.Errorf("no ipmi/bmc template specified") + } + if !strings.HasPrefix(ipmi.Template, "/") { + conf := warewulfconf.Get() + ipmi.Template = path.Join(conf.Warewulf.DataStore, "warewulf/bmc", ipmi.Template) + } + fbuf, err := os.ReadFile(ipmi.Template) + if err != nil { + return "", fmt.Errorf("couldn't find the template which defines the ipmi/bmc command: %s", err) + } + cmdTmpl, err := template.New("bmc command").Parse(string(fbuf)) + if err != nil { + return "", err + } + var tbuffer bytes.Buffer + err = cmdTmpl.Execute(&tbuffer, *ipmi) + if err != nil { + return "", err + } + rg := regexp.MustCompile(`(\r\n?|\n){2,}`) + cmdStr = rg.ReplaceAllString(tbuffer.String(), " ") + wwlog.Debug("bmc string is: %s", strings.TrimSpace(cmdStr)) + return strings.TrimSpace(cmdStr), nil - var args []string +} - if ipmi.Interface == "" { - ipmi.Interface = "lan" +func (ipmi *IPMI) Command() ([]byte, error) { + cmdStr, err := ipmi.getStr() + if err != nil { + return []byte{}, err } - if ipmi.Port == "" { - ipmi.Port = "623" + if ipmi.ShowOnly { + return []byte(cmdStr), nil } - if ipmi.EscapeChar == "" { - ipmi.EscapeChar = "~" - } - args = append(args, "-I", ipmi.Interface, "-H", ipmi.HostName, "-p", ipmi.Port, "-U", ipmi.User, "-P", ipmi.Password, "-e", ipmi.EscapeChar) - args = append(args, ipmiArgs...) - ipmiCmd := exec.Command("ipmitool", args...) + ipmiCmd := exec.Command(cmdStr) return ipmiCmd.CombinedOutput() } -func (ipmi *IPMI) InteractiveCommand(ipmiArgs []string) error { - - var args []string - - if ipmi.Interface == "" { - ipmi.Interface = "lan" +func (ipmi *IPMI) InteractiveCommand() (err error) { + cmdStr, err := ipmi.getStr() + if err != nil { + return err } - if ipmi.Port == "" { - ipmi.Port = "623" - } - if ipmi.EscapeChar == "" { - ipmi.EscapeChar = "~" - } - args = append(args, "-I", ipmi.Interface, "-H", ipmi.HostName, "-p", ipmi.Port, "-U", ipmi.User, "-P", ipmi.Password, "-e", ipmi.EscapeChar) - args = append(args, ipmiArgs...) - ipmiCmd := exec.Command("ipmitool", args...) + ipmiCmd := exec.Command(cmdStr) ipmiCmd.Stdout = os.Stdout ipmiCmd.Stdin = os.Stdin ipmiCmd.Stderr = os.Stderr return ipmiCmd.Run() } -func (ipmi *IPMI) IPMIInteractiveCommand(args ...string) error { - return ipmi.InteractiveCommand(args) +func (ipmi *IPMI) IPMIInteractiveCommand(cmd string) error { + ipmi.Cmd = cmd + return ipmi.InteractiveCommand() } -func (ipmi *IPMI) IPMICommand(args ...string) (string, error) { - ipmiOut, err := ipmi.Command(args) +func (ipmi *IPMI) IPMICommand(cmd string) (string, error) { + ipmi.Cmd = cmd + ipmiOut, err := ipmi.Command() ipmi.result.out = strings.TrimSpace(string(ipmiOut)) ipmi.result.err = err return ipmi.result.out, ipmi.result.err } -//func (ipmi IPMI) PowerOn() (string, error) { -// -// var args []string -// -// args = append(args, "chassis", "power", "on") -// ipmiOut, err := ipmi.Command(args) -// ipmi.result.out = string(ipmiOut) -// ipmi.result.err = err -// return ipmi.result.out, ipmi.result.err -//} +/* +Just define meta commands here, implementation is in the template +*/ func (ipmi *IPMI) PowerOn() (string, error) { - return ipmi.IPMICommand("chassis", "power", "on") + return ipmi.IPMICommand("PowerOn") } func (ipmi *IPMI) PowerOff() (string, error) { - return ipmi.IPMICommand("chassis", "power", "off") + return ipmi.IPMICommand("PowerOff") } func (ipmi *IPMI) PowerCycle() (string, error) { - return ipmi.IPMICommand("chassis", "power", "cycle") + return ipmi.IPMICommand("PowerCycle") } func (ipmi *IPMI) PowerReset() (string, error) { - return ipmi.IPMICommand("chassis", "power", "reset") + return ipmi.IPMICommand("PowerReset") } func (ipmi *IPMI) PowerSoft() (string, error) { - return ipmi.IPMICommand("chassis", "power", "soft") + return ipmi.IPMICommand("PowerSoft") } func (ipmi *IPMI) PowerStatus() (string, error) { - return ipmi.IPMICommand("chassis", "power", "status") + return ipmi.IPMICommand("PowerStatus") } func (ipmi *IPMI) SDRList() (string, error) { - return ipmi.IPMICommand("sdr", "list") + return ipmi.IPMICommand("SDRList") } func (ipmi *IPMI) SensorList() (string, error) { - return ipmi.IPMICommand("sensor", "list") + return ipmi.IPMICommand("SensorList") } func (ipmi *IPMI) Console() error { - return ipmi.IPMIInteractiveCommand("sol", "activate") + return ipmi.IPMIInteractiveCommand("Console") } diff --git a/internal/pkg/power/power.go b/internal/pkg/power/power.go deleted file mode 100644 index 2faa3e67..00000000 --- a/internal/pkg/power/power.go +++ /dev/null @@ -1,31 +0,0 @@ -package power - -//type PowerControl interface { -//PowerOn() (result string, err error) -//PowerOff() (result string, err error) -//PowerStatus() (result string, err error) -//} - -type PowerOnInterface interface { - PowerOn() (result string, err error) -} - -type PowerOffInterface interface { - PowerOff() (result string, err error) -} - -type PowerResetInterface interface { - PowerReset() (result string, err error) -} - -type PowerSoftInterface interface { - PowerSoft() (result string, err error) -} - -type PowerCycleInterface interface { - PowerCycle() (result string, err error) -} - -type PowerStatusInterface interface { - PowerStatus() (result string, err error) -} diff --git a/lib/warewulf/bmc/ipmitool.tmpl b/lib/warewulf/bmc/ipmitool.tmpl new file mode 100644 index 00000000..a1e8c604 --- /dev/null +++ b/lib/warewulf/bmc/ipmitool.tmpl @@ -0,0 +1,15 @@ +{{/* used command to access the ipmi interface of the nodes */}} +{{- $escapechar := "~" }}{{ if .EscapeChar }}{{ $escapechar = .EscapeChar }}{{ end }} +{{- $port := "623" }}{{ if .Port }}{{ $port = .Port }}{{ end }} +{{- $interface := "lan" }}{{- if .Interface }}{{ $interface = .Interface }}{{ end }} +{{- $args := "" }} +{{- if eq .Cmd "PowerOn" }}{{ $args = "chassis power on" }} +{{- else if eq .Cmd "PowerOff" }}{{ $args = "chassis power off" }} +{{- else if eq .Cmd "PowerCycle" }}{{ $args = "chassis power cycle" }} +{{- else if eq .Cmd "PowerReset" }}{{ $args = "chassis power reset" }} +{{- else if eq .Cmd "PowerSoft" }}{{ $args = "chassis power soft" }} +{{- else if eq .Cmd "PowerStatus" }}{{ $args = "chassis power status" }} +{{- else if eq .Cmd "SDRList" }}{{ $args = "sdr list" }} +{{- else if eq .Cmd "SensorList" }}{{ $args = "sensor list" }} +{{- else if eq .Cmd "Console" }}{{ $args = "sol activate" }}{{ end }} +ipmitool -I {{ $interface }} -H {{ .Ipaddr }} -p {{ $port }} -U {{ .UserName }} -P {{ .Password }} -e {{ $escapechar }} {{ $args }} diff --git a/lib/warewulf/bmc/nobmc.tmpl b/lib/warewulf/bmc/nobmc.tmpl new file mode 100644 index 00000000..ab6ab127 --- /dev/null +++ b/lib/warewulf/bmc/nobmc.tmpl @@ -0,0 +1,11 @@ +{{/* used command to access nodes without bmc*/}} +{{- if eq .Cmd "PowerOn" }}wol {{ .Interface }} +{{- else if eq .Cmd "PowerOff" }}ssh {{ .Ipaddr }} echo o > /proc/sysrq-trigger +{{- else if eq .Cmd "PowerCycle" }}ssh {{ .Ipaddr }} echo r > /proc/sysrq-trigger +{{- else if eq .Cmd "PowerReset" }}ssh {{ .Ipaddr }} echo r > /proc/sysrq-trigger +{{- else if eq .Cmd "PowerSoft" }}ssh {{ .Ipaddr }} reboot +{{- else if eq .Cmd "PowerStatus" }}ping -c 1 {{ .Ipaddr }} &> /dev/null && echo ON || echo OFF +{{- else if eq .Cmd "SDRList" }}ssh {{ .Ipaddr }} sensors +{{- else if eq .Cmd "SensorList" }}ssh {{ .Ipaddr }} sensors +{{- else if eq .Cmd "Console" }}echo node sol +{{- else }}echo "command not found"{{ end }} diff --git a/userdocs/contents/ipmi.rst b/userdocs/contents/ipmi.rst index 627b1f09..6a4036cd 100644 --- a/userdocs/contents/ipmi.rst +++ b/userdocs/contents/ipmi.rst @@ -4,7 +4,9 @@ IPMI It is possible to control the power or connect a console to your nodes being managed by Warewulf by connecting to the BMC through the use of -IPMI. We will discuss how to set this up below. +`ipmitool`. Other methods can also configured, but require additional +configuration. +We will discuss how to set this up below. IPMI Settings ============= @@ -15,8 +17,8 @@ individual node would be the IP address. The settings are only written to the IPMI interface if ``--ipmiwrite`` is set to `true`. The write process happens at every boot of the node -through the script ``/warewulf/init.d/50-ipmi`` in the wwinit -overlay. +through the script ``/warewulf/init.d/50-ipmi`` in the **system** +overlay and are done with `ipmitool`. If an individual node has different settings, you can set the IPMI settings for that specific node, overriding the default settings. @@ -25,27 +27,29 @@ Here is a table outlining the fields on a Profile and Node which is the same as the parameter that can be used when running ``wwctl profile set`` or ``wwctl node set``. -+----------------------+---------+------+--------------------+---------------+ -| Parameter | Profile | Node | Valid Values | Default Value | -+======================+=========+======+====================+===============+ -| ``--ipmiaddr`` | false | true | | | -+----------------------+---------+------+--------------------+---------------+ -| ``--ipminetmask`` | true | true | | | -+----------------------+---------+------+--------------------+---------------+ -| ``--ipmiport`` | true | true | | 623 | -+----------------------+---------+------+--------------------+---------------+ -| ``--ipmigateway`` | true | true | | | -+----------------------+---------+------+--------------------+---------------+ -| ``--ipmiuser`` | true | true | | | -+----------------------+---------+------+--------------------+---------------+ -| ``--ipmipass`` | true | true | | | -+----------------------+---------+------+--------------------+---------------+ -| ``--ipmiinterface`` | true | true | 'lan' or 'lanplus' | lan | -+----------------------+---------+------+--------------------+---------------+ -| ``--ipmiwrite`` | true | true | true or false | false | -+----------------------+---------+------+--------------------+---------------+ -| ``--ipmiescapechar`` | true | true | single character | ~ | -+----------------------+---------+------+--------------------+---------------+ ++---------------------+---------+------+--------------------+---------------+ +| Parameter | Profile | Node | Valid Values | Default Value | ++=====================+=========+======+====================+===============+ +| ``--ipmiaddr`` | false | true | | | ++---------------------+---------+------+--------------------+---------------+ +| ``--ipminetmask`` | true | true | | | ++---------------------+---------+------+--------------------+---------------+ +| ``--ipmiport`` | true | true | | 623 | ++---------------------+---------+------+--------------------+---------------+ +| ``--ipmigateway`` | true | true | | | ++---------------------+---------+------+--------------------+---------------+ +| ``--ipmiuser`` | true | true | | | ++---------------------+---------+------+--------------------+---------------+ +| ``--ipmipass`` | true | true | | | ++---------------------+---------+------+--------------------+---------------+ +| ``--ipmiinterface`` | true | true | 'lan' or 'lanplus' | lan | ++---------------------+---------+------+--------------------+---------------+ +| ``--ipmiwrite`` | true | true | true or false | false | ++---------------------+---------+------+--------------------+---------------+ +| ``--ipmiescapechar``| true | true | single character | ~ | ++---------------------+---------+------+--------------------+---------------+ +| ``--ipmitemplate`` | true | true | path to template | | ++---------------------+---------+------+--------------------+---------------+ Reviewing Settings @@ -181,3 +185,76 @@ connect a console to the node. .. code-block:: console # wwctl node console n001 + + +Ipmi template +============= + +As warewulf doesn't manage the ipmi/bmc interfaces directly, but calls ``ipmitool`` +this managed with a template which defines the behavior. For ``ipmitool`` following +template is used + +.. code-block:: golang + {{/* used command to access the ipmi interface of the nodes */}} + {{- $escapechar := "~" }} + {{- $port := "623" }} + {{- $interface := "lan" }} + {{- $args := "" }} + {{- if .EscapeChar }} $escapechar = .EscapeChar {{ end }} + {{- if .Port }} {{ $port = .Port }} {{ end }} + {{- if .Interface }} {{ $interface = .Interface }} {{ end }} + {{- if eq .Cmd "PowerOn" }} {{ $args = "chassis power on" }} {{ end }} + {{- if eq .Cmd "PowerOff" }} {{ $args = "chassis power off" }} {{ end }} + {{- if eq .Cmd "PowerCycle" }} {{ $args = "chassis power cycle" }} {{ end }} + {{- if eq .Cmd "PowerReset" }} {{ $args = "chassis power reset" }} {{ end }} + {{- if eq .Cmd "PowerSoft" }} {{ $args = "chassis power soft" }} {{ end }} + {{- if eq .Cmd "PowerStatus" }} {{ $args = "chassis power status" }} {{ end }} + {{- if eq .Cmd "SDRList" }} {{ $args = "sdr list" }} {{ end }} + {{- if eq .Cmd "SensorList" }} {{ $args = "sensor list" }} {{ end }} + {{- if eq .Cmd "Console" }} {{ $args = "sol activate" }} {{ end }} + {{- $cmd := printf "ipmitool -I %s -H %s -p %s -U %s -P %s -e %s %s" $interface .Ipaddr $port .UserName .Password $escapechar $args }} + {{ $cmd }} + +In order to use another template, its filename must be specified for a node or profile via the +``--ipmitemplate`` switch and the template must placed under ``/usr/lib/warewulf/bmc`` or to the +path which is was defined as ``datadir`` in ``warwulf.conf`` or during compile time. +All IPMI specific variables are accessible in the template which are the following + +| Parameter | Template variable | ++=====================+====================+ +| ``--ipmiaddr`` | ``.Ipaddr`` | ++---------------------+--------------------+ +| ``--ipminetmask`` | ``.Netmask`` | ++---------------------+--------------------+ +| ``--ipmiport`` | ``.Port`` | ++---------------------+--------------------+ +| ``--ipmigateway`` | ``.Gateway`` | ++---------------------+--------------------+ +| ``--ipmiuser`` | ``.UserName`` | ++---------------------+--------------------+ +| ``--ipmipass`` | ``.Password`` | ++---------------------+--------------------+ +| ``--ipmiinterface`` | ``.Interface`` | ++---------------------+--------------------+ +| ``--ipmiwrite`` | ``.Write`` | ++---------------------+--------------------+ +| ``--ipmiescapechar``| ``.EscapeChar`` | ++---------------------+--------------------+ +| ``--ipmitemplate`` | ``.Template`` | ++---------------------+--------------------+ + +Additional the ``.Args`` variable is accessible which can have following +values: +* `PowerOn` +* `PowerOff` +* `PowerCycle` +* `PowerReset` +* `PowerSoft` +* `PowerStatus` +* `SDRList` +* `SensorList` +* `Console` +which are the calls done by `wwctl power` commands. + +Also the script ``/warewulf/init.d/50-ipmi`` in the **system** +overlay may need an update. There the variables must have the prefix ``.Ipmi``