engine: resources: Add a bmc resource
This resource manages bmc devices in servers or elsewhere. This also integrates with the provisioner code.
This commit is contained in:
45
examples/mockbmc/fixtures/service_root.json
Normal file
45
examples/mockbmc/fixtures/service_root.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"@odata.context": "/redfish/v1/$metadata#ServiceRoot.ServiceRoot",
|
||||
"@odata.type": "#ServiceRoot.v1_1_0.ServiceRoot",
|
||||
"@odata.id": "/redfish/v1/",
|
||||
"Id": "RootService",
|
||||
"Name": "Root Service",
|
||||
"RedfishVersion": "1.0.1",
|
||||
"UUID": "00000000-0000-0000-0000-0CC47A8BDADA",
|
||||
"Systems": {
|
||||
"@odata.id": "/redfish/v1/Systems"
|
||||
},
|
||||
"Chassis": {
|
||||
"@odata.id": "/redfish/v1/Chassis"
|
||||
},
|
||||
"Managers": {
|
||||
"@odata.id": "/redfish/v1/Managers"
|
||||
},
|
||||
"Tasks": {
|
||||
"@odata.id": "/redfish/v1/TaskService"
|
||||
},
|
||||
"SessionService": {
|
||||
"@odata.id": "/redfish/v1/SessionService"
|
||||
},
|
||||
"AccountService": {
|
||||
"@odata.id": "/redfish/v1/AccountService"
|
||||
},
|
||||
"EventService": {
|
||||
"@odata.id": "/redfish/v1/EventService"
|
||||
},
|
||||
"UpdateService": {
|
||||
"@odata.id": "/redfish/v1/UpdateService"
|
||||
},
|
||||
"Registries": {
|
||||
"@odata.id": "/redfish/v1/Registries"
|
||||
},
|
||||
"JsonSchemas": {
|
||||
"@odata.id": "/redfish/v1/JsonSchemas"
|
||||
},
|
||||
"Links": {
|
||||
"Sessions": {
|
||||
"@odata.id": "/redfish/v1/SessionService/Sessions"
|
||||
}
|
||||
},
|
||||
"Oem": {}
|
||||
}
|
||||
6
examples/mockbmc/fixtures/session_delete.json
Normal file
6
examples/mockbmc/fixtures/session_delete.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"Success": {
|
||||
"code": "Base.v1_4_0.Success",
|
||||
"Message": "Successfully Completed Request."
|
||||
}
|
||||
}
|
||||
10
examples/mockbmc/fixtures/session_service.json
Normal file
10
examples/mockbmc/fixtures/session_service.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"@odata.context": "/redfish/v1/$metadata#Session.Session",
|
||||
"@odata.type": "#Session.v1_0_0.Session",
|
||||
"@odata.id": "/redfish/v1/SessionService/Sessions/1",
|
||||
"Id": "1",
|
||||
"Name": "User Session",
|
||||
"Description": "Manager User Session",
|
||||
"UserName": "ADMIN",
|
||||
"Oem": {}
|
||||
}
|
||||
13
examples/mockbmc/fixtures/systems.json
Normal file
13
examples/mockbmc/fixtures/systems.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"@odata.context": "/redfish/v1/$metadata#ComputerSystemCollection.ComputerSystemCollection",
|
||||
"@odata.type": "#ComputerSystemCollection.ComputerSystemCollection",
|
||||
"@odata.id": "/redfish/v1/Systems",
|
||||
"Name": "Computer System Collection",
|
||||
"Description": "Computer System Collection",
|
||||
"Members@odata.count": 1,
|
||||
"Members": [
|
||||
{
|
||||
"@odata.id": "/redfish/v1/Systems/1"
|
||||
}
|
||||
]
|
||||
}
|
||||
107
examples/mockbmc/fixtures/systems_1.json.tmpl
Normal file
107
examples/mockbmc/fixtures/systems_1.json.tmpl
Normal file
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"@odata.context": "/redfish/v1/$metadata#ComputerSystem.ComputerSystem",
|
||||
"@odata.type": "#ComputerSystem.v1_3_0.ComputerSystem",
|
||||
"@odata.id": "/redfish/v1/Systems/1",
|
||||
"Id": "1",
|
||||
"Name": "System",
|
||||
"Description": "Description of server",
|
||||
"Status": {
|
||||
"State": "Enabled",
|
||||
"Health": "Critical"
|
||||
},
|
||||
"SerialNumber": " ",
|
||||
"PartNumber": "",
|
||||
"SystemType": "Physical",
|
||||
"BiosVersion": "3.3",
|
||||
"Manufacturer": "Supermicro",
|
||||
"Model": "Super Server",
|
||||
"SKU": "To be filled by O.E.M.",
|
||||
"UUID": "00000000-0000-0000-0000-0CC47A847624",
|
||||
"ProcessorSummary": {
|
||||
"Count": 1,
|
||||
"Model": "Intel(R) Xeon(R) processor",
|
||||
"Status": {
|
||||
"State": "Enabled",
|
||||
"Health": "OK"
|
||||
}
|
||||
},
|
||||
"MemorySummary": {
|
||||
"TotalSystemMemoryGiB": 16,
|
||||
"Status": {
|
||||
"State": "Enabled",
|
||||
"Health": "OK"
|
||||
}
|
||||
},
|
||||
"IndicatorLED": "Off",
|
||||
"PowerState": "{{ .PowerState }}",
|
||||
"Boot": {
|
||||
"BootSourceOverrideEnabled": "Disabled",
|
||||
"BootSourceOverrideTarget": "None",
|
||||
"BootSourceOverrideTarget@Redfish.AllowableValues": [
|
||||
"None",
|
||||
"Pxe",
|
||||
"Hdd",
|
||||
"Diags",
|
||||
"CD/DVD",
|
||||
"BiosSetup",
|
||||
"FloppyRemovableMedia",
|
||||
"UsbKey",
|
||||
"UsbHdd",
|
||||
"UsbFloppy",
|
||||
"UsbCd",
|
||||
"UefiUsbKey",
|
||||
"UefiCd",
|
||||
"UefiHdd",
|
||||
"UefiUsbHdd",
|
||||
"UefiUsbCd"
|
||||
]
|
||||
},
|
||||
"Processors": {
|
||||
"@odata.id": "/redfish/v1/Systems/1/Processors"
|
||||
},
|
||||
"Memory": {
|
||||
"@odata.id": "/redfish/v1/Systems/1/Memory"
|
||||
},
|
||||
"EthernetInterfaces": {
|
||||
"@odata.id": "/redfish/v1/Systems/1/EthernetInterfaces"
|
||||
},
|
||||
"SimpleStorage": {
|
||||
"@odata.id": "/redfish/v1/Systems/1/SimpleStorage"
|
||||
},
|
||||
"Storage": {
|
||||
"@odata.id": "/redfish/v1/Systems/1/Storage"
|
||||
},
|
||||
"LogServices": {
|
||||
"@odata.id": "/redfish/v1/Systems/1/LogServices"
|
||||
},
|
||||
"Links": {
|
||||
"Chassis": [
|
||||
{
|
||||
"@odata.id": "/redfish/v1/Chassis/1"
|
||||
}
|
||||
],
|
||||
"ManagedBy": [
|
||||
{
|
||||
"@odata.id": "/redfish/v1/Managers/1"
|
||||
}
|
||||
],
|
||||
"Oem": {}
|
||||
},
|
||||
"Actions": {
|
||||
"#ComputerSystem.Reset": {
|
||||
"target": "/redfish/v1/Systems/1/Actions/ComputerSystem.Reset",
|
||||
"ResetType@Redfish.AllowableValues": [
|
||||
"On",
|
||||
"ForceOff",
|
||||
"GracefulShutdown",
|
||||
"GracefulRestart",
|
||||
"ForceRestart",
|
||||
"Nmi",
|
||||
"ForceOn"
|
||||
]
|
||||
}
|
||||
},
|
||||
"Oem": {
|
||||
"Supermicro": {}
|
||||
}
|
||||
}
|
||||
297
examples/mockbmc/mockbmc.go
Normal file
297
examples/mockbmc/mockbmc.go
Normal file
@@ -0,0 +1,297 @@
|
||||
// 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())
|
||||
}
|
||||
Reference in New Issue
Block a user