Handle missing ipmi fields during bmc commands

Signed-off-by: Jonathon Anderson <janderson@ciq.com>
This commit is contained in:
Christian Goll
2025-02-25 18:58:07 +01:00
committed by Jonathon Anderson
parent 099e7cd4ea
commit ea9a013812
22 changed files with 293 additions and 212 deletions

136
internal/pkg/bmc/bmc.go Normal file
View File

@@ -0,0 +1,136 @@
package bmc
import (
"bytes"
"fmt"
"os"
"os/exec"
"path"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
warewulfconf "github.com/warewulf/warewulf/internal/pkg/config"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/wwlog"
)
type Result struct {
err error
out string
}
type TemplateStruct struct {
node.IpmiConf
ShowOnly bool
Cmd string
result Result
}
func (tstruct *TemplateStruct) Result() (string, error) {
return tstruct.result.out, tstruct.result.err
}
func (tstruct *TemplateStruct) getCommand() (cmdStr string, err error) {
if tstruct.Template == "" {
return "", fmt.Errorf("no bmc template specified")
}
if !strings.HasPrefix(tstruct.Template, "/") {
conf := warewulfconf.Get()
tstruct.Template = path.Join(conf.Paths.Datadir, "warewulf/bmc", tstruct.Template)
}
fbuf, err := os.ReadFile(tstruct.Template)
if err != nil {
return "", fmt.Errorf("couldn't find the template which defines the bmc command: %s", err)
}
cmdTmpl, err := template.New("bmc command").Funcs(sprig.TxtFuncMap()).Parse(string(fbuf))
if err != nil {
return "", err
}
var tbuffer bytes.Buffer
err = cmdTmpl.Execute(&tbuffer, *tstruct)
if err != nil {
return "", err
}
cmdStr = strings.TrimSpace(tbuffer.String())
wwlog.Debug("bmc command: %s", cmdStr)
return cmdStr, nil
}
func (tstruct *TemplateStruct) runCommand() ([]byte, error) {
cmdStr, err := tstruct.getCommand()
if err != nil {
return []byte{}, err
}
if tstruct.ShowOnly {
return []byte(cmdStr), nil
}
return exec.Command("/bin/sh", "-c", cmdStr).CombinedOutput()
}
func (tstruct *TemplateStruct) runInteractiveCommand() (err error) {
cmdStr, err := tstruct.getCommand()
if err != nil {
return err
}
cmd := exec.Command("/bin/sh", "-c", cmdStr)
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
return cmd.Run()
}
func (tstruct *TemplateStruct) InteractiveCommand(cmd string) error {
tstruct.Cmd = cmd
return tstruct.runInteractiveCommand()
}
func (tstruct *TemplateStruct) Command(cmd string) (string, error) {
tstruct.Cmd = cmd
out, err := tstruct.runCommand()
tstruct.result.out = strings.TrimSpace(string(out))
tstruct.result.err = err
return tstruct.result.out, tstruct.result.err
}
/*
Just define meta commands here, implementation is in the template
*/
func (tstruct *TemplateStruct) PowerOn() (string, error) {
return tstruct.Command("PowerOn")
}
func (tstruct *TemplateStruct) PowerOff() (string, error) {
return tstruct.Command("PowerOff")
}
func (tstruct *TemplateStruct) PowerCycle() (string, error) {
return tstruct.Command("PowerCycle")
}
func (tstruct *TemplateStruct) PowerReset() (string, error) {
return tstruct.Command("PowerReset")
}
func (tstruct *TemplateStruct) PowerSoft() (string, error) {
return tstruct.Command("PowerSoft")
}
func (tstruct *TemplateStruct) PowerStatus() (string, error) {
return tstruct.Command("PowerStatus")
}
func (tstruct *TemplateStruct) SDRList() (string, error) {
return tstruct.Command("SDRList")
}
func (tstruct *TemplateStruct) SensorList() (string, error) {
return tstruct.Command("SensorList")
}
func (tstruct *TemplateStruct) Console() error {
return tstruct.InteractiveCommand("Console")
}

View File

