This resource manages bmc devices in servers or elsewhere. This also integrates with the provisioner code.
298 lines
7.5 KiB
Go
298 lines
7.5 KiB
Go
// This is an example mock BMC server/device.
|
|
// Many thanks to Joel Rebello for figuring out the specific endpoints needed.
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/purpleidea/mgmt/util"
|
|
|
|
"github.com/alexflint/go-arg"
|
|
"github.com/bmc-toolbox/bmclib/v2/providers/rpc"
|
|
)
|
|
|
|
const (
|
|
// DefaultPort to listen on. Seen in the bmclib docs.
|
|
DefaultPort = 8800
|
|
|
|
// StateOn is the power "on" state.
|
|
StateOn = "on"
|
|
|
|
// StateOff is the power "off" state.
|
|
StateOff = "off"
|
|
)
|
|
|
|
// MockBMC is a simple mocked BMC device.
|
|
type MockBMC struct {
|
|
// Addr to listen on. Eg: :8800 for example.
|
|
Addr string
|
|
|
|
// State of the device power. This gets read and changed by the API.
|
|
State string
|
|
|
|
// Driver specifies which driver we want to mock.
|
|
// TODO: Do I mean "driver" or "provider" ?
|
|
Driver string
|
|
}
|
|
|
|
// Data is what we use to template the outputs.
|
|
type Data struct {
|
|
PowerState string
|
|
}
|
|
|
|
// Run kicks this all off.
|
|
func (obj *MockBMC) Run() error {
|
|
|
|
tls := util.NewTLS()
|
|
tls.Host = "localhost" // TODO: choose something
|
|
keyPemFile := "/tmp/key.pem"
|
|
certPemFile := "/tmp/cert.pem"
|
|
|
|
if err := tls.Generate(keyPemFile, certPemFile); err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("running at: %s\n", obj.Addr)
|
|
fmt.Printf("driver is: %s\n", obj.Driver)
|
|
fmt.Printf("device is: %s\n", obj.State) // we start off in this state
|
|
if obj.Driver == "rpc" {
|
|
http.HandleFunc("/", obj.rpcHandler)
|
|
}
|
|
if obj.Driver == "redfish" || obj.Driver == "gofish" {
|
|
http.HandleFunc("/redfish/v1/", obj.endpointFunc("service_root.json", http.MethodGet, 200, nil))
|
|
|
|
// login
|
|
sessionHeader := map[string]string{
|
|
"X-Auth-Token": "t5tpiajo89fyvvel5434h9p2l3j69hzx", // TODO: how do we get this?
|
|
"Location": "/redfish/v1/SessionService/Sessions/1",
|
|
}
|
|
|
|
http.HandleFunc("/redfish/v1/SessionService/Sessions", obj.endpointFunc("session_service.json", http.MethodPost, 201, sessionHeader))
|
|
|
|
// get power state
|
|
http.HandleFunc("/redfish/v1/Systems", obj.endpointFunc("systems.json", http.MethodGet, 200, nil))
|
|
http.HandleFunc("/redfish/v1/Systems/1", obj.endpointFunc("systems_1.json.tmpl", http.MethodGet, 200, nil))
|
|
|
|
// set pxe - we can't have two routes with the same pattern
|
|
//http.HandleFunc("/redfish/v1/Systems/1", obj.endpointFunc("", http.MethodPatch, 200, nil))
|
|
|
|
// power on/off XXX: seems to post here to turn on
|
|
http.HandleFunc("/redfish/v1/Systems/1/Actions/ComputerSystem.Reset", obj.endpointFunc("", http.MethodPost, 200, nil))
|
|
|
|
// logoff
|
|
http.HandleFunc("/redfish/v1/SessionService/Sessions/1", obj.endpointFunc("session_delete.json", http.MethodDelete, 200, nil))
|
|
}
|
|
|
|
http.HandleFunc("/hello", obj.hello)
|
|
//return http.ListenAndServe(obj.Addr, nil)
|
|
return http.ListenAndServeTLS(obj.Addr, certPemFile, keyPemFile, nil)
|
|
}
|
|
|
|
func (obj *MockBMC) template(templateText string, data interface{}) (string, error) {
|
|
var err error
|
|
tmpl := template.New("name") // whatever name you want
|
|
//tmpl = tmpl.Funcs(funcMap)
|
|
tmpl, err = tmpl.Parse(templateText)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
// run the template
|
|
if err := tmpl.Execute(buf, data); err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
// endpointFunc handles the bmc mock requirements.
|
|
func (obj *MockBMC) endpointFunc(file, method string, retStatus int, retHeader map[string]string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
data := &Data{
|
|
PowerState: obj.State,
|
|
}
|
|
|
|
fmt.Printf("URL: %s\n", r.URL.Path)
|
|
fmt.Printf("[%s] file: %s\n", method, file)
|
|
//if method == "POST" {
|
|
//for name, values := range r.Header {
|
|
// fmt.Printf("\t[%s] header: %+v\n", name, values)
|
|
//}
|
|
|
|
if err := r.ParseForm(); err != nil {
|
|
fmt.Printf("error parsing form: %+v\n", err)
|
|
}
|
|
for name, values := range r.PostForm {
|
|
fmt.Printf("\t[%s] values: %+v\n", name, values)
|
|
}
|
|
//}
|
|
|
|
// purge check on patch method if set pxe request is attempted
|
|
if r.Method != method && r.Method != http.MethodPatch {
|
|
resp := fmt.Sprintf("unexpected request - url: %s, method: %s", r.URL, r.Method)
|
|
_, _ = w.Write([]byte(resp))
|
|
}
|
|
|
|
for k, v := range retHeader {
|
|
w.Header().Add(k, v)
|
|
}
|
|
|
|
w.WriteHeader(retStatus)
|
|
if file != "" {
|
|
out1 := mustReadFile(file)
|
|
if !strings.HasSuffix(file, ".tmpl") {
|
|
_, _ = w.Write(out1)
|
|
return
|
|
}
|
|
|
|
out2, err := obj.template(string(out1), data)
|
|
if err != nil {
|
|
resp := fmt.Sprintf("unexpected request - url: %s, method: %s", r.URL, r.Method)
|
|
_, _ = w.Write([]byte(resp))
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte(out2))
|
|
|
|
}
|
|
|
|
return
|
|
}
|
|
}
|
|
|
|
// rpcHandler is used for the rpc driver.
|
|
func (obj *MockBMC) rpcHandler(w http.ResponseWriter, r *http.Request) {
|
|
//fmt.Printf("req1: %+v\n", r)
|
|
//fmt.Printf("method: %s\n", r.Method)
|
|
//fmt.Printf("URL: %s\n", r.URL)
|
|
//fmt.Printf("Body: %v\n", r.Body)
|
|
|
|
req := rpc.RequestPayload{}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
//fmt.Printf("data: %+v\n", req)
|
|
|
|
rp := rpc.ResponsePayload{
|
|
ID: req.ID,
|
|
Host: req.Host,
|
|
}
|
|
switch req.Method {
|
|
case rpc.PowerGetMethod:
|
|
rp.Result = obj.State
|
|
fmt.Printf("get state: %s\n", obj.State)
|
|
|
|
case rpc.PowerSetMethod:
|
|
//fmt.Printf("req2: %T %+v\n", req.Params, req.Params)
|
|
// TODO: This is a mess, isn't there a cleaner way to unpack it?
|
|
m, ok := req.Params.(map[string]interface{})
|
|
if ok {
|
|
param, exists := m["state"]
|
|
state, ok := param.(string)
|
|
if ok {
|
|
if exists && (state == StateOn || state == StateOff) {
|
|
obj.State = state
|
|
fmt.Printf("set state: %s\n", state)
|
|
}
|
|
}
|
|
}
|
|
|
|
case rpc.BootDeviceMethod:
|
|
|
|
case rpc.PingMethod:
|
|
fmt.Printf("got ping\n")
|
|
rp.Result = "pong"
|
|
|
|
default:
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
b, _ := json.Marshal(rp)
|
|
//fmt.Printf("out: %s\n", b)
|
|
w.Write(b)
|
|
}
|
|
|
|
func (obj *MockBMC) hello(w http.ResponseWriter, req *http.Request) {
|
|
fmt.Printf("req: %+v\n", req)
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.Write([]byte("This is hello world!\n"))
|
|
//w.Write([]byte("OpenBMC says hello!\n"))
|
|
}
|
|
|
|
func mustReadFile(filename string) []byte {
|
|
fixture := "fixtures" + "/" + filename
|
|
fh, err := os.Open(fixture)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
defer fh.Close()
|
|
|
|
b, err := io.ReadAll(fh)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// The original examples had no trailing newlines. Not sure if allowed.
|
|
return bytes.TrimSuffix(b, []byte("\n"))
|
|
}
|
|
|
|
// Args are what are used to build the CLI.
|
|
type Args struct {
|
|
// XXX: We cannot have both subcommands and a positional argument.
|
|
// XXX: I think it's a bug of this library that it can't handle argv[0].
|
|
//Argv0 string `arg:"positional"`
|
|
|
|
On bool `arg:"--on" help:"start on"`
|
|
|
|
Port int `arg:"--port" help:"port to listen on"`
|
|
|
|
Driver string `arg:"--driver" default:"redfish" help:"driver to use"`
|
|
}
|
|
|
|
// Main program that returns error.
|
|
func Main() error {
|
|
args := Args{
|
|
Port: DefaultPort,
|
|
}
|
|
config := arg.Config{}
|
|
parser, err := arg.NewParser(config, &args)
|
|
if err != nil {
|
|
// programming error
|
|
return err
|
|
}
|
|
err = parser.Parse(os.Args[1:]) // XXX: args[0] needs to be dropped
|
|
if err == arg.ErrHelp {
|
|
parser.WriteHelp(os.Stdout)
|
|
return nil
|
|
}
|
|
|
|
state := StateOff
|
|
if args.On {
|
|
state = StateOn
|
|
}
|
|
|
|
mock := &MockBMC{
|
|
Addr: fmt.Sprintf("localhost:%d", args.Port),
|
|
//State: StateOff, // starts off off
|
|
//State: StateOn,
|
|
State: state,
|
|
Driver: args.Driver,
|
|
}
|
|
return mock.Run()
|
|
}
|
|
|
|
// wget --no-check-certificate --post-data 'user=foo&password=bar' \
|
|
// https://localhost:8800/redfish/v1/Systems/1/Actions/ComputerSystem.Reset -O -
|
|
func main() {
|
|
fmt.Printf("main: %+v\n", Main())
|
|
}
|