feat: 添加 Node 领域类型和 NodesService
包含 Node、UpdateNodeMsg、AcctGatherEnergy、ExtSensorsData、PowerMgmtData 等类型。NodesService 提供 GetNodes、GetNode、PostNode、DeleteNode 4 个方法,并定义 NodeFlag* 常量。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
126
internal/slurm/slurm_nodes.go
Normal file
126
internal/slurm/slurm_nodes.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package slurm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Node query flags for GetNodesOptions.Flags and GetNodeOptions.Flags.
|
||||||
|
const (
|
||||||
|
NodeFlagAll = "ALL"
|
||||||
|
NodeFlagDetail = "DETAIL"
|
||||||
|
NodeFlagMixed = "MIXED"
|
||||||
|
NodeFlagLocal = "LOCAL"
|
||||||
|
NodeFlagSibling = "SIBLING"
|
||||||
|
NodeFlagFederation = "FEDERATION"
|
||||||
|
NodeFlagFuture = "FUTURE"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetNodesOptions specifies optional parameters for GetNodes.
|
||||||
|
type GetNodesOptions struct {
|
||||||
|
UpdateTime *string `url:"update_time,omitempty"`
|
||||||
|
Flags *string `url:"flags,omitempty"` // Use NodeFlag* constants (e.g. NodeFlagDetail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNodeOptions specifies optional parameters for GetNode.
|
||||||
|
type GetNodeOptions struct {
|
||||||
|
UpdateTime *string `url:"update_time,omitempty"`
|
||||||
|
Flags *string `url:"flags,omitempty"` // Use NodeFlag* constants (e.g. NodeFlagDetail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNodes lists all nodes.
|
||||||
|
func (s *NodesService) GetNodes(ctx context.Context, opts *GetNodesOptions) (*OpenapiNodesResp, *Response, error) {
|
||||||
|
path := "slurm/v0.0.40/nodes"
|
||||||
|
req, err := s.client.NewRequest("GET", path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts != nil {
|
||||||
|
u, parseErr := url.Parse(req.URL.String())
|
||||||
|
if parseErr != nil {
|
||||||
|
return nil, nil, parseErr
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
if opts.UpdateTime != nil {
|
||||||
|
q.Set("update_time", *opts.UpdateTime)
|
||||||
|
}
|
||||||
|
if opts.Flags != nil {
|
||||||
|
q.Set("flags", *opts.Flags)
|
||||||
|
}
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
req.URL = u
|
||||||
|
}
|
||||||
|
|
||||||
|
var result OpenapiNodesResp
|
||||||
|
resp, err := s.client.Do(ctx, req, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp, err
|
||||||
|
}
|
||||||
|
return &result, resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNode gets a single node by name.
|
||||||
|
func (s *NodesService) GetNode(ctx context.Context, nodeName string, opts *GetNodeOptions) (*OpenapiNodesResp, *Response, error) {
|
||||||
|
path := fmt.Sprintf("slurm/v0.0.40/node/%s", nodeName)
|
||||||
|
req, err := s.client.NewRequest("GET", path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts != nil {
|
||||||
|
u, parseErr := url.Parse(req.URL.String())
|
||||||
|
if parseErr != nil {
|
||||||
|
return nil, nil, parseErr
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
if opts.UpdateTime != nil {
|
||||||
|
q.Set("update_time", *opts.UpdateTime)
|
||||||
|
}
|
||||||
|
if opts.Flags != nil {
|
||||||
|
q.Set("flags", *opts.Flags)
|
||||||
|
}
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
req.URL = u
|
||||||
|
}
|
||||||
|
|
||||||
|
var result OpenapiNodesResp
|
||||||
|
resp, err := s.client.Do(ctx, req, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp, err
|
||||||
|
}
|
||||||
|
return &result, resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostNode updates a node.
|
||||||
|
func (s *NodesService) PostNode(ctx context.Context, nodeName string, update *UpdateNodeMsg) (*OpenapiResp, *Response, error) {
|
||||||
|
path := fmt.Sprintf("slurm/v0.0.40/node/%s", nodeName)
|
||||||
|
req, err := s.client.NewRequest("POST", path, update)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result OpenapiResp
|
||||||
|
resp, err := s.client.Do(ctx, req, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp, err
|
||||||
|
}
|
||||||
|
return &result, resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteNode deletes a node.
|
||||||
|
func (s *NodesService) DeleteNode(ctx context.Context, nodeName string) (*OpenapiResp, *Response, error) {
|
||||||
|
path := fmt.Sprintf("slurm/v0.0.40/node/%s", nodeName)
|
||||||
|
req, err := s.client.NewRequest("DELETE", path, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result OpenapiResp
|
||||||
|
resp, err := s.client.Do(ctx, req, &result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp, err
|
||||||
|
}
|
||||||
|
return &result, resp, nil
|
||||||
|
}
|
||||||
163
internal/slurm/slurm_nodes_test.go
Normal file
163
internal/slurm/slurm_nodes_test.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package slurm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNodesService_GetNodes(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/slurm/v0.0.40/nodes", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testMethod(t, r, "GET")
|
||||||
|
fmt.Fprint(w, `{"nodes": []}`)
|
||||||
|
})
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, _ := NewClient(server.URL, nil)
|
||||||
|
resp, _, err := client.Nodes.GetNodes(context.Background(), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("expected non-nil response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodesService_GetNodes_WithOptions(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/slurm/v0.0.40/nodes", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testMethod(t, r, "GET")
|
||||||
|
q := r.URL.Query()
|
||||||
|
if q.Get("update_time") != "12345" {
|
||||||
|
t.Errorf("expected update_time=12345, got %s", q.Get("update_time"))
|
||||||
|
}
|
||||||
|
if q.Get("flags") != NodeFlagDetail {
|
||||||
|
t.Errorf("expected flags=%s, got %s", NodeFlagDetail, q.Get("flags"))
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `{"nodes": []}`)
|
||||||
|
})
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, _ := NewClient(server.URL, nil)
|
||||||
|
opts := &GetNodesOptions{
|
||||||
|
UpdateTime: Ptr("12345"),
|
||||||
|
Flags: Ptr(NodeFlagDetail),
|
||||||
|
}
|
||||||
|
resp, _, err := client.Nodes.GetNodes(context.Background(), opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("expected non-nil response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodesService_GetNode(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/slurm/v0.0.40/node/node1", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testMethod(t, r, "GET")
|
||||||
|
fmt.Fprint(w, `{"nodes": [{"name": "node1"}]}`)
|
||||||
|
})
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, _ := NewClient(server.URL, nil)
|
||||||
|
resp, _, err := client.Nodes.GetNode(context.Background(), "node1", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("expected non-nil response")
|
||||||
|
}
|
||||||
|
if resp.Nodes == nil || len(*resp.Nodes) != 1 {
|
||||||
|
t.Fatalf("expected 1 node, got %v", resp.Nodes)
|
||||||
|
}
|
||||||
|
if resp.Nodes == nil || (*resp.Nodes)[0].Name == nil || *(*resp.Nodes)[0].Name != "node1" {
|
||||||
|
t.Errorf("expected name=node1, got %v", resp.Nodes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodesService_PostNode(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/slurm/v0.0.40/node/node1", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testMethod(t, r, "POST")
|
||||||
|
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
|
||||||
|
t.Errorf("expected Content-Type application/json, got %s", ct)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `{}`)
|
||||||
|
})
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, _ := NewClient(server.URL, nil)
|
||||||
|
update := &UpdateNodeMsg{
|
||||||
|
Comment: Ptr("updated comment"),
|
||||||
|
}
|
||||||
|
resp, _, err := client.Nodes.PostNode(context.Background(), "node1", update)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("expected non-nil response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodesService_DeleteNode(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/slurm/v0.0.40/node/node1", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
testMethod(t, r, "DELETE")
|
||||||
|
fmt.Fprint(w, `{}`)
|
||||||
|
})
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, _ := NewClient(server.URL, nil)
|
||||||
|
resp, _, err := client.Nodes.DeleteNode(context.Background(), "node1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
t.Fatal("expected non-nil response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodesService_GetNode_Error(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/slurm/v0.0.40/node/nonexistent", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
fmt.Fprint(w, `{"errors": [{"error": "node not found"}]}`)
|
||||||
|
})
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, _ := NewClient(server.URL, nil)
|
||||||
|
_, _, err := client.Nodes.GetNode(context.Background(), "nonexistent", nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for 404 response")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "404") {
|
||||||
|
t.Errorf("expected error to contain 404, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodesService_GetNodes_Error(t *testing.T) {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/slurm/v0.0.40/nodes", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
fmt.Fprint(w, `{"errors": [{"error": "internal error"}]}`)
|
||||||
|
})
|
||||||
|
server := httptest.NewServer(mux)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client, _ := NewClient(server.URL, nil)
|
||||||
|
_, _, err := client.Nodes.GetNodes(context.Background(), nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for 500 response")
|
||||||
|
}
|
||||||
|
}
|
||||||
121
internal/slurm/types_node.go
Normal file
121
internal/slurm/types_node.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package slurm
|
||||||
|
|
||||||
|
// AcctGatherEnergy represents energy accounting data (v0.0.40_acct_gather_energy).
|
||||||
|
type AcctGatherEnergy struct {
|
||||||
|
AverageWatts *int32 `json:"average_watts,omitempty"`
|
||||||
|
BaseConsumedEnergy *int64 `json:"base_consumed_energy,omitempty"`
|
||||||
|
ConsumedEnergy *int64 `json:"consumed_energy,omitempty"`
|
||||||
|
CurrentWatts *Uint32NoVal `json:"current_watts,omitempty"`
|
||||||
|
PreviousConsumedEnergy *int64 `json:"previous_consumed_energy,omitempty"`
|
||||||
|
LastCollected *int64 `json:"last_collected,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtSensorsData represents external sensor data (v0.0.40_ext_sensors_data).
|
||||||
|
type ExtSensorsData struct {
|
||||||
|
ConsumedEnergy *Uint64NoVal `json:"consumed_energy,omitempty"`
|
||||||
|
Temperature *Uint32NoVal `json:"temperature,omitempty"`
|
||||||
|
EnergyUpdateTime *int64 `json:"energy_update_time,omitempty"`
|
||||||
|
CurrentWatts *int32 `json:"current_watts,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PowerMgmtData represents power management data (v0.0.40_power_mgmt_data).
|
||||||
|
type PowerMgmtData struct {
|
||||||
|
MaximumWatts *Uint32NoVal `json:"maximum_watts,omitempty"`
|
||||||
|
CurrentWatts *int32 `json:"current_watts,omitempty"`
|
||||||
|
TotalEnergy *int64 `json:"total_energy,omitempty"`
|
||||||
|
NewMaximumWatts *int32 `json:"new_maximum_watts,omitempty"`
|
||||||
|
PeakWatts *int32 `json:"peak_watts,omitempty"`
|
||||||
|
LowestWatts *int32 `json:"lowest_watts,omitempty"`
|
||||||
|
NewJobTime *Uint64NoVal `json:"new_job_time,omitempty"`
|
||||||
|
State *int32 `json:"state,omitempty"`
|
||||||
|
TimeStartDay *int64 `json:"time_start_day,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node represents a Slurm compute node (v0.0.40_node).
|
||||||
|
type Node struct {
|
||||||
|
Architecture *string `json:"architecture,omitempty"`
|
||||||
|
BurstbufferNetworkAddress *string `json:"burstbuffer_network_address,omitempty"`
|
||||||
|
Boards *int32 `json:"boards,omitempty"`
|
||||||
|
BootTime *Uint64NoVal `json:"boot_time,omitempty"`
|
||||||
|
ClusterName *string `json:"cluster_name,omitempty"`
|
||||||
|
Cores *int32 `json:"cores,omitempty"`
|
||||||
|
SpecializedCores *int32 `json:"specialized_cores,omitempty"`
|
||||||
|
CpuBinding *int32 `json:"cpu_binding,omitempty"`
|
||||||
|
CpuLoad *int32 `json:"cpu_load,omitempty"`
|
||||||
|
FreeMem *Uint64NoVal `json:"free_mem,omitempty"`
|
||||||
|
Cpus *int32 `json:"cpus,omitempty"`
|
||||||
|
EffectiveCpus *int32 `json:"effective_cpus,omitempty"`
|
||||||
|
SpecializedCpus *string `json:"specialized_cpus,omitempty"`
|
||||||
|
Energy *AcctGatherEnergy `json:"energy,omitempty"`
|
||||||
|
ExternalSensors *ExtSensorsData `json:"external_sensors,omitempty"`
|
||||||
|
Extra *string `json:"extra,omitempty"`
|
||||||
|
Power *PowerMgmtData `json:"power,omitempty"`
|
||||||
|
Features *CSVString `json:"features,omitempty"`
|
||||||
|
ActiveFeatures *CSVString `json:"active_features,omitempty"`
|
||||||
|
Gres *string `json:"gres,omitempty"`
|
||||||
|
GresDrained *string `json:"gres_drained,omitempty"`
|
||||||
|
GresUsed *string `json:"gres_used,omitempty"`
|
||||||
|
InstanceID *string `json:"instance_id,omitempty"`
|
||||||
|
InstanceType *string `json:"instance_type,omitempty"`
|
||||||
|
LastBusy *Uint64NoVal `json:"last_busy,omitempty"`
|
||||||
|
McsLabel *string `json:"mcs_label,omitempty"`
|
||||||
|
SpecializedMemory *int64 `json:"specialized_memory,omitempty"`
|
||||||
|
Name *string `json:"name,omitempty"`
|
||||||
|
NextStateAfterReboot []string `json:"next_state_after_reboot,omitempty"`
|
||||||
|
Address *string `json:"address,omitempty"`
|
||||||
|
Hostname *string `json:"hostname,omitempty"`
|
||||||
|
State []string `json:"state,omitempty"`
|
||||||
|
OperatingSystem *string `json:"operating_system,omitempty"`
|
||||||
|
Owner *string `json:"owner,omitempty"`
|
||||||
|
Partitions *CSVString `json:"partitions,omitempty"`
|
||||||
|
Port *int32 `json:"port,omitempty"`
|
||||||
|
RealMemory *int64 `json:"real_memory,omitempty"`
|
||||||
|
Comment *string `json:"comment,omitempty"`
|
||||||
|
Reason *string `json:"reason,omitempty"`
|
||||||
|
ReasonChangedAt *Uint64NoVal `json:"reason_changed_at,omitempty"`
|
||||||
|
ReasonSetByUser *string `json:"reason_set_by_user,omitempty"`
|
||||||
|
ResumeAfter *Uint64NoVal `json:"resume_after,omitempty"`
|
||||||
|
Reservation *string `json:"reservation,omitempty"`
|
||||||
|
AllocMemory *int64 `json:"alloc_memory,omitempty"`
|
||||||
|
AllocCpus *int32 `json:"alloc_cpus,omitempty"`
|
||||||
|
AllocIdleCpus *int32 `json:"alloc_idle_cpus,omitempty"`
|
||||||
|
TresUsed *string `json:"tres_used,omitempty"`
|
||||||
|
TresWeighted *float64 `json:"tres_weighted,omitempty"`
|
||||||
|
SlurmdStartTime *Uint64NoVal `json:"slurmd_start_time,omitempty"`
|
||||||
|
Sockets *int32 `json:"sockets,omitempty"`
|
||||||
|
Threads *int32 `json:"threads,omitempty"`
|
||||||
|
TemporaryDisk *int32 `json:"temporary_disk,omitempty"`
|
||||||
|
Weight *int32 `json:"weight,omitempty"`
|
||||||
|
Tres *string `json:"tres,omitempty"`
|
||||||
|
Version *string `json:"version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nodes is a collection of Node objects (v0.0.40_nodes).
|
||||||
|
type Nodes []Node
|
||||||
|
|
||||||
|
// UpdateNodeMsg represents a node update request (v0.0.40_update_node_msg).
|
||||||
|
type UpdateNodeMsg struct {
|
||||||
|
Comment *string `json:"comment,omitempty"`
|
||||||
|
CpuBind *int32 `json:"cpu_bind,omitempty"`
|
||||||
|
Extra *string `json:"extra,omitempty"`
|
||||||
|
Features *CSVString `json:"features,omitempty"`
|
||||||
|
FeaturesAct *CSVString `json:"features_act,omitempty"`
|
||||||
|
Gres *string `json:"gres,omitempty"`
|
||||||
|
Address *HostlistString `json:"address,omitempty"`
|
||||||
|
Hostname *HostlistString `json:"hostname,omitempty"`
|
||||||
|
Name *HostlistString `json:"name,omitempty"`
|
||||||
|
State []string `json:"state,omitempty"`
|
||||||
|
Reason *string `json:"reason,omitempty"`
|
||||||
|
ReasonUID *string `json:"reason_uid,omitempty"`
|
||||||
|
ResumeAfter *Uint32NoVal `json:"resume_after,omitempty"`
|
||||||
|
Weight *Uint32NoVal `json:"weight,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenapiNodesResp represents the response for node queries (v0.0.40_openapi_nodes_resp).
|
||||||
|
type OpenapiNodesResp struct {
|
||||||
|
Nodes *Nodes `json:"nodes,omitempty"`
|
||||||
|
LastUpdate *Uint64NoVal `json:"last_update,omitempty"`
|
||||||
|
Meta *OpenapiMeta `json:"meta,omitempty"`
|
||||||
|
Errors OpenapiErrors `json:"errors,omitempty"`
|
||||||
|
Warnings OpenapiWarnings `json:"warnings,omitempty"`
|
||||||
|
}
|
||||||
296
internal/slurm/types_node_test.go
Normal file
296
internal/slurm/types_node_test.go
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
package slurm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNodeRoundTrip(t *testing.T) {
|
||||||
|
orig := Node{
|
||||||
|
Architecture: Ptr("x86_64"),
|
||||||
|
Boards: Ptr(int32(1)),
|
||||||
|
BootTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))},
|
||||||
|
ClusterName: Ptr("test-cluster"),
|
||||||
|
Cores: Ptr(int32(16)),
|
||||||
|
CpuLoad: Ptr(int32(50)),
|
||||||
|
FreeMem: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(32768))},
|
||||||
|
Cpus: Ptr(int32(64)),
|
||||||
|
Energy: &AcctGatherEnergy{
|
||||||
|
AverageWatts: Ptr(int32(200)),
|
||||||
|
ConsumedEnergy: Ptr(int64(15000)),
|
||||||
|
CurrentWatts: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(180))},
|
||||||
|
LastCollected: Ptr(int64(1234567900)),
|
||||||
|
},
|
||||||
|
ExternalSensors: &ExtSensorsData{
|
||||||
|
CurrentWatts: Ptr(int32(175)),
|
||||||
|
ConsumedEnergy: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(12000))},
|
||||||
|
EnergyUpdateTime: Ptr(int64(1234567900)),
|
||||||
|
},
|
||||||
|
Power: &PowerMgmtData{
|
||||||
|
CurrentWatts: Ptr(int32(200)),
|
||||||
|
PeakWatts: Ptr(int32(350)),
|
||||||
|
LowestWatts: Ptr(int32(100)),
|
||||||
|
},
|
||||||
|
Features: &CSVString{"gpu", "nvme"},
|
||||||
|
ActiveFeatures: &CSVString{"gpu"},
|
||||||
|
Gres: Ptr("gpu:4"),
|
||||||
|
Name: Ptr("node01"),
|
||||||
|
State: []string{"IDLE", "POWERED_DOWN"},
|
||||||
|
Hostname: Ptr("node01.example.com"),
|
||||||
|
Address: Ptr("10.0.0.1"),
|
||||||
|
Partitions: &CSVString{"normal", "gpu"},
|
||||||
|
Port: Ptr(int32(6818)),
|
||||||
|
RealMemory: Ptr(int64(128000)),
|
||||||
|
Sockets: Ptr(int32(2)),
|
||||||
|
Threads: Ptr(int32(2)),
|
||||||
|
Weight: Ptr(int32(1)),
|
||||||
|
Tres: Ptr("cpu=64,mem=128G"),
|
||||||
|
Version: Ptr("24.05.5"),
|
||||||
|
AllocCpus: Ptr(int32(0)),
|
||||||
|
AllocMemory: Ptr(int64(0)),
|
||||||
|
SlurmdStartTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234500000))},
|
||||||
|
ReasonChangedAt: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(0))},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(orig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
var decoded Node
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if decoded.Name == nil || *decoded.Name != "node01" {
|
||||||
|
t.Fatalf("name mismatch: %v", decoded.Name)
|
||||||
|
}
|
||||||
|
if decoded.Cpus == nil || *decoded.Cpus != 64 {
|
||||||
|
t.Fatalf("cpus mismatch: %v", decoded.Cpus)
|
||||||
|
}
|
||||||
|
if len(decoded.State) != 2 || decoded.State[0] != "IDLE" {
|
||||||
|
t.Fatalf("state mismatch: %v", decoded.State)
|
||||||
|
}
|
||||||
|
if decoded.Energy == nil || decoded.Energy.AverageWatts == nil || *decoded.Energy.AverageWatts != 200 {
|
||||||
|
t.Fatalf("energy.average_watts mismatch: %v", decoded.Energy)
|
||||||
|
}
|
||||||
|
if decoded.ExternalSensors == nil || decoded.ExternalSensors.CurrentWatts == nil || *decoded.ExternalSensors.CurrentWatts != 175 {
|
||||||
|
t.Fatalf("external_sensors.current_watts mismatch: %v", decoded.ExternalSensors)
|
||||||
|
}
|
||||||
|
if decoded.Power == nil || decoded.Power.PeakWatts == nil || *decoded.Power.PeakWatts != 350 {
|
||||||
|
t.Fatalf("power.peak_watts mismatch: %v", decoded.Power)
|
||||||
|
}
|
||||||
|
if decoded.Features == nil || len(*decoded.Features) != 2 || (*decoded.Features)[0] != "gpu" {
|
||||||
|
t.Fatalf("features mismatch: %v", decoded.Features)
|
||||||
|
}
|
||||||
|
if decoded.BootTime == nil || decoded.BootTime.Number == nil || *decoded.BootTime.Number != 1234567890 {
|
||||||
|
t.Fatalf("boot_time mismatch: %v", decoded.BootTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNodeEmptyRoundTrip(t *testing.T) {
|
||||||
|
orig := Node{}
|
||||||
|
data, err := json.Marshal(orig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
if string(data) != "{}" {
|
||||||
|
t.Fatalf("empty Node should marshal to {}, got %s", data)
|
||||||
|
}
|
||||||
|
var decoded Node
|
||||||
|
if err := json.Unmarshal([]byte(`{}`), &decoded); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if decoded.Name != nil {
|
||||||
|
t.Fatal("name should be nil for empty object")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateNodeMsgRoundTrip(t *testing.T) {
|
||||||
|
orig := UpdateNodeMsg{
|
||||||
|
Comment: Ptr("maintenance window"),
|
||||||
|
CpuBind: Ptr(int32(1)),
|
||||||
|
Extra: Ptr("custom data"),
|
||||||
|
Features: &CSVString{"new_feat"},
|
||||||
|
FeaturesAct: &CSVString{"active_feat"},
|
||||||
|
Gres: Ptr("gpu:2"),
|
||||||
|
Address: &HostlistString{"node01"},
|
||||||
|
Hostname: &HostlistString{"node01.example.com"},
|
||||||
|
Name: &HostlistString{"node01"},
|
||||||
|
State: []string{"DRAIN"},
|
||||||
|
Reason: Ptr("scheduled maintenance"),
|
||||||
|
ReasonUID: Ptr("root"),
|
||||||
|
ResumeAfter: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(3600))},
|
||||||
|
Weight: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(10))},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(orig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
var decoded UpdateNodeMsg
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if decoded.Comment == nil || *decoded.Comment != "maintenance window" {
|
||||||
|
t.Fatalf("comment mismatch: %v", decoded.Comment)
|
||||||
|
}
|
||||||
|
if len(decoded.State) != 1 || decoded.State[0] != "DRAIN" {
|
||||||
|
t.Fatalf("state mismatch: %v", decoded.State)
|
||||||
|
}
|
||||||
|
if decoded.ResumeAfter == nil || decoded.ResumeAfter.Number == nil || *decoded.ResumeAfter.Number != 3600 {
|
||||||
|
t.Fatalf("resume_after mismatch: %v", decoded.ResumeAfter)
|
||||||
|
}
|
||||||
|
if decoded.Weight == nil || decoded.Weight.Number == nil || *decoded.Weight.Number != 10 {
|
||||||
|
t.Fatalf("weight mismatch: %v", decoded.Weight)
|
||||||
|
}
|
||||||
|
if decoded.Name == nil || len(*decoded.Name) != 1 || (*decoded.Name)[0] != "node01" {
|
||||||
|
t.Fatalf("name mismatch: %v", decoded.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenapiNodesRespRoundTrip(t *testing.T) {
|
||||||
|
nodes := Nodes{
|
||||||
|
{
|
||||||
|
Name: Ptr("node01"),
|
||||||
|
State: []string{"IDLE"},
|
||||||
|
Cpus: Ptr(int32(64)),
|
||||||
|
RealMemory: Ptr(int64(128000)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: Ptr("node02"),
|
||||||
|
State: []string{"ALLOCATED"},
|
||||||
|
Cpus: Ptr(int32(32)),
|
||||||
|
RealMemory: Ptr(int64(64000)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
orig := OpenapiNodesResp{
|
||||||
|
Nodes: &nodes,
|
||||||
|
LastUpdate: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))},
|
||||||
|
Meta: &OpenapiMeta{
|
||||||
|
Slurm: &MetaSlurm{
|
||||||
|
Version: &MetaSlurmVersion{
|
||||||
|
Major: Ptr("24"),
|
||||||
|
Micro: Ptr("5"),
|
||||||
|
Minor: Ptr("05"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Errors: OpenapiErrors{},
|
||||||
|
Warnings: OpenapiWarnings{},
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(orig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
var decoded OpenapiNodesResp
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if decoded.Nodes == nil || len(*decoded.Nodes) != 2 {
|
||||||
|
t.Fatalf("expected 2 nodes, got %d", len(*decoded.Nodes))
|
||||||
|
}
|
||||||
|
nodesSlice := *decoded.Nodes
|
||||||
|
if nodesSlice[0].Name == nil || *nodesSlice[0].Name != "node01" {
|
||||||
|
t.Fatalf("nodes[0].name mismatch")
|
||||||
|
}
|
||||||
|
if nodesSlice[1].Cpus == nil || *nodesSlice[1].Cpus != 32 {
|
||||||
|
t.Fatalf("nodes[1].cpus mismatch")
|
||||||
|
}
|
||||||
|
if decoded.LastUpdate == nil || decoded.LastUpdate.Number == nil || *decoded.LastUpdate.Number != 1234567890 {
|
||||||
|
t.Fatalf("last_update mismatch: %v", decoded.LastUpdate)
|
||||||
|
}
|
||||||
|
if decoded.Meta == nil || decoded.Meta.Slurm == nil || decoded.Meta.Slurm.Version == nil {
|
||||||
|
t.Fatal("meta.slurm.version should not be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAcctGatherEnergyRoundTrip(t *testing.T) {
|
||||||
|
orig := AcctGatherEnergy{
|
||||||
|
AverageWatts: Ptr(int32(250)),
|
||||||
|
BaseConsumedEnergy: Ptr(int64(10000)),
|
||||||
|
ConsumedEnergy: Ptr(int64(50000)),
|
||||||
|
CurrentWatts: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(210))},
|
||||||
|
PreviousConsumedEnergy: Ptr(int64(40000)),
|
||||||
|
LastCollected: Ptr(int64(1234567999)),
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(orig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
var decoded AcctGatherEnergy
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if decoded.AverageWatts == nil || *decoded.AverageWatts != 250 {
|
||||||
|
t.Fatalf("average_watts mismatch: %v", decoded.AverageWatts)
|
||||||
|
}
|
||||||
|
if decoded.ConsumedEnergy == nil || *decoded.ConsumedEnergy != 50000 {
|
||||||
|
t.Fatalf("consumed_energy mismatch: %v", decoded.ConsumedEnergy)
|
||||||
|
}
|
||||||
|
if decoded.CurrentWatts == nil || decoded.CurrentWatts.Number == nil || *decoded.CurrentWatts.Number != 210 {
|
||||||
|
t.Fatalf("current_watts mismatch: %v", decoded.CurrentWatts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtSensorsDataRoundTrip(t *testing.T) {
|
||||||
|
orig := ExtSensorsData{
|
||||||
|
ConsumedEnergy: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(99999))},
|
||||||
|
Temperature: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(42))},
|
||||||
|
EnergyUpdateTime: Ptr(int64(1234567900)),
|
||||||
|
CurrentWatts: Ptr(int32(190)),
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(orig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
var decoded ExtSensorsData
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if decoded.ConsumedEnergy == nil || decoded.ConsumedEnergy.Number == nil || *decoded.ConsumedEnergy.Number != 99999 {
|
||||||
|
t.Fatalf("consumed_energy mismatch: %v", decoded.ConsumedEnergy)
|
||||||
|
}
|
||||||
|
if decoded.Temperature == nil || decoded.Temperature.Number == nil || *decoded.Temperature.Number != 42 {
|
||||||
|
t.Fatalf("temperature mismatch: %v", decoded.Temperature)
|
||||||
|
}
|
||||||
|
if decoded.CurrentWatts == nil || *decoded.CurrentWatts != 190 {
|
||||||
|
t.Fatalf("current_watts mismatch: %v", decoded.CurrentWatts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPowerMgmtDataRoundTrip(t *testing.T) {
|
||||||
|
orig := PowerMgmtData{
|
||||||
|
MaximumWatts: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(400))},
|
||||||
|
CurrentWatts: Ptr(int32(220)),
|
||||||
|
TotalEnergy: Ptr(int64(500000)),
|
||||||
|
NewMaximumWatts: Ptr(int32(450)),
|
||||||
|
PeakWatts: Ptr(int32(380)),
|
||||||
|
LowestWatts: Ptr(int32(90)),
|
||||||
|
NewJobTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567999))},
|
||||||
|
State: Ptr(int32(1)),
|
||||||
|
TimeStartDay: Ptr(int64(1234500000)),
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(orig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
var decoded PowerMgmtData
|
||||||
|
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if decoded.MaximumWatts == nil || decoded.MaximumWatts.Number == nil || *decoded.MaximumWatts.Number != 400 {
|
||||||
|
t.Fatalf("maximum_watts mismatch: %v", decoded.MaximumWatts)
|
||||||
|
}
|
||||||
|
if decoded.CurrentWatts == nil || *decoded.CurrentWatts != 220 {
|
||||||
|
t.Fatalf("current_watts mismatch: %v", decoded.CurrentWatts)
|
||||||
|
}
|
||||||
|
if decoded.TotalEnergy == nil || *decoded.TotalEnergy != 500000 {
|
||||||
|
t.Fatalf("total_energy mismatch: %v", decoded.TotalEnergy)
|
||||||
|
}
|
||||||
|
if decoded.PeakWatts == nil || *decoded.PeakWatts != 380 {
|
||||||
|
t.Fatalf("peak_watts mismatch: %v", decoded.PeakWatts)
|
||||||
|
}
|
||||||
|
if decoded.NewJobTime == nil || decoded.NewJobTime.Number == nil || *decoded.NewJobTime.Number != 1234567999 {
|
||||||
|
t.Fatalf("new_job_time mismatch: %v", decoded.NewJobTime)
|
||||||
|
}
|
||||||
|
if decoded.State == nil || *decoded.State != 1 {
|
||||||
|
t.Fatalf("state mismatch: %v", decoded.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user