-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Find and Invoke Slurm APIs using reflect to avoid version specific code #32
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package slurm | ||
|
||
// Method names used in the Slurm API | ||
const ( | ||
methodPostNode = "PostNode" | ||
methodPing = "Ping" | ||
) | ||
|
||
// withExecuteSuffix appends "Execute" to the given method name. | ||
// For example, "PostNode" becomes "PostNodeExecute". | ||
func withExecuteSuffix(method string) string { | ||
return method + "Execute" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,9 @@ import ( | |
"log" | ||
"math" | ||
"net/http" | ||
"reflect" | ||
"regexp" | ||
"strings" | ||
"time" | ||
|
||
"github.com/nod-ai/ADA/redfish-exporter/api/generated/slurmrestdapi" | ||
|
@@ -50,11 +53,13 @@ type SlurmServerConfig struct { | |
|
||
type Client struct { | ||
apiClient *slurmrestdapi.APIClient // slurm URL to client mapping | ||
helper map[string]reflect.Method | ||
} | ||
|
||
var apiCl *Client // singleton client | ||
var singletonAPICl *Client // singleton client | ||
|
||
func NewClient(slurmControlNode, slurmUser, slurmToken string) (*Client, error) { | ||
c := &Client{} | ||
slConfig := &SlurmServerConfig{ | ||
URL: slurmControlNode, | ||
Username: defaultSlurmUsername, | ||
|
@@ -64,125 +69,175 @@ func NewClient(slurmControlNode, slurmUser, slurmToken string) (*Client, error) | |
slConfig.Username = slurmUser | ||
} | ||
cl := createRestClient(slConfig) | ||
c := &Client{apiClient: cl} | ||
|
||
// populate the methods required for ping and node update operations | ||
t := reflect.TypeOf(cl.SlurmAPI) | ||
postNodeRe := regexp.MustCompile(fmt.Sprintf(`%s$`, methodPostNode)) | ||
pingRe := regexp.MustCompile(fmt.Sprintf(`%s$`, methodPing)) | ||
for i := 0; i < t.NumMethod(); i++ { | ||
method := t.Method(i) | ||
if postNodeRe.MatchString(method.Name) { | ||
postNodeExecuteMethod, found := t.MethodByName(withExecuteSuffix(method.Name)) | ||
if !found { | ||
return nil, fmt.Errorf("could not find PostNodeExecute method from Slurm REST APIs") | ||
} | ||
|
||
if _, found := c.helper[methodPostNode]; !found { | ||
c.helper[methodPostNode] = method | ||
c.helper[withExecuteSuffix(methodPostNode)] = postNodeExecuteMethod | ||
} | ||
} else if pingRe.MatchString(method.Name) { | ||
pingExecuteMethod, found := t.MethodByName(withExecuteSuffix(method.Name)) | ||
if !found { | ||
return nil, fmt.Errorf("could not find PingExecute method from Slurm REST APIs") | ||
} | ||
|
||
if _, found := c.helper[methodPing]; !found { | ||
c.helper[methodPing] = method | ||
c.helper[withExecuteSuffix(methodPing)] = pingExecuteMethod | ||
} | ||
} | ||
} | ||
|
||
c.apiClient = cl | ||
|
||
log.Printf("[slurm] created slurm client for node: %v\n", slurmControlNode) | ||
err := c.getConnectionStatus() | ||
if err != nil { | ||
log.Printf("[slurm] error in getting the connection status of the slurm node: %v, err: %+v\n", slurmControlNode, err) | ||
} | ||
|
||
apiCl = c | ||
singletonAPICl = c | ||
return c, err | ||
} | ||
|
||
func GetClient() *Client { | ||
return apiCl | ||
return singletonAPICl | ||
} | ||
|
||
func (c *Client) ResumeNode(nodeName string) error { | ||
apiCall := func() (interface{}, *http.Response, error) { | ||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) | ||
jreq := c.apiClient.SlurmAPI.SlurmV0040PostNode(ctx, nodeName) | ||
req := slurmrestdapi.V0040UpdateNodeMsg{State: []string{"resume"}} | ||
jreq = jreq.V0040UpdateNodeMsg(req) | ||
res, resp, err := c.apiClient.SlurmAPI.SlurmV0040PostNodeExecute(jreq) | ||
cancel() | ||
if err != nil { | ||
return res, resp, err | ||
} else if resp.StatusCode != 200 { | ||
return res, resp, fmt.Errorf("invalid status code: %v", resp.StatusCode) | ||
} | ||
return res, resp, nil | ||
} | ||
|
||
_, resp, err := CallWithRetry(apiCall, maxRetries, baseDelay) | ||
if err != nil { | ||
return err | ||
} | ||
defer resp.Body.Close() | ||
|
||
return nil | ||
return c.updateNodeState(nodeName, "resume") | ||
} | ||
|
||
func (c *Client) DrainNode(nodeName string) error { | ||
return c.updateNodeState(nodeName, "drain") | ||
} | ||
|
||
func (c *Client) getConnectionStatus() error { | ||
apiCall := func() (interface{}, *http.Response, error) { | ||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) | ||
jreq := c.apiClient.SlurmAPI.SlurmV0040PostNode(ctx, nodeName) | ||
req := slurmrestdapi.V0040UpdateNodeMsg{State: []string{"drain"}} | ||
jreq = jreq.V0040UpdateNodeMsg(req) | ||
res, resp, err := c.apiClient.SlurmAPI.SlurmV0040PostNodeExecute(jreq) | ||
cancel() | ||
if err != nil { | ||
return res, resp, err | ||
} else if resp.StatusCode != 200 { | ||
return res, resp, fmt.Errorf("invalid status code: %v", resp.StatusCode) | ||
defer cancel() | ||
|
||
// Step 1: Call the Ping method using reflection | ||
pingVals := c.helper["Ping"].Func.Call([]reflect.Value{ | ||
reflect.ValueOf(c.apiClient.SlurmAPI), | ||
reflect.ValueOf(ctx), | ||
}) | ||
|
||
// Check if the call produced results | ||
if len(pingVals) == 0 { | ||
return nil, nil, fmt.Errorf("Ping call returned no values") | ||
} | ||
return res, resp, nil | ||
|
||
// Step 2: Execute the Ping method with the request | ||
pingResp := c.helper["PingExecute"].Func.Call([]reflect.Value{ | ||
reflect.ValueOf(c.apiClient.SlurmAPI), | ||
pingVals[0], | ||
}) | ||
|
||
// Extract and return the response and error | ||
resp, _ := pingResp[1].Interface().(*http.Response) | ||
err, _ := pingResp[2].Interface().(error) | ||
return pingResp[0].Interface(), resp, err | ||
} | ||
|
||
_, resp, err := CallWithRetry(apiCall, maxRetries, baseDelay) | ||
if err != nil { | ||
return err | ||
return nil | ||
} | ||
defer resp.Body.Close() | ||
|
||
log.Printf("[slurm] ping success: %v\n", resp.StatusCode) | ||
return nil | ||
} | ||
|
||
func (c *Client) GetNodes() ([]string, error) { | ||
var nodes []string | ||
func (c *Client) updateNodeState(nodeName, state string) error { | ||
apiCall := func() (interface{}, *http.Response, error) { | ||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) | ||
jreq := c.apiClient.SlurmAPI.SlurmV0039GetNodes(ctx) | ||
res, resp, err := c.apiClient.SlurmAPI.SlurmV0039GetNodesExecute(jreq) | ||
cancel() | ||
if err != nil { | ||
return res, resp, err | ||
} else if resp.StatusCode != 200 { | ||
return res, resp, fmt.Errorf("invalid status code: %v", resp.StatusCode) | ||
defer cancel() | ||
|
||
// Step 1: Call the PostNode method using reflection | ||
postNodeVals := c.helper["PostNode"].Func.Call([]reflect.Value{ | ||
reflect.ValueOf(c.apiClient.SlurmAPI), | ||
reflect.ValueOf(ctx), | ||
reflect.ValueOf(nodeName), | ||
}) | ||
|
||
// Check if the call produced results | ||
if len(postNodeVals) == 0 { | ||
return nil, nil, fmt.Errorf("PostNode call returned no values") | ||
} | ||
return res, resp, nil | ||
} | ||
|
||
res, resp, err := CallWithRetry(apiCall, maxRetries, baseDelay) | ||
if err != nil { | ||
return nodes, err | ||
} | ||
defer resp.Body.Close() | ||
|
||
log.Printf("[slurm] get nodes: %+v\n", nodes) | ||
temp := res.(*slurmrestdapi.V0039NodesResponse) | ||
for _, node := range temp.GetNodes() { | ||
nodes = append(nodes, *node.Name) | ||
} | ||
return nodes, nil | ||
} | ||
|
||
func (c *Client) getConnectionStatus() error { | ||
apiCall := func() (interface{}, *http.Response, error) { | ||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) | ||
jreq := c.apiClient.SlurmAPI.SlurmV0039Ping(ctx) | ||
res, resp, err := c.apiClient.SlurmAPI.SlurmV0039PingExecute(jreq) | ||
cancel() | ||
if err != nil { | ||
return res, resp, err | ||
} else if resp.StatusCode != 200 { | ||
return res, resp, fmt.Errorf("invalid status code: %v", resp.StatusCode) | ||
// Step 2: Find and call the UpdateNodeMsg method on the request object | ||
newInstance := reflect.New(postNodeVals[0].Type()).Elem() | ||
instanceType := newInstance.Type() | ||
|
||
for i := 0; i < instanceType.NumMethod(); i++ { | ||
method := instanceType.Method(i) | ||
if strings.Contains(method.Name, "UpdateNodeMsg") { | ||
// Create a new UpdateNodeMsg request | ||
updateNodeMsgReq := createUpdateNodeMsgRequest(method.Type) | ||
if updateNodeMsgReq.IsValid() { | ||
updateNodeMsgReq.FieldByName("State").Set(reflect.ValueOf([]string{state})) | ||
} | ||
|
||
// Step 3: Call UpdateNodeMsg with the request | ||
updatedNodeVals := method.Func.Call([]reflect.Value{postNodeVals[0], updateNodeMsgReq}) | ||
if len(updatedNodeVals) == 0 { | ||
return nil, nil, fmt.Errorf("UpdateNodeMsg call returned no values") | ||
} | ||
|
||
// Step 4: Execute the PostNode method with the updated request | ||
postNodeResp := c.helper["PostNodeExecute"].Func.Call([]reflect.Value{ | ||
reflect.ValueOf(c.apiClient.SlurmAPI), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's just one corner case, if the input parameters of the PostNodeExecute function (or any other function that we call) change between Slurm versions that this code will not work. Such an API change would be a breaking change though and hopefully will happen in a corner case. If it happens, user would have to manually change this code to pass the new parameters. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, If the API, params or the names change, then this code will have to updated. |
||
updatedNodeVals[0], | ||
}) | ||
|
||
if len(postNodeResp) < 3 { | ||
return nil, nil, fmt.Errorf("PostNodeExecute call returned insufficient values") | ||
} | ||
|
||
// Extract and return the response and error | ||
resp, _ := postNodeResp[1].Interface().(*http.Response) | ||
err, _ := postNodeResp[2].Interface().(error) | ||
return postNodeResp[0].Interface(), resp, err | ||
} | ||
} | ||
return res, resp, nil | ||
|
||
return nil, nil, fmt.Errorf("no suitable UpdateNodeMsg method found") | ||
} | ||
|
||
// Retry the API call | ||
_, resp, err := CallWithRetry(apiCall, maxRetries, baseDelay) | ||
if err != nil { | ||
return nil | ||
return err | ||
} | ||
defer resp.Body.Close() | ||
|
||
log.Printf("[slurm] ping success: %v\n", resp.StatusCode) | ||
return nil | ||
} | ||
|
||
// Helper function to create an UpdateNodeMsg request using reflection | ||
func createUpdateNodeMsgRequest(methodType reflect.Type) reflect.Value { | ||
for j := 1; j < methodType.NumIn(); j++ { // Start from 1 to skip the receiver | ||
paramType := methodType.In(j) | ||
if strings.Contains(paramType.Name(), "UpdateNodeMsg") { | ||
return reflect.New(paramType).Elem() | ||
} | ||
} | ||
return reflect.Value{} | ||
} | ||
|
||
func createRestClient(c *SlurmServerConfig) *slurmrestdapi.APIClient { | ||
cfg := slurmrestdapi.NewConfiguration() | ||
cfg.HTTPClient = &http.Client{Timeout: slurmRestClientTimeout} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to store the function pointer to the method instead of storing the reflect method? That way, when calling the method you can directly call the function pointer instead of calling the function using reflect. Just a minor optimization, if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sajmera-pensando
If we need to convert the reflect.Method to a function pointer then we need to know the exact API signature and In this case, the parameters are also version specific.
func (a *SlurmAPIService) SlurmV0040PostNodeExecute(r ApiSlurmV0040PostNodeRequest) (*V0040OpenapiResp, *http.Response, error)
func (r ApiSlurmV0040PostNodeRequest) V0040UpdateNodeMsg(v0040UpdateNodeMsg V0040UpdateNodeMsg) ApiSlurmV0040PostNodeRequest