@@ -0,0 +1,81 @@
package bmc
import (
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/warewulf/warewulf/internal/pkg/node"
"github.com/warewulf/warewulf/internal/pkg/testenv"
)
func Test_Ipmitool(t *testing.T) {
tests := map[string]struct {
bmc TemplateStruct
err bool
cmdStr string
}{
"no template": {
bmc: TemplateStruct{},
err: true,
},
"ipmitool PowerStatus empty": {
bmc: TemplateStruct{
Cmd: "PowerStatus",
IpmiConf: node.IpmiConf{
Template: "ipmitool.tmpl",
},
},
cmdStr: `ipmitool chassis power status`,
},
"ipmitool PowerStatus full": {
bmc: TemplateStruct{
Cmd: "PowerStatus",
IpmiConf: node.IpmiConf{
Template: "ipmitool.tmpl",
Interface: "lanplus",
EscapeChar: "~",
Port: "687",
Ipaddr: net.IP{192, 168, 1, 100},
UserName: "root",
Password: "calvin",
},
},
cmdStr: `ipmitool -I lanplus -e ~ -p 687 -H 192.168.1.100 -U "root" -P "calvin" chassis power status`,
},
"nobmc PowerStatus full": {
bmc: TemplateStruct{
Cmd: "PowerStatus",
IpmiConf: node.IpmiConf{
Template: "nobmc.tmpl",
Interface: "lanplus",
EscapeChar: "~",
Port: "687",
Ipaddr: net.IP{192, 168, 1, 100},
UserName: "root",
Password: "calvin",
},
},
cmdStr: `ping -c 1 "192.168.1.100" &> /dev/null && echo ON || echo OFF`,
},
}
for name, test := range tests {
env := testenv.New(t)
defer env.RemoveAll()
env.ImportFile("usr/share/warewulf/bmc/ipmitool.tmpl", "../../../lib/warewulf/bmc/ipmitool.tmpl")
env.ImportFile("usr/share/warewulf/bmc/nobmc.tmpl", "../../../lib/warewulf/bmc/nobmc.tmpl")
t.Run(name, func(t *testing.T) {
cmdStr, err := test.bmc.getCommand()
if test.err {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if test.cmdStr != "" {
assert.Equal(t, test.cmdStr, cmdStr)
}
})
}
}

View File

@@ -1,138 +0,0 @@
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 {
err error
out string
}
type IPMI struct {
node.IpmiConf
ShowOnly bool
Cmd string
result IPMIResult
}
func (ipmi *IPMI) Result() (string, error) {
return ipmi.result.out, ipmi.result.err
}
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.Paths.Datadir, "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
}
func (ipmi *IPMI) Command() ([]byte, error) {
cmdStr, err := ipmi.getStr()
if err != nil {
return []byte{}, err
}
if ipmi.ShowOnly {
return []byte(cmdStr), nil
}
ipmiCmd := exec.Command("/bin/sh", "-c", cmdStr)
return ipmiCmd.CombinedOutput()
}
func (ipmi *IPMI) InteractiveCommand() (err error) {
cmdStr, err := ipmi.getStr()
if err != nil {
return err
}
ipmiCmd := exec.Command("/bin/sh", "-c", cmdStr)
ipmiCmd.Stdout = os.Stdout
ipmiCmd.Stdin = os.Stdin
ipmiCmd.Stderr = os.Stderr
return ipmiCmd.Run()
}
func (ipmi *IPMI) IPMIInteractiveCommand(cmd string) error {
ipmi.Cmd = cmd
return ipmi.InteractiveCommand()
}
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
}
/*
Just define meta commands here, implementation is in the template
*/
func (ipmi *IPMI) PowerOn() (string, error) {
return ipmi.IPMICommand("PowerOn")
}
func (ipmi *IPMI) PowerOff() (string, error) {
return ipmi.IPMICommand("PowerOff")
}
func (ipmi *IPMI) PowerCycle() (string, error) {
return ipmi.IPMICommand("PowerCycle")
}
func (ipmi *IPMI) PowerReset() (string, error) {
return ipmi.IPMICommand("PowerReset")
}
func (ipmi *IPMI) PowerSoft() (string, error) {
return ipmi.IPMICommand("PowerSoft")
}
func (ipmi *IPMI) PowerStatus() (string, error) {
return ipmi.IPMICommand("PowerStatus")
}
func (ipmi *IPMI) SDRList() (string, error) {
return ipmi.IPMICommand("SDRList")
}
func (ipmi *IPMI) SensorList() (string, error) {
return ipmi.IPMICommand("SensorList")
}
func (ipmi *IPMI) Console() error {
return ipmi.IPMIInteractiveCommand("Console")
}