// 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()) }