diff --git a/internal/slurm/slurm_jobs.go b/internal/slurm/slurm_jobs.go new file mode 100644 index 0000000..775afc6 --- /dev/null +++ b/internal/slurm/slurm_jobs.go @@ -0,0 +1,172 @@ +package slurm + +import ( + "context" + "fmt" + "net/url" +) + +// Job query flags for GetJobsOptions.Flags and GetNodeOptions.Flags. +const ( + JobFlagAll = "ALL" + JobFlagDetail = "DETAIL" + JobFlagMixed = "MIXED" + JobFlagLocal = "LOCAL" + JobFlagSibling = "SIBLING" + JobFlagFederation = "FEDERATION" + JobFlagFuture = "FUTURE" +) + +// GetJobsOptions specifies optional parameters for GetJobs. +type GetJobsOptions struct { + UpdateTime *string `url:"update_time,omitempty"` + Flags *string `url:"flags,omitempty"` // Use JobFlag* constants (e.g. JobFlagDetail) +} + +// Job signal flags for DeleteJobOptions.Flags. +const ( + JobSignalFlagBatchJob = "BATCH_JOB" + JobSignalFlagArrayTask = "ARRAY_TASK" + JobSignalFlagFullStepsOnly = "FULL_STEPS_ONLY" + JobSignalFlagFullJob = "FULL_JOB" + JobSignalFlagFederationRequeue = "FEDERATION_REQUEUE" + JobSignalFlagHurry = "HURRY" + JobSignalFlagOutOfMemory = "OUT_OF_MEMORY" + JobSignalFlagNoSiblingJobs = "NO_SIBLING_JOBS" + JobSignalFlagReservationJob = "RESERVATION_JOB" + JobSignalFlagWarningSent = "WARNING_SENT" +) + +// DeleteJobOptions specifies optional parameters for DeleteJob. +type DeleteJobOptions struct { + Signal *string `url:"signal,omitempty"` + Flags *string `url:"flags,omitempty"` // Use JobSignalFlag* constants (e.g. JobSignalFlagHurry) +} + +// GetJobs lists all jobs. +func (s *JobsService) GetJobs(ctx context.Context, opts *GetJobsOptions) (*OpenapiJobInfoResp, *Response, error) { + path := "slurm/v0.0.40/jobs" + 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 OpenapiJobInfoResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// GetJob gets a single job by ID. +func (s *JobsService) GetJob(ctx context.Context, jobID string, opts *GetJobsOptions) (*OpenapiJobInfoResp, *Response, error) { + path := fmt.Sprintf("slurm/v0.0.40/job/%s", jobID) + 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 OpenapiJobInfoResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// PostJob updates a job. +func (s *JobsService) PostJob(ctx context.Context, jobID string, desc *JobDescMsg) (*OpenapiJobPostResponse, *Response, error) { + path := fmt.Sprintf("slurm/v0.0.40/job/%s", jobID) + req, err := s.client.NewRequest("POST", path, desc) + if err != nil { + return nil, nil, err + } + + var result OpenapiJobPostResponse + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// DeleteJob cancels (signals) a job. +func (s *JobsService) DeleteJob(ctx context.Context, jobID string, opts *DeleteJobOptions) (*OpenapiResp, *Response, error) { + path := fmt.Sprintf("slurm/v0.0.40/job/%s", jobID) + req, err := s.client.NewRequest("DELETE", 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.Signal != nil { + q.Set("signal", *opts.Signal) + } + if opts.Flags != nil { + q.Set("flags", *opts.Flags) + } + u.RawQuery = q.Encode() + req.URL = u + } + + var result OpenapiResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// SubmitJob submits a new job. +func (s *JobsService) SubmitJob(ctx context.Context, reqBody *JobSubmitReq) (*OpenapiJobSubmitResponse, *Response, error) { + path := "slurm/v0.0.40/job/submit" + req, err := s.client.NewRequest("POST", path, reqBody) + if err != nil { + return nil, nil, err + } + + var result OpenapiJobSubmitResponse + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} diff --git a/internal/slurm/slurm_jobs_test.go b/internal/slurm/slurm_jobs_test.go new file mode 100644 index 0000000..a25125b --- /dev/null +++ b/internal/slurm/slurm_jobs_test.go @@ -0,0 +1,286 @@ +package slurm + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" +) + +func testMethod(t *testing.T, r *http.Request, expected string) { + t.Helper() + if r.Method != expected { + t.Errorf("expected method %s, got %s", expected, r.Method) + } +} + +func TestJobsService_GetJobs(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/jobs", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"jobs": [], "last_backfill": {}, "last_update": {}}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.Jobs.GetJobs(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if len(resp.Jobs) != 0 { + t.Errorf("expected empty jobs, got %d", len(resp.Jobs)) + } +} + +func TestJobsService_GetJobs_WithOptions(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/jobs", 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") != JobFlagDetail { + t.Errorf("expected flags=%s, got %s", JobFlagDetail, q.Get("flags")) + } + fmt.Fprint(w, `{"jobs": [], "last_update": {}}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &GetJobsOptions{ + UpdateTime: Ptr("12345"), + Flags: Ptr(JobFlagDetail), + } + resp, _, err := client.Jobs.GetJobs(context.Background(), opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestJobsService_GetJob(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/job/42", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"jobs": [{"job_id": 42, "name": "test-job"}], "last_update": {}}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.Jobs.GetJob(context.Background(), "42", nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if len(resp.Jobs) != 1 { + t.Fatalf("expected 1 job, got %d", len(resp.Jobs)) + } + if resp.Jobs[0].JobID == nil || *resp.Jobs[0].JobID != 42 { + t.Errorf("expected job_id=42, got %v", resp.Jobs[0].JobID) + } +} + +func TestJobsService_PostJob(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/job/42", 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, `{"job_id": "42", "step_id": "0", "results": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + desc := &JobDescMsg{ + Name: Ptr("updated-job"), + } + resp, _, err := client.Jobs.PostJob(context.Background(), "42", desc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if resp.JobID == nil || *resp.JobID != "42" { + t.Errorf("expected job_id=42, got %v", resp.JobID) + } +} + +func TestJobsService_DeleteJob(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/job/42", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + q := r.URL.Query() + if q.Get("signal") != "SIGTERM" { + t.Errorf("expected signal=SIGTERM, got %s", q.Get("signal")) + } + fmt.Fprint(w, `{}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &DeleteJobOptions{ + Signal: Ptr("SIGTERM"), + } + resp, _, err := client.Jobs.DeleteJob(context.Background(), "42", opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestJobsService_SubmitJob(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/job/submit", 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, `{"job_id": 100, "step_id": "0", "result": {"job_id": 100, "step_id": "0"}}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + reqBody := &JobSubmitReq{ + Script: Ptr("#!/bin/bash\necho hello"), + Job: &JobDescMsg{ + Name: Ptr("test-submit"), + }, + } + resp, _, err := client.Jobs.SubmitJob(context.Background(), reqBody) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if resp.JobID == nil || *resp.JobID != 100 { + t.Errorf("expected job_id=100, got %v", resp.JobID) + } +} + +func TestJobsService_GetJobs_Error(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/jobs", 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.Jobs.GetJobs(context.Background(), nil) + if err == nil { + t.Fatal("expected error for 500 response") + } + if !strings.Contains(err.Error(), "500") { + t.Errorf("expected error to contain 500, got %v", err) + } +} + +func TestJobsService_GetJob_Error(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/job/999", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + fmt.Fprint(w, `{"errors": [{"error": "job not found"}]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + _, _, err := client.Jobs.GetJob(context.Background(), "999", nil) + if err == nil { + t.Fatal("expected error for 404 response") + } +} + +func TestJobsService_DeleteJob_NoOptions(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/job/42", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + q := r.URL.Query() + if len(q) != 0 { + t.Errorf("expected no query params, got %v", q) + } + fmt.Fprint(w, `{}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.Jobs.DeleteJob(context.Background(), "42", nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestJobsService_GetJobPathEscaping(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/job/", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + parts := strings.Split(r.URL.Path, "/") + jobID := parts[len(parts)-1] + if jobID != "123" { + t.Errorf("expected job ID 123, got %s", jobID) + } + fmt.Fprint(w, `{"jobs": [{"job_id": 123}]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.Jobs.GetJob(context.Background(), "123", nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestJobsService_GetJobsWithURLQuery(t *testing.T) { + var capturedQuery url.Values + mux := http.NewServeMux() + mux.HandleFunc("/slurm/v0.0.40/jobs", func(w http.ResponseWriter, r *http.Request) { + capturedQuery = r.URL.Query() + testMethod(t, r, "GET") + fmt.Fprint(w, `{"jobs": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &GetJobsOptions{ + UpdateTime: Ptr("1700000000"), + } + _, _, err := client.Jobs.GetJobs(context.Background(), opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if capturedQuery.Get("update_time") != "1700000000" { + t.Errorf("expected update_time=1700000000, got %s", capturedQuery.Get("update_time")) + } +} diff --git a/internal/slurm/types_job.go b/internal/slurm/types_job.go new file mode 100644 index 0000000..dda39b5 --- /dev/null +++ b/internal/slurm/types_job.go @@ -0,0 +1,1149 @@ +package slurm + +// --------------------------------------------------------------------------- +// Core Job types — v0.0.40 OpenAPI job schemas +// All fields are optional (pointer types) unless otherwise noted. +// JSON tags use snake_case with omitempty per spec convention. +// --------------------------------------------------------------------------- + +// JobInfo represents a Slurm job with full detail (~100+ fields). +// Corresponds to v0.0.40_job_info. +type JobInfo struct { + Account *string `json:"account,omitempty"` + AccrueTime *Uint64NoVal `json:"accrue_time,omitempty"` + AdminComment *string `json:"admin_comment,omitempty"` + AllocatingNode *string `json:"allocating_node,omitempty"` + ArrayJobID *Uint32NoVal `json:"array_job_id,omitempty"` + ArrayTaskID *Uint32NoVal `json:"array_task_id,omitempty"` + ArrayMaxTasks *Uint32NoVal `json:"array_max_tasks,omitempty"` + ArrayTaskString *string `json:"array_task_string,omitempty"` + AssociationID *int32 `json:"association_id,omitempty"` + BatchFeatures *string `json:"batch_features,omitempty"` + BatchFlag *bool `json:"batch_flag,omitempty"` + BatchHost *string `json:"batch_host,omitempty"` + Flags []string `json:"flags,omitempty"` + BurstBuffer *string `json:"burst_buffer,omitempty"` + BurstBufferState *string `json:"burst_buffer_state,omitempty"` + Cluster *string `json:"cluster,omitempty"` + ClusterFeatures *string `json:"cluster_features,omitempty"` + Command *string `json:"command,omitempty"` + Comment *string `json:"comment,omitempty"` + Container *string `json:"container,omitempty"` + ContainerID *string `json:"container_id,omitempty"` + Contiguous *bool `json:"contiguous,omitempty"` + CoreSpec *int32 `json:"core_spec,omitempty"` + ThreadSpec *int32 `json:"thread_spec,omitempty"` + CoresPerSocket *Uint16NoVal `json:"cores_per_socket,omitempty"` + BillableTres *Float64NoVal `json:"billable_tres,omitempty"` + CpusPerTask *Uint16NoVal `json:"cpus_per_task,omitempty"` + CpuFrequencyMinimum *Uint32NoVal `json:"cpu_frequency_minimum,omitempty"` + CpuFrequencyMaximum *Uint32NoVal `json:"cpu_frequency_maximum,omitempty"` + CpuFrequencyGovernor *Uint32NoVal `json:"cpu_frequency_governor,omitempty"` + CpusPerTres *string `json:"cpus_per_tres,omitempty"` + Cron *string `json:"cron,omitempty"` + Deadline *Uint64NoVal `json:"deadline,omitempty"` + DelayBoot *Uint32NoVal `json:"delay_boot,omitempty"` + Dependency *string `json:"dependency,omitempty"` + DerivedExitCode *ProcessExitCodeVerbose `json:"derived_exit_code,omitempty"` + EligibleTime *Uint64NoVal `json:"eligible_time,omitempty"` + EndTime *Uint64NoVal `json:"end_time,omitempty"` + ExcludedNodes *string `json:"excluded_nodes,omitempty"` + ExitCode *ProcessExitCodeVerbose `json:"exit_code,omitempty"` + Extra *string `json:"extra,omitempty"` + FailedNode *string `json:"failed_node,omitempty"` + Features *string `json:"features,omitempty"` + FederationOrigin *string `json:"federation_origin,omitempty"` + FederationSiblingsActive *string `json:"federation_siblings_active,omitempty"` + FederationSiblingsViable *string `json:"federation_siblings_viable,omitempty"` + GresDetail JobInfoGresDetail `json:"gres_detail,omitempty"` + GroupID *int32 `json:"group_id,omitempty"` + GroupName *string `json:"group_name,omitempty"` + HetJobID *Uint32NoVal `json:"het_job_id,omitempty"` + HetJobIDSet *string `json:"het_job_id_set,omitempty"` + HetJobOffset *Uint32NoVal `json:"het_job_offset,omitempty"` + JobID *int32 `json:"job_id,omitempty"` + JobResources *JobRes `json:"job_resources,omitempty"` + JobSizeStr CSVString `json:"job_size_str,omitempty"` + JobState []string `json:"job_state,omitempty"` + LastSchedEvaluation *Uint64NoVal `json:"last_sched_evaluation,omitempty"` + Licenses *string `json:"licenses,omitempty"` + MailType []string `json:"mail_type,omitempty"` + MailUser *string `json:"mail_user,omitempty"` + MaxCpus *Uint32NoVal `json:"max_cpus,omitempty"` + MaxNodes *Uint32NoVal `json:"max_nodes,omitempty"` + McsLabel *string `json:"mcs_label,omitempty"` + MemoryPerTres *string `json:"memory_per_tres,omitempty"` + Name *string `json:"name,omitempty"` + Network *string `json:"network,omitempty"` + Nodes *string `json:"nodes,omitempty"` + Nice *int32 `json:"nice,omitempty"` + TasksPerCore *Uint16NoVal `json:"tasks_per_core,omitempty"` + TasksPerTres *Uint16NoVal `json:"tasks_per_tres,omitempty"` + TasksPerNode *Uint16NoVal `json:"tasks_per_node,omitempty"` + TasksPerSocket *Uint16NoVal `json:"tasks_per_socket,omitempty"` + TasksPerBoard *Uint16NoVal `json:"tasks_per_board,omitempty"` + Cpus *Uint32NoVal `json:"cpus,omitempty"` + NodeCount *Uint32NoVal `json:"node_count,omitempty"` + Tasks *Uint32NoVal `json:"tasks,omitempty"` + Partition *string `json:"partition,omitempty"` + Prefer *string `json:"prefer,omitempty"` + MemoryPerCpu *Uint64NoVal `json:"memory_per_cpu,omitempty"` + MemoryPerNode *Uint64NoVal `json:"memory_per_node,omitempty"` + MinimumCpusPerNode *Uint16NoVal `json:"minimum_cpus_per_node,omitempty"` + MinimumTmpDiskPerNode *Uint32NoVal `json:"minimum_tmp_disk_per_node,omitempty"` + Power *JobInfoPower `json:"power,omitempty"` + PreemptTime *Uint64NoVal `json:"preempt_time,omitempty"` + PreemptableTime *Uint64NoVal `json:"preemptable_time,omitempty"` + PreSusTime *Uint64NoVal `json:"pre_sus_time,omitempty"` + Priority *Uint32NoVal `json:"priority,omitempty"` + Profile []string `json:"profile,omitempty"` + Qos *string `json:"qos,omitempty"` + Reboot *bool `json:"reboot,omitempty"` + RequiredNodes *string `json:"required_nodes,omitempty"` + MinimumSwitches *int32 `json:"minimum_switches,omitempty"` + Requeue *bool `json:"requeue,omitempty"` + ResizeTime *Uint64NoVal `json:"resize_time,omitempty"` + RestartCnt *int32 `json:"restart_cnt,omitempty"` + ResvName *string `json:"resv_name,omitempty"` + ScheduledNodes *string `json:"scheduled_nodes,omitempty"` + SelinuxContext *string `json:"selinux_context,omitempty"` + Shared []string `json:"shared,omitempty"` + Exclusive []string `json:"exclusive,omitempty"` + Oversubscribe *bool `json:"oversubscribe,omitempty"` + ShowFlags []string `json:"show_flags,omitempty"` + SocketsPerBoard *int32 `json:"sockets_per_board,omitempty"` + SocketsPerNode *Uint16NoVal `json:"sockets_per_node,omitempty"` + StartTime *Uint64NoVal `json:"start_time,omitempty"` + StateDescription *string `json:"state_description,omitempty"` + StateReason *string `json:"state_reason,omitempty"` + StandardError *string `json:"standard_error,omitempty"` + StandardInput *string `json:"standard_input,omitempty"` + StandardOutput *string `json:"standard_output,omitempty"` + SubmitTime *Uint64NoVal `json:"submit_time,omitempty"` + SuspendTime *Uint64NoVal `json:"suspend_time,omitempty"` + SystemComment *string `json:"system_comment,omitempty"` + TimeLimit *Uint32NoVal `json:"time_limit,omitempty"` + TimeMinimum *Uint32NoVal `json:"time_minimum,omitempty"` + ThreadsPerCore *Uint16NoVal `json:"threads_per_core,omitempty"` + TresBind *string `json:"tres_bind,omitempty"` + TresFreq *string `json:"tres_freq,omitempty"` + TresPerJob *string `json:"tres_per_job,omitempty"` + TresPerNode *string `json:"tres_per_node,omitempty"` + TresPerSocket *string `json:"tres_per_socket,omitempty"` + TresPerTask *string `json:"tres_per_task,omitempty"` + TresReqStr *string `json:"tres_req_str,omitempty"` + TresAllocStr *string `json:"tres_alloc_str,omitempty"` + UserID *int32 `json:"user_id,omitempty"` + UserName *string `json:"user_name,omitempty"` + MaximumSwitchWaitTime *int32 `json:"maximum_switch_wait_time,omitempty"` + Wckey *string `json:"wckey,omitempty"` + CurrentWorkingDirectory *string `json:"current_working_directory,omitempty"` +} + +// JobInfoPower represents the power settings of a job. +type JobInfoPower struct { + Flags []string `json:"flags,omitempty"` +} + +// JobInfoGresDetail is a list of GRES detail strings. +// Corresponds to v0.0.40_job_info_gres_detail. +type JobInfoGresDetail []string + +// JobInfoMsg is a list of JobInfo objects. +// Corresponds to v0.0.40_job_info_msg. +type JobInfoMsg []JobInfo + +// JobRes represents job resource allocation. +// Corresponds to v0.0.40_job_res. +type JobRes struct { + Nodes *string `json:"nodes,omitempty"` + AllocatedCores *int32 `json:"allocated_cores,omitempty"` + AllocatedCpus *int32 `json:"allocated_cpus,omitempty"` + AllocatedHosts *int32 `json:"allocated_hosts,omitempty"` + AllocatedNodes JobResNodes `json:"allocated_nodes,omitempty"` +} + +// JobResNodes is an array of job node resources. +// Corresponds to v0.0.40_job_res_nodes. +type JobResNodes []interface{} + +// CronEntry represents a crontab entry for a job. +// Corresponds to v0.0.40_cron_entry. +type CronEntry struct { + Flags []string `json:"flags,omitempty"` + Minute *string `json:"minute,omitempty"` + Hour *string `json:"hour,omitempty"` + DayOfMonth *string `json:"day_of_month,omitempty"` + Month *string `json:"month,omitempty"` + DayOfWeek *string `json:"day_of_week,omitempty"` + Specification *string `json:"specification,omitempty"` + Command *string `json:"command,omitempty"` + Line *CronEntryLine `json:"line,omitempty"` +} + +// CronEntryLine represents line numbers in a crontab entry. +type CronEntryLine struct { + Start *int32 `json:"start,omitempty"` + End *int32 `json:"end,omitempty"` +} + +// --------------------------------------------------------------------------- +// Job Description (POST/submit) types +// --------------------------------------------------------------------------- + +// JobDescMsg represents a job description for submission or update. +// Corresponds to v0.0.40_job_desc_msg (~80+ fields). +type JobDescMsg struct { + Account *string `json:"account,omitempty"` + AccountGatherFrequency *string `json:"account_gather_frequency,omitempty"` + AdminComment *string `json:"admin_comment,omitempty"` + AllocationNodeList *string `json:"allocation_node_list,omitempty"` + AllocationNodePort *int32 `json:"allocation_node_port,omitempty"` + Argv StringArray `json:"argv,omitempty"` + Array *string `json:"array,omitempty"` + BatchFeatures *string `json:"batch_features,omitempty"` + BeginTime *Uint64NoVal `json:"begin_time,omitempty"` + Flags []string `json:"flags,omitempty"` + BurstBuffer *string `json:"burst_buffer,omitempty"` + Clusters *string `json:"clusters,omitempty"` + ClusterConstraint *string `json:"cluster_constraint,omitempty"` + Comment *string `json:"comment,omitempty"` + Contiguous *bool `json:"contiguous,omitempty"` + Container *string `json:"container,omitempty"` + ContainerID *string `json:"container_id,omitempty"` + CoreSpecification *int32 `json:"core_specification,omitempty"` + ThreadSpecification *int32 `json:"thread_specification,omitempty"` + CpuBinding *string `json:"cpu_binding,omitempty"` + CpuBindingFlags []string `json:"cpu_binding_flags,omitempty"` + CpuFrequency *string `json:"cpu_frequency,omitempty"` + CpusPerTres *string `json:"cpus_per_tres,omitempty"` + Crontab *CronEntry `json:"crontab,omitempty"` + Deadline *int64 `json:"deadline,omitempty"` + DelayBoot *int32 `json:"delay_boot,omitempty"` + Dependency *string `json:"dependency,omitempty"` + EndTime *int64 `json:"end_time,omitempty"` + Environment StringArray `json:"environment,omitempty"` + Rlimits *JobDescRlimits `json:"rlimits,omitempty"` + ExcludedNodes CSVString `json:"excluded_nodes,omitempty"` + Extra *string `json:"extra,omitempty"` + Constraints *string `json:"constraints,omitempty"` + GroupID *string `json:"group_id,omitempty"` + HetjobGroup *int32 `json:"hetjob_group,omitempty"` + Immediate *bool `json:"immediate,omitempty"` + JobID *int32 `json:"job_id,omitempty"` + KillOnNodeFail *bool `json:"kill_on_node_fail,omitempty"` + Licenses *string `json:"licenses,omitempty"` + MailType []string `json:"mail_type,omitempty"` + MailUser *string `json:"mail_user,omitempty"` + McsLabel *string `json:"mcs_label,omitempty"` + MemoryBinding *string `json:"memory_binding,omitempty"` + MemoryBindingType []string `json:"memory_binding_type,omitempty"` + MemoryPerTres *string `json:"memory_per_tres,omitempty"` + Name *string `json:"name,omitempty"` + Network *string `json:"network,omitempty"` + Nice *int32 `json:"nice,omitempty"` + Tasks *int32 `json:"tasks,omitempty"` + OpenMode []string `json:"open_mode,omitempty"` + ReservePorts *int32 `json:"reserve_ports,omitempty"` + Overcommit *bool `json:"overcommit,omitempty"` + Partition *string `json:"partition,omitempty"` + DistributionPlaneSize *int32 `json:"distribution_plane_size,omitempty"` + PowerFlags []string `json:"power_flags,omitempty"` + Prefer *string `json:"prefer,omitempty"` + Priority *Uint32NoVal `json:"priority,omitempty"` + Profile []string `json:"profile,omitempty"` + Qos *string `json:"qos,omitempty"` + Reboot *bool `json:"reboot,omitempty"` + RequiredNodes CSVString `json:"required_nodes,omitempty"` + Requeue *bool `json:"requeue,omitempty"` + Reservation *string `json:"reservation,omitempty"` + Script *string `json:"script,omitempty"` + Shared []string `json:"shared,omitempty"` + Exclusive []string `json:"exclusive,omitempty"` + Oversubscribe *bool `json:"oversubscribe,omitempty"` + SiteFactor *int32 `json:"site_factor,omitempty"` + SpankEnvironment StringArray `json:"spank_environment,omitempty"` + Distribution *string `json:"distribution,omitempty"` + TimeLimit *Uint32NoVal `json:"time_limit,omitempty"` + TimeMinimum *Uint32NoVal `json:"time_minimum,omitempty"` + TresBind *string `json:"tres_bind,omitempty"` + TresFreq *string `json:"tres_freq,omitempty"` + TresPerJob *string `json:"tres_per_job,omitempty"` + TresPerNode *string `json:"tres_per_node,omitempty"` + TresPerSocket *string `json:"tres_per_socket,omitempty"` + TresPerTask *string `json:"tres_per_task,omitempty"` + UserID *string `json:"user_id,omitempty"` + WaitAllNodes *bool `json:"wait_all_nodes,omitempty"` + KillWarningFlags []string `json:"kill_warning_flags,omitempty"` + KillWarningSignal *string `json:"kill_warning_signal,omitempty"` + KillWarningDelay *Uint16NoVal `json:"kill_warning_delay,omitempty"` + CurrentWorkingDirectory *string `json:"current_working_directory,omitempty"` + CpusPerTask *int32 `json:"cpus_per_task,omitempty"` + MinimumCpus *int32 `json:"minimum_cpus,omitempty"` + MaximumCpus *int32 `json:"maximum_cpus,omitempty"` + Nodes *string `json:"nodes,omitempty"` + MinimumNodes *int32 `json:"minimum_nodes,omitempty"` + MaximumNodes *int32 `json:"maximum_nodes,omitempty"` + MinimumBoardsPerNode *int32 `json:"minimum_boards_per_node,omitempty"` + MinimumSocketsPerBoard *int32 `json:"minimum_sockets_per_board,omitempty"` + SocketsPerNode *int32 `json:"sockets_per_node,omitempty"` + ThreadsPerCore *int32 `json:"threads_per_core,omitempty"` + TasksPerNode *int32 `json:"tasks_per_node,omitempty"` + TasksPerSocket *int32 `json:"tasks_per_socket,omitempty"` + TasksPerCore *int32 `json:"tasks_per_core,omitempty"` + TasksPerBoard *int32 `json:"tasks_per_board,omitempty"` + NtasksPerTres *int32 `json:"ntasks_per_tres,omitempty"` + MinimumCpusPerNode *int32 `json:"minimum_cpus_per_node,omitempty"` + MemoryPerCpu *Uint64NoVal `json:"memory_per_cpu,omitempty"` + MemoryPerNode *Uint64NoVal `json:"memory_per_node,omitempty"` + TemporaryDiskPerNode *int32 `json:"temporary_disk_per_node,omitempty"` + SelinuxContext *string `json:"selinux_context,omitempty"` + RequiredSwitches *Uint32NoVal `json:"required_switches,omitempty"` + StandardError *string `json:"standard_error,omitempty"` + StandardInput *string `json:"standard_input,omitempty"` + StandardOutput *string `json:"standard_output,omitempty"` + WaitForSwitch *int32 `json:"wait_for_switch,omitempty"` + Wckey *string `json:"wckey,omitempty"` + X11 []string `json:"x11,omitempty"` + X11MagicCookie *string `json:"x11_magic_cookie,omitempty"` + X11TargetHost *string `json:"x11_target_host,omitempty"` + X11TargetPort *int32 `json:"x11_target_port,omitempty"` +} + +// JobDescRlimits represents resource limits for a job description. +type JobDescRlimits struct { + Cpu *Uint64NoVal `json:"cpu,omitempty"` + Fsize *Uint64NoVal `json:"fsize,omitempty"` + Data *Uint64NoVal `json:"data,omitempty"` + Stack *Uint64NoVal `json:"stack,omitempty"` + Core *Uint64NoVal `json:"core,omitempty"` + Rss *Uint64NoVal `json:"rss,omitempty"` + Nproc *Uint64NoVal `json:"nproc,omitempty"` + Nofile *Uint64NoVal `json:"nofile,omitempty"` + Memlock *Uint64NoVal `json:"memlock,omitempty"` + As *Uint64NoVal `json:"as,omitempty"` +} + +// JobDescMsgList is a list of JobDescMsg objects. +// Corresponds to v0.0.40_job_desc_msg_list. +type JobDescMsgList []JobDescMsg + +// JobSubmitReq represents a job submit request. +// Corresponds to v0.0.40_job_submit_req. +type JobSubmitReq struct { + Script *string `json:"script,omitempty"` + Jobs JobDescMsgList `json:"jobs,omitempty"` + Job *JobDescMsg `json:"job,omitempty"` +} + +// JobSubmitResponseMsg represents the response from a job submission. +// Corresponds to v0.0.40_job_submit_response_msg. +type JobSubmitResponseMsg struct { + JobID *int32 `json:"job_id,omitempty"` + StepID *string `json:"step_id,omitempty"` + ErrorCode *int32 `json:"error_code,omitempty"` + Error *string `json:"error,omitempty"` + JobSubmitUserMsg *string `json:"job_submit_user_msg,omitempty"` +} + +// JobArrayResponseMsgEntry represents a single entry in a job array response. +// Corresponds to v0.0.40_job_array_response_msg_entry. +type JobArrayResponseMsgEntry struct { + JobID *int32 `json:"job_id,omitempty"` + StepID *string `json:"step_id,omitempty"` + Error *string `json:"error,omitempty"` + ErrorCode *int32 `json:"error_code,omitempty"` + Why *string `json:"why,omitempty"` +} + +// JobArrayResponseArray is a list of job array response entries. +// Corresponds to v0.0.40_job_array_response_array. +type JobArrayResponseArray []JobArrayResponseMsgEntry + +// --------------------------------------------------------------------------- +// Response wrapper types +// --------------------------------------------------------------------------- + +// OpenapiJobInfoResp is the response for listing jobs. +// Corresponds to v0.0.40_openapi_job_info_resp. +type OpenapiJobInfoResp struct { + Jobs JobInfoMsg `json:"jobs,omitempty"` + LastBackfill *Uint64NoVal `json:"last_backfill,omitempty"` + LastUpdate *Uint64NoVal `json:"last_update,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiJobPostResponse is the response for a job POST (update). +// Corresponds to v0.0.40_openapi_job_post_response. +type OpenapiJobPostResponse struct { + Results JobArrayResponseArray `json:"results,omitempty"` + JobID *string `json:"job_id,omitempty"` + StepID *string `json:"step_id,omitempty"` + JobSubmitUserMsg *string `json:"job_submit_user_msg,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiJobSubmitResponse is the response for a job submit. +// Corresponds to v0.0.40_openapi_job_submit_response. +type OpenapiJobSubmitResponse struct { + Result *JobSubmitResponseMsg `json:"result,omitempty"` + JobID *int32 `json:"job_id,omitempty"` + StepID *string `json:"step_id,omitempty"` + JobSubmitUserMsg *string `json:"job_submit_user_msg,omitempty"` + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// OpenapiResp is a generic response with only meta, errors, and warnings. +// Corresponds to v0.0.40_openapi_resp. +type OpenapiResp struct { + Meta *OpenapiMeta `json:"meta,omitempty"` + Errors OpenapiErrors `json:"errors,omitempty"` + Warnings OpenapiWarnings `json:"warnings,omitempty"` +} + +// --------------------------------------------------------------------------- +// SlurmDBD Job types — v0.0.40_job and dependencies +// --------------------------------------------------------------------------- + +// Job represents a SlurmDBD job record. +// Corresponds to v0.0.40_job. +type Job struct { + Account *string `json:"account,omitempty"` + Comment *JobComment `json:"comment,omitempty"` + AllocationNodes *int32 `json:"allocation_nodes,omitempty"` + Array *JobArray `json:"array,omitempty"` + Association *AssocShort `json:"association,omitempty"` + Block *string `json:"block,omitempty"` + Cluster *string `json:"cluster,omitempty"` + Constraints *string `json:"constraints,omitempty"` + Container *string `json:"container,omitempty"` + DerivedExitCode *ProcessExitCodeVerbose `json:"derived_exit_code,omitempty"` + Time *JobTime `json:"time,omitempty"` + ExitCode *ProcessExitCodeVerbose `json:"exit_code,omitempty"` + Extra *string `json:"extra,omitempty"` + FailedNode *string `json:"failed_node,omitempty"` + Flags []string `json:"flags,omitempty"` + Group *string `json:"group,omitempty"` + Het *JobHet `json:"het,omitempty"` + JobID *int32 `json:"job_id,omitempty"` + Name *string `json:"name,omitempty"` + Licenses *string `json:"licenses,omitempty"` + Mcs *JobMcs `json:"mcs,omitempty"` + Nodes *string `json:"nodes,omitempty"` + Partition *string `json:"partition,omitempty"` + Priority *Uint32NoVal `json:"priority,omitempty"` + Qos *string `json:"qos,omitempty"` + Required *JobRequired `json:"required,omitempty"` + KillRequestUser *string `json:"kill_request_user,omitempty"` + Reservation *JobReservation `json:"reservation,omitempty"` + Script *string `json:"script,omitempty"` + State *JobState `json:"state,omitempty"` + Steps StepList `json:"steps,omitempty"` + SubmitLine *string `json:"submit_line,omitempty"` + Tres *JobTres `json:"tres,omitempty"` + UsedGres *string `json:"used_gres,omitempty"` + User *string `json:"user,omitempty"` + Wckey *WckeyTagStruct `json:"wckey,omitempty"` + WorkingDirectory *string `json:"working_directory,omitempty"` +} + +// JobComment represents the comment sub-object in a SlurmDBD Job. +type JobComment struct { + Administrator *string `json:"administrator,omitempty"` + Job *string `json:"job,omitempty"` + System *string `json:"system,omitempty"` +} + +// JobArray represents the array sub-object in a SlurmDBD Job. +type JobArray struct { + JobID *int32 `json:"job_id,omitempty"` + Limits *JobArrayLimits `json:"limits,omitempty"` + TaskID *Uint32NoVal `json:"task_id,omitempty"` + Task *string `json:"task,omitempty"` +} + +// JobArrayLimits represents limits within a job array. +type JobArrayLimits struct { + Max *JobArrayLimitsMax `json:"max,omitempty"` +} + +// JobArrayLimitsMax represents max limits within a job array. +type JobArrayLimitsMax struct { + Running *JobArrayLimitsMaxRunning `json:"running,omitempty"` +} + +// JobArrayLimitsMaxRunning represents running task limits. +type JobArrayLimitsMaxRunning struct { + Tasks *int32 `json:"tasks,omitempty"` +} + +// JobTime represents the time sub-object in a SlurmDBD Job. +type JobTime struct { + Elapsed *int32 `json:"elapsed,omitempty"` + Eligible *int64 `json:"eligible,omitempty"` + End *int64 `json:"end,omitempty"` + Start *int64 `json:"start,omitempty"` + Submission *int64 `json:"submission,omitempty"` + Suspended *int32 `json:"suspended,omitempty"` + System *JobTimeCpu `json:"system,omitempty"` + Limit *Uint32NoVal `json:"limit,omitempty"` + Total *JobTimeCpu `json:"total,omitempty"` + User *JobTimeCpu `json:"user,omitempty"` +} + +// JobTimeCpu represents CPU time with seconds and microseconds. +type JobTimeCpu struct { + Seconds *int64 `json:"seconds,omitempty"` + Microseconds *int64 `json:"microseconds,omitempty"` +} + +// JobHet represents the heterogeneous job sub-object. +type JobHet struct { + JobID *int32 `json:"job_id,omitempty"` + JobOffset *Uint32NoVal `json:"job_offset,omitempty"` +} + +// JobMcs represents the MCS label sub-object. +type JobMcs struct { + Label *string `json:"label,omitempty"` +} + +// JobRequired represents the required resources sub-object. +type JobRequired struct { + CPUs *int32 `json:"CPUs,omitempty"` + MemoryPerCpu *Uint64NoVal `json:"memory_per_cpu,omitempty"` + MemoryPerNode *Uint64NoVal `json:"memory_per_node,omitempty"` +} + +// JobReservation represents the reservation sub-object. +type JobReservation struct { + ID *int32 `json:"id,omitempty"` + Name *string `json:"name,omitempty"` +} + +// JobState represents the state sub-object in a SlurmDBD Job. +type JobState struct { + Current []string `json:"current,omitempty"` + Reason *string `json:"reason,omitempty"` +} + +// JobTres represents the TRES sub-object in a SlurmDBD Job. +type JobTres struct { + Allocated TresList `json:"allocated,omitempty"` + Requested TresList `json:"requested,omitempty"` +} + +// JobList is a list of SlurmDBD Job objects. +// Corresponds to v0.0.40_job_list. +type JobList []Job + +// --------------------------------------------------------------------------- +// AssocShort and related types +// --------------------------------------------------------------------------- + +// AssocShort represents a short association reference. +// Corresponds to v0.0.40_assoc_short. +type AssocShort struct { + Account *string `json:"account,omitempty"` + Cluster *string `json:"cluster,omitempty"` + Partition *string `json:"partition,omitempty"` + User *string `json:"user,omitempty"` + ID *int32 `json:"id,omitempty"` +} + +// AssocShortList is a list of AssocShort. +// Corresponds to v0.0.40_assoc_short_list. +type AssocShortList []AssocShort + +// --------------------------------------------------------------------------- +// TRES (Trackable Resources) types +// --------------------------------------------------------------------------- + +// Tres represents a trackable resource entry. +// Corresponds to v0.0.40_tres. +type Tres struct { + Type *string `json:"type,omitempty"` + Name *string `json:"name,omitempty"` + ID *int32 `json:"id,omitempty"` + Count *int64 `json:"count,omitempty"` +} + +// TresList is a list of Tres objects. +// Corresponds to v0.0.40_tres_list. +type TresList []Tres + +// StepTresReqMax is a list of Tres for step request max. +// Corresponds to v0.0.40_step_tres_req_max. +type StepTresReqMax []Tres + +// StepTresReqMin is a list of Tres for step request min. +// Corresponds to v0.0.40_step_tres_req_min. +type StepTresReqMin []Tres + +// StepTresUsageMax is a list of Tres for step usage max. +// Corresponds to v0.0.40_step_tres_usage_max. +type StepTresUsageMax []Tres + +// StepTresUsageMin is a list of Tres for step usage min. +// Corresponds to v0.0.40_step_tres_usage_min. +type StepTresUsageMin []Tres + +// --------------------------------------------------------------------------- +// Step types +// --------------------------------------------------------------------------- + +// Step represents a job step with full detail. +// Corresponds to v0.0.40_step. +type Step struct { + Time *StepTime `json:"time,omitempty"` + ExitCode *ProcessExitCodeVerbose `json:"exit_code,omitempty"` + Nodes *StepNodes `json:"nodes,omitempty"` + Tasks *StepTasks `json:"tasks,omitempty"` + Pid *string `json:"pid,omitempty"` + CPU *StepCPU `json:"CPU,omitempty"` + KillRequestUser *string `json:"kill_request_user,omitempty"` + State []string `json:"state,omitempty"` + Statistics *StepStatistics `json:"statistics,omitempty"` + Step *StepIDName `json:"step,omitempty"` + Task *StepTask `json:"task,omitempty"` + Tres *StepTres `json:"tres,omitempty"` +} + +// StepTime represents timing information for a step. +type StepTime struct { + Elapsed *int32 `json:"elapsed,omitempty"` + End *Uint64NoVal `json:"end,omitempty"` + Start *Uint64NoVal `json:"start,omitempty"` + Suspended *int32 `json:"suspended,omitempty"` + System *StepTimeCpu `json:"system,omitempty"` + Total *StepTimeCpu `json:"total,omitempty"` + User *StepTimeCpu `json:"user,omitempty"` +} + +// StepTimeCpu represents CPU time for a step (seconds + microseconds). +type StepTimeCpu struct { + Seconds *int64 `json:"seconds,omitempty"` + Microseconds *int32 `json:"microseconds,omitempty"` +} + +// StepNodes represents node information for a step. +type StepNodes struct { + Count *int32 `json:"count,omitempty"` + Range *string `json:"range,omitempty"` + List Hostlist `json:"list,omitempty"` +} + +// StepTasks represents task information for a step. +type StepTasks struct { + Count *int32 `json:"count,omitempty"` +} + +// StepCPU represents CPU configuration for a step. +type StepCPU struct { + RequestedFrequency *StepCPURequestedFrequency `json:"requested_frequency,omitempty"` + Governor *string `json:"governor,omitempty"` +} + +// StepCPURequestedFrequency represents CPU frequency range. +type StepCPURequestedFrequency struct { + Min *Uint32NoVal `json:"min,omitempty"` + Max *Uint32NoVal `json:"max,omitempty"` +} + +// StepStatistics represents statistics for a step. +type StepStatistics struct { + CPU *StepStatisticsCPU `json:"CPU,omitempty"` + Energy *StepStatisticsEnergy `json:"energy,omitempty"` +} + +// StepStatisticsCPU represents CPU statistics. +type StepStatisticsCPU struct { + ActualFrequency *int64 `json:"actual_frequency,omitempty"` +} + +// StepStatisticsEnergy represents energy statistics. +type StepStatisticsEnergy struct { + Consumed *Uint64NoVal `json:"consumed,omitempty"` +} + +// StepIDName represents the step identification. +type StepIDName struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` +} + +// StepTask represents task distribution for a step. +type StepTask struct { + Distribution *string `json:"distribution,omitempty"` +} + +// StepTres represents TRES information for a step. +type StepTres struct { + Requested *StepTresRequested `json:"requested,omitempty"` + Consumed *StepTresConsumed `json:"consumed,omitempty"` + Allocated TresList `json:"allocated,omitempty"` +} + +// StepTresRequested represents requested TRES for a step. +type StepTresRequested struct { + Max StepTresReqMax `json:"max,omitempty"` + Min StepTresReqMin `json:"min,omitempty"` + Average TresList `json:"average,omitempty"` + Total TresList `json:"total,omitempty"` +} + +// StepTresConsumed represents consumed TRES for a step. +type StepTresConsumed struct { + Max StepTresUsageMax `json:"max,omitempty"` + Min StepTresUsageMin `json:"min,omitempty"` + Average TresList `json:"average,omitempty"` + Total TresList `json:"total,omitempty"` +} + +// StepList is a list of Step objects. +// Corresponds to v0.0.40_step_list. +type StepList []Step + +// --------------------------------------------------------------------------- +// WCKeY tag struct +// --------------------------------------------------------------------------- + +// WckeyTagStruct represents a WCKey with tag flags. +// Corresponds to v0.0.40_wckey_tag_struct. +type WckeyTagStruct struct { + Wckey *string `json:"wckey,omitempty"` + Flags []string `json:"flags,omitempty"` +} + +// --------------------------------------------------------------------------- +// Account types +// --------------------------------------------------------------------------- + +// Account represents a Slurm account. +// Corresponds to v0.0.40_account. +type Account struct { + Associations AssocShortList `json:"associations,omitempty"` + Coordinators CoordList `json:"coordinators,omitempty"` + Description *string `json:"description,omitempty"` + Name *string `json:"name,omitempty"` + Organization *string `json:"organization,omitempty"` + Flags []string `json:"flags,omitempty"` +} + +// AccountList is a list of Account objects. +// Corresponds to v0.0.40_account_list. +type AccountList []Account + +// AccountShort represents a short account reference. +// Corresponds to v0.0.40_account_short. +type AccountShort struct { + Description *string `json:"description,omitempty"` + Organization *string `json:"organization,omitempty"` +} + +// --------------------------------------------------------------------------- +// Accounting types +// --------------------------------------------------------------------------- + +// Accounting represents an accounting record. +// Corresponds to v0.0.40_accounting. +type Accounting struct { + Allocated *AccountingAllocated `json:"allocated,omitempty"` + ID *int32 `json:"id,omitempty"` + Start *int64 `json:"start,omitempty"` + TRES *Tres `json:"TRES,omitempty"` +} + +// AccountingAllocated represents allocated seconds in accounting. +type AccountingAllocated struct { + Seconds *int64 `json:"seconds,omitempty"` +} + +// AccountingList is a list of Accounting objects. +// Corresponds to v0.0.40_accounting_list. +type AccountingList []Accounting + +// --------------------------------------------------------------------------- +// Association types +// --------------------------------------------------------------------------- + +// Assoc represents a full association record. +// Corresponds to v0.0.40_assoc. +type Assoc struct { + Accounting AccountingList `json:"accounting,omitempty"` + Account *string `json:"account,omitempty"` + Cluster *string `json:"cluster,omitempty"` + Comment *string `json:"comment,omitempty"` + Default *AssocDefault `json:"default,omitempty"` + Flags []string `json:"flags,omitempty"` + Max *AssocMax `json:"max,omitempty"` + ID *AssocShort `json:"id,omitempty"` + IsDefault *bool `json:"is_default,omitempty"` + Lineage *string `json:"lineage,omitempty"` + Min *AssocMin `json:"min,omitempty"` + ParentAccount *string `json:"parent_account,omitempty"` + Partition *string `json:"partition,omitempty"` + Priority *Uint32NoVal `json:"priority,omitempty"` + Qos QosStringIdList `json:"qos,omitempty"` + SharesRaw *int32 `json:"shares_raw,omitempty"` + User *string `json:"user,omitempty"` +} + +// AssocDefault represents default settings for an association. +type AssocDefault struct { + Qos *string `json:"qos,omitempty"` +} + +// AssocMax represents max limits for an association. +type AssocMax struct { + Jobs *AssocMaxJobs `json:"jobs,omitempty"` + Tres *AssocMaxTres `json:"tres,omitempty"` + Per *AssocMaxPer `json:"per,omitempty"` +} + +// AssocMaxJobs represents job limits for an association. +type AssocMaxJobs struct { + Per *AssocMaxJobsPer `json:"per,omitempty"` + Active *Uint32NoVal `json:"active,omitempty"` + Accruing *Uint32NoVal `json:"accruing,omitempty"` + Total *Uint32NoVal `json:"total,omitempty"` +} + +// AssocMaxJobsPer represents per-entity job limits. +type AssocMaxJobsPer struct { + Count *Uint32NoVal `json:"count,omitempty"` + Accruing *Uint32NoVal `json:"accruing,omitempty"` + Submitted *Uint32NoVal `json:"submitted,omitempty"` + WallClock *Uint32NoVal `json:"wall_clock,omitempty"` +} + +// AssocMaxTres represents TRES limits for an association. +type AssocMaxTres struct { + Total TresList `json:"total,omitempty"` + Group *AssocMaxTresGroup `json:"group,omitempty"` + Minutes *AssocMaxTresMinutes `json:"minutes,omitempty"` + Per *AssocMaxTresPer `json:"per,omitempty"` +} + +// AssocMaxTresGroup represents TRES group limits. +type AssocMaxTresGroup struct { + Minutes TresList `json:"minutes,omitempty"` + Active TresList `json:"active,omitempty"` +} + +// AssocMaxTresMinutes represents TRES minutes limits. +type AssocMaxTresMinutes struct { + Total TresList `json:"total,omitempty"` + Per *AssocMaxTresMinutesPer `json:"per,omitempty"` +} + +// AssocMaxTresMinutesPer represents per-job TRES minutes limits. +type AssocMaxTresMinutesPer struct { + Job TresList `json:"job,omitempty"` +} + +// AssocMaxTresPer represents per-entity TRES limits. +type AssocMaxTresPer struct { + Job TresList `json:"job,omitempty"` + Node TresList `json:"node,omitempty"` +} + +// AssocMaxPer represents per-entity account limits. +type AssocMaxPer struct { + Account *AssocMaxPerAccount `json:"account,omitempty"` +} + +// AssocMaxPerAccount represents per-account limits. +type AssocMaxPerAccount struct { + WallClock *Uint32NoVal `json:"wall_clock,omitempty"` +} + +// AssocMin represents minimum settings for an association. +type AssocMin struct { + PriorityThreshold *Uint32NoVal `json:"priority_threshold,omitempty"` +} + +// AssocList is a list of Assoc objects. +// Corresponds to v0.0.40_assoc_list. +type AssocList []Assoc + +// --------------------------------------------------------------------------- +// Cluster record types +// --------------------------------------------------------------------------- + +// ClusterRec represents a cluster record. +// Corresponds to v0.0.40_cluster_rec. +type ClusterRec struct { + Controller *ClusterRecController `json:"controller,omitempty"` + Flags []string `json:"flags,omitempty"` + Name *string `json:"name,omitempty"` + Nodes *string `json:"nodes,omitempty"` + SelectPlugin *string `json:"select_plugin,omitempty"` + Associations *ClusterRecAssociations `json:"associations,omitempty"` + RpcVersion *int32 `json:"rpc_version,omitempty"` + Tres TresList `json:"tres,omitempty"` +} + +// ClusterRecController represents the controller info for a cluster. +type ClusterRecController struct { + Host *string `json:"host,omitempty"` + Port *int32 `json:"port,omitempty"` +} + +// ClusterRecAssociations represents associations for a cluster. +type ClusterRecAssociations struct { + Root *AssocShort `json:"root,omitempty"` +} + +// ClusterRecList is a list of ClusterRec objects. +// Corresponds to v0.0.40_cluster_rec_list. +type ClusterRecList []ClusterRec + +// --------------------------------------------------------------------------- +// Coordinator types +// --------------------------------------------------------------------------- + +// Coord represents a coordinator. +// Corresponds to v0.0.40_coord. +type Coord struct { + Name *string `json:"name,omitempty"` + Direct *bool `json:"direct,omitempty"` +} + +// CoordList is a list of Coord objects. +// Corresponds to v0.0.40_coord_list. +type CoordList []Coord + +// --------------------------------------------------------------------------- +// QOS types +// --------------------------------------------------------------------------- + +// QOS represents a Quality of Service. +// Corresponds to v0.0.40_qos. +type QOS struct { + Description *string `json:"description,omitempty"` + Flags []string `json:"flags,omitempty"` + ID *int32 `json:"id,omitempty"` + Limits *QOSLimits `json:"limits,omitempty"` + Name *string `json:"name,omitempty"` + Preempt *QOSPreempt `json:"preempt,omitempty"` + Priority *Uint32NoVal `json:"priority,omitempty"` + UsageFactor *Float64NoVal `json:"usage_factor,omitempty"` + UsageThreshold *Float64NoVal `json:"usage_threshold,omitempty"` +} + +// QOSLimits represents limits for a QOS. +type QOSLimits struct { + GraceTime *int32 `json:"grace_time,omitempty"` + Max *QOSLimitsMax `json:"max,omitempty"` + Factor *Float64NoVal `json:"factor,omitempty"` + Min *QOSLimitsMin `json:"min,omitempty"` +} + +// QOSLimitsMax represents max limits for a QOS. +type QOSLimitsMax struct { + ActiveJobs *QOSLimitsMaxActiveJobs `json:"active_jobs,omitempty"` + Tres *QOSLimitsMaxTres `json:"tres,omitempty"` + WallClock *QOSLimitsMaxWallClock `json:"wall_clock,omitempty"` + Jobs *QOSLimitsMaxJobs `json:"jobs,omitempty"` + Accruing *QOSLimitsMaxAccruing `json:"accruing,omitempty"` +} + +// QOSLimitsMaxActiveJobs represents active job limits. +type QOSLimitsMaxActiveJobs struct { + Accruing *Uint32NoVal `json:"accruing,omitempty"` + Count *Uint32NoVal `json:"count,omitempty"` +} + +// QOSLimitsMaxTres represents TRES limits. +type QOSLimitsMaxTres struct { + Total TresList `json:"total,omitempty"` + Minutes *QOSLimitsMaxTresMinutes `json:"minutes,omitempty"` + Per *QOSLimitsMaxTresPer `json:"per,omitempty"` +} + +// QOSLimitsMaxTresMinutes represents TRES minutes limits. +type QOSLimitsMaxTresMinutes struct { + Per *QOSLimitsMaxTresMinutesPer `json:"per,omitempty"` +} + +// QOSLimitsMaxTresMinutesPer represents per-entity TRES minutes. +type QOSLimitsMaxTresMinutesPer struct { + Qos TresList `json:"qos,omitempty"` + Job TresList `json:"job,omitempty"` + Account TresList `json:"account,omitempty"` + User TresList `json:"user,omitempty"` +} + +// QOSLimitsMaxTresPer represents per-entity TRES limits. +type QOSLimitsMaxTresPer struct { + Account TresList `json:"account,omitempty"` + Job TresList `json:"job,omitempty"` + Node TresList `json:"node,omitempty"` + User TresList `json:"user,omitempty"` +} + +// QOSLimitsMaxWallClock represents wall clock limits. +type QOSLimitsMaxWallClock struct { + Per *QOSLimitsMaxWallClockPer `json:"per,omitempty"` +} + +// QOSLimitsMaxWallClockPer represents per-entity wall clock limits. +type QOSLimitsMaxWallClockPer struct { + Qos *Uint32NoVal `json:"qos,omitempty"` + Job *Uint32NoVal `json:"job,omitempty"` +} + +// QOSLimitsMaxJobs represents job count limits. +type QOSLimitsMaxJobs struct { + ActiveJobs *QOSLimitsMaxJobsActiveJobs `json:"active_jobs,omitempty"` + Per *QOSLimitsMaxJobsPer `json:"per,omitempty"` +} + +// QOSLimitsMaxJobsActiveJobs represents active job per-entity limits. +type QOSLimitsMaxJobsActiveJobs struct { + Per *QOSLimitsMaxJobsActiveJobsPer `json:"per,omitempty"` +} + +// QOSLimitsMaxJobsActiveJobsPer represents per-entity active job limits. +type QOSLimitsMaxJobsActiveJobsPer struct { + Account *Uint32NoVal `json:"account,omitempty"` + User *Uint32NoVal `json:"user,omitempty"` +} + +// QOSLimitsMaxJobsPer represents per-entity job limits. +type QOSLimitsMaxJobsPer struct { + Account *Uint32NoVal `json:"account,omitempty"` + User *Uint32NoVal `json:"user,omitempty"` +} + +// QOSLimitsMaxAccruing represents accruing limits. +type QOSLimitsMaxAccruing struct { + Per *QOSLimitsMaxAccruingPer `json:"per,omitempty"` +} + +// QOSLimitsMaxAccruingPer represents per-entity accruing limits. +type QOSLimitsMaxAccruingPer struct { + Account *Uint32NoVal `json:"account,omitempty"` + User *Uint32NoVal `json:"user,omitempty"` +} + +// QOSLimitsMin represents minimum limits for a QOS. +type QOSLimitsMin struct { + PriorityThreshold *Uint32NoVal `json:"priority_threshold,omitempty"` + Tres *QOSLimitsMinTres `json:"tres,omitempty"` +} + +// QOSLimitsMinTres represents minimum TRES. +type QOSLimitsMinTres struct { + Per *QOSLimitsMinTresPer `json:"per,omitempty"` +} + +// QOSLimitsMinTresPer represents per-job minimum TRES. +type QOSLimitsMinTresPer struct { + Job TresList `json:"job,omitempty"` +} + +// QOSPreempt represents preempt settings for a QOS. +type QOSPreempt struct { + List QosPreemptList `json:"list,omitempty"` + Mode []string `json:"mode,omitempty"` + ExemptTime *Uint32NoVal `json:"exempt_time,omitempty"` +} + +// QosList is a list of QOS objects. +// Corresponds to v0.0.40_qos_list. +type QosList []QOS + +// QosPreemptList is a list of QOS names for preemption. +// Corresponds to v0.0.40_qos_preempt_list. +type QosPreemptList []string + +// QosStringIdList is a list of QOS name strings. +// Corresponds to v0.0.40_qos_string_id_list. +type QosStringIdList []string + +// --------------------------------------------------------------------------- +// User types +// --------------------------------------------------------------------------- + +// User represents a Slurm user. +// Corresponds to v0.0.40_user. +type User struct { + AdministratorLevel []string `json:"administrator_level,omitempty"` + Associations AssocShortList `json:"associations,omitempty"` + Coordinators CoordList `json:"coordinators,omitempty"` + Default *UserDefault `json:"default,omitempty"` + Flags []string `json:"flags,omitempty"` + Name *string `json:"name,omitempty"` + OldName *string `json:"old_name,omitempty"` + Wckeys WckeyList `json:"wckeys,omitempty"` +} + +// UserDefault represents default settings for a user. +type UserDefault struct { + Account *string `json:"account,omitempty"` + Wckey *string `json:"wckey,omitempty"` +} + +// UserList is a list of User objects. +// Corresponds to v0.0.40_user_list. +type UserList []User + +// UserShort represents a short user reference. +// Corresponds to v0.0.40_user_short. +type UserShort struct { + Adminlevel []string `json:"adminlevel,omitempty"` + Defaultaccount *string `json:"defaultaccount,omitempty"` + Defaultwckey *string `json:"defaultwckey,omitempty"` +} + +// --------------------------------------------------------------------------- +// WCKeY types +// --------------------------------------------------------------------------- + +// Wckey represents a WCKey record. +// Corresponds to v0.0.40_wckey. +type Wckey struct { + Accounting AccountingList `json:"accounting,omitempty"` + Cluster *string `json:"cluster,omitempty"` + ID *int32 `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + User *string `json:"user,omitempty"` + Flags []string `json:"flags,omitempty"` +} + +// WckeyList is a list of Wckey objects. +// Corresponds to v0.0.40_wckey_list. +type WckeyList []Wckey + +// --------------------------------------------------------------------------- +// Instance types +// --------------------------------------------------------------------------- + +// Instance represents a compute instance. +// Corresponds to v0.0.40_instance. +type Instance struct { + Cluster *string `json:"cluster,omitempty"` + Extra *string `json:"extra,omitempty"` + InstanceID *string `json:"instance_id,omitempty"` + InstanceType *string `json:"instance_type,omitempty"` + NodeName *string `json:"node_name,omitempty"` + Time *InstanceTime `json:"time,omitempty"` +} + +// InstanceTime represents time range for an instance. +type InstanceTime struct { + TimeEnd *int64 `json:"time_end,omitempty"` + TimeStart *int64 `json:"time_start,omitempty"` +} + +// InstanceList is a list of Instance objects. +// Corresponds to v0.0.40_instance_list. +type InstanceList []Instance diff --git a/internal/slurm/types_job_test.go b/internal/slurm/types_job_test.go new file mode 100644 index 0000000..d219079 --- /dev/null +++ b/internal/slurm/types_job_test.go @@ -0,0 +1,777 @@ +package slurm + +import ( + "encoding/json" + "testing" +) + +func TestJobInfoRoundTrip(t *testing.T) { + orig := JobInfo{ + Account: Ptr("testacct"), + AccrueTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))}, + ArrayJobID: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(100))}, + ArrayTaskID: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(1))}, + BatchFlag: Ptr(true), + BatchHost: Ptr("node01"), + Cluster: Ptr("test-cluster"), + Command: Ptr("/usr/bin/true"), + Contiguous: Ptr(false), + Cpus: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(4))}, + ExitCode: &ProcessExitCodeVerbose{ + Status: []string{"EXITED"}, + ReturnCode: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(0))}, + }, + Flags: []string{"CRON_JOB"}, + GroupID: Ptr(int32(1000)), + JobID: Ptr(int32(12345)), + JobState: []string{"COMPLETED"}, + Name: Ptr("test-job"), + NodeCount: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(2))}, + Nodes: Ptr("node[01-02]"), + Partition: Ptr("normal"), + Priority: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(4294901758))}, + StartTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))}, + SubmitTime: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567800))}, + Tasks: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(2))}, + TimeLimit: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(60))}, + UserID: Ptr(int32(1001)), + UserName: Ptr("testuser"), + CurrentWorkingDirectory: Ptr("/home/testuser"), + Power: &JobInfoPower{Flags: []string{"EQUAL_POWER"}}, + StandardOutput: Ptr("/home/testuser/slurm-%j.out"), + StandardError: Ptr("/home/testuser/slurm-%j.err"), + StandardInput: Ptr("/dev/null"), + GresDetail: JobInfoGresDetail{"gpu:2", "mps:100"}, + BillableTres: &Float64NoVal{Set: Ptr(true), Number: Ptr(4.0)}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded JobInfo + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Account == nil || *decoded.Account != "testacct" { + t.Fatalf("account mismatch: %v", decoded.Account) + } + if decoded.JobID == nil || *decoded.JobID != 12345 { + t.Fatalf("job_id mismatch: %v", decoded.JobID) + } + if len(decoded.JobState) != 1 || decoded.JobState[0] != "COMPLETED" { + t.Fatalf("job_state mismatch: %v", decoded.JobState) + } + if decoded.Cpus == nil || decoded.Cpus.Number == nil || *decoded.Cpus.Number != 4 { + t.Fatalf("cpus mismatch: %v", decoded.Cpus) + } + if decoded.ExitCode == nil || decoded.ExitCode.ReturnCode == nil { + t.Fatal("exit_code should not be nil") + } + if decoded.Power == nil || len(decoded.Power.Flags) != 1 || decoded.Power.Flags[0] != "EQUAL_POWER" { + t.Fatalf("power mismatch: %v", decoded.Power) + } + if len(decoded.GresDetail) != 2 || decoded.GresDetail[0] != "gpu:2" { + t.Fatalf("gres_detail mismatch: %v", decoded.GresDetail) + } + if decoded.BillableTres == nil || decoded.BillableTres.Number == nil || *decoded.BillableTres.Number != 4.0 { + t.Fatalf("billable_tres mismatch: %v", decoded.BillableTres) + } +} + +func TestJobDescMsgRoundTrip(t *testing.T) { + orig := JobDescMsg{ + Script: Ptr("#!/bin/bash\necho hello"), + Name: Ptr("submit-test"), + Account: Ptr("testacct"), + Partition: Ptr("normal"), + Tasks: Ptr(int32(4)), + Nodes: Ptr("node[01-02]"), + TimeLimit: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(30))}, + MemoryPerCpu: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(4096))}, + MemoryPerNode: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(8192))}, + Argv: StringArray{"/bin/bash", "-c", "echo hello"}, + Environment: StringArray{"PATH=/usr/bin", "HOME=/root"}, + CurrentWorkingDirectory: Ptr("/tmp"), + StandardOutput: Ptr("slurm-%j.out"), + Flags: []string{"SPREAD_JOB"}, + MailType: []string{"BEGIN", "END"}, + MailUser: Ptr("user@example.com"), + Rlimits: &JobDescRlimits{ + Cpu: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(3600))}, + Nofile: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(65536))}, + }, + Crontab: &CronEntry{ + Minute: Ptr("*/5"), + Hour: Ptr("*"), + Line: &CronEntryLine{Start: Ptr(int32(1)), End: Ptr(int32(5))}, + }, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded JobDescMsg + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Script == nil || *decoded.Script != "#!/bin/bash\necho hello" { + t.Fatalf("script mismatch") + } + if decoded.Name == nil || *decoded.Name != "submit-test" { + t.Fatalf("name mismatch") + } + if decoded.Tasks == nil || *decoded.Tasks != 4 { + t.Fatalf("tasks mismatch: %v", decoded.Tasks) + } + if decoded.Rlimits == nil || decoded.Rlimits.Cpu == nil { + t.Fatal("rlimits should not be nil") + } + if *decoded.Rlimits.Cpu.Number != 3600 { + t.Fatalf("rlimits.cpu mismatch: %v", decoded.Rlimits.Cpu) + } + if decoded.Crontab == nil || decoded.Crontab.Minute == nil || *decoded.Crontab.Minute != "*/5" { + t.Fatal("crontab.minute mismatch") + } + if len(decoded.Argv) != 3 { + t.Fatalf("argv mismatch: %v", decoded.Argv) + } + if len(decoded.Flags) != 1 || decoded.Flags[0] != "SPREAD_JOB" { + t.Fatalf("flags mismatch: %v", decoded.Flags) + } +} + +func TestJobSubmitReqRoundTrip(t *testing.T) { + orig := JobSubmitReq{ + Script: Ptr("#!/bin/bash\necho test"), + Job: &JobDescMsg{ + Name: Ptr("myjob"), + Partition: Ptr("debug"), + Tasks: Ptr(int32(1)), + }, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded JobSubmitReq + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Script == nil || *decoded.Script != "#!/bin/bash\necho test" { + t.Fatal("script mismatch") + } + if decoded.Job == nil || decoded.Job.Name == nil || *decoded.Job.Name != "myjob" { + t.Fatal("job.name mismatch") + } + if decoded.Job.Tasks == nil || *decoded.Job.Tasks != 1 { + t.Fatal("job.tasks mismatch") + } +} + +func TestOpenapiJobInfoRespRoundTrip(t *testing.T) { + orig := OpenapiJobInfoResp{ + Jobs: JobInfoMsg{ + { + JobID: Ptr(int32(100)), + Name: Ptr("job-a"), + JobState: []string{"RUNNING"}, + }, + { + JobID: Ptr(int32(101)), + Name: Ptr("job-b"), + JobState: []string{"PENDING"}, + }, + }, + LastBackfill: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))}, + LastUpdate: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567900))}, + Meta: &OpenapiMeta{ + Slurm: &MetaSlurm{ + Version: &MetaSlurmVersion{ + Major: Ptr("24"), + Micro: Ptr("5"), + Minor: Ptr("05"), + }, + }, + }, + Errors: OpenapiErrors{{Error: Ptr("none")}}, + Warnings: OpenapiWarnings{}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded OpenapiJobInfoResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if len(decoded.Jobs) != 2 { + t.Fatalf("expected 2 jobs, got %d", len(decoded.Jobs)) + } + if decoded.Jobs[0].JobID == nil || *decoded.Jobs[0].JobID != 100 { + t.Fatalf("job[0].job_id mismatch") + } + if decoded.Jobs[1].Name == nil || *decoded.Jobs[1].Name != "job-b" { + t.Fatalf("job[1].name mismatch") + } + if decoded.Meta == nil || decoded.Meta.Slurm == nil || decoded.Meta.Slurm.Version == nil { + t.Fatal("meta.slurm.version should not be nil") + } + if decoded.LastBackfill == nil || decoded.LastBackfill.Number == nil || *decoded.LastBackfill.Number != 1234567890 { + t.Fatalf("last_backfill mismatch") + } +} + +func TestOpenapiJobPostResponseRoundTrip(t *testing.T) { + orig := OpenapiJobPostResponse{ + Results: JobArrayResponseArray{ + { + JobID: Ptr(int32(200)), + StepID: Ptr("batch"), + Error: Ptr(""), + Why: Ptr("Job updated"), + }, + }, + JobID: Ptr("200"), + StepID: Ptr("batch"), + JobSubmitUserMsg: Ptr("Job updated successfully"), + Meta: &OpenapiMeta{ + Plugin: &MetaPlugin{Type: Ptr("type")}, + }, + Errors: OpenapiErrors{}, + Warnings: OpenapiWarnings{}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded OpenapiJobPostResponse + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if len(decoded.Results) != 1 { + t.Fatalf("expected 1 result, got %d", len(decoded.Results)) + } + if decoded.Results[0].JobID == nil || *decoded.Results[0].JobID != 200 { + t.Fatalf("results[0].job_id mismatch") + } + if decoded.JobSubmitUserMsg == nil || *decoded.JobSubmitUserMsg != "Job updated successfully" { + t.Fatal("job_submit_user_msg mismatch") + } +} + +func TestOpenapiJobSubmitResponseRoundTrip(t *testing.T) { + orig := OpenapiJobSubmitResponse{ + Result: &JobSubmitResponseMsg{ + JobID: Ptr(int32(300)), + StepID: Ptr("batch"), + JobSubmitUserMsg: Ptr("Job submitted"), + }, + JobID: Ptr(int32(300)), + StepID: Ptr("batch"), + JobSubmitUserMsg: Ptr("Job submitted"), + Meta: &OpenapiMeta{ + Client: &MetaClient{User: Ptr("testuser")}, + }, + Errors: OpenapiErrors{{Description: Ptr("no error")}}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded OpenapiJobSubmitResponse + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Result == nil || decoded.Result.JobID == nil || *decoded.Result.JobID != 300 { + t.Fatal("result.job_id mismatch") + } + if decoded.JobID == nil || *decoded.JobID != 300 { + t.Fatal("job_id mismatch") + } + if decoded.Meta == nil || decoded.Meta.Client == nil || decoded.Meta.Client.User == nil { + t.Fatal("meta.client.user should not be nil") + } +} + +func TestOpenapiRespRoundTrip(t *testing.T) { + orig := OpenapiResp{ + Meta: &OpenapiMeta{ + Slurm: &MetaSlurm{Release: Ptr("24.05.5")}, + }, + Errors: OpenapiErrors{}, + Warnings: OpenapiWarnings{}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded OpenapiResp + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Meta == nil || decoded.Meta.Slurm == nil || decoded.Meta.Slurm.Release == nil { + t.Fatal("meta.slurm.release should not be nil") + } + if *decoded.Meta.Slurm.Release != "24.05.5" { + t.Fatalf("release mismatch: %v", decoded.Meta.Slurm.Release) + } +} + +func TestJobInfoMinimalRoundTrip(t *testing.T) { + orig := JobInfo{JobID: Ptr(int32(1))} + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded JobInfo + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.JobID == nil || *decoded.JobID != 1 { + t.Fatal("job_id mismatch") + } +} + +func TestJobInfoEmptyRoundTrip(t *testing.T) { + orig := JobInfo{} + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + if string(data) != "{}" { + t.Fatalf("empty JobInfo should marshal to {}, got %s", data) + } + var decoded JobInfo + if err := json.Unmarshal([]byte(`{}`), &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.JobID != nil { + t.Fatal("job_id should be nil for empty object") + } +} + +func TestJobResRoundTrip(t *testing.T) { + orig := JobRes{ + Nodes: Ptr("node[01-02]"), + AllocatedCores: Ptr(int32(8)), + AllocatedCpus: Ptr(int32(16)), + AllocatedHosts: Ptr(int32(2)), + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded JobRes + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.AllocatedCpus == nil || *decoded.AllocatedCpus != 16 { + t.Fatal("allocated_cpus mismatch") + } +} + +func TestCronEntryRoundTrip(t *testing.T) { + orig := CronEntry{ + Flags: []string{"WILD_MINUTE"}, + Minute: Ptr("0,30"), + Hour: Ptr("*"), + DayOfMonth: Ptr("*"), + Month: Ptr("*"), + DayOfWeek: Ptr("1-5"), + Specification: Ptr("0,30 * * * 1-5"), + Command: Ptr("/usr/bin/run-job"), + Line: &CronEntryLine{Start: Ptr(int32(10)), End: Ptr(int32(12))}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded CronEntry + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Specification == nil || *decoded.Specification != "0,30 * * * 1-5" { + t.Fatal("specification mismatch") + } + if decoded.Line == nil || decoded.Line.Start == nil || *decoded.Line.Start != 10 { + t.Fatal("line.start mismatch") + } +} + +func TestTresRoundTrip(t *testing.T) { + orig := Tres{ + Type: Ptr("cpu"), + Name: Ptr(""), + ID: Ptr(int32(1)), + Count: Ptr(int64(4)), + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded Tres + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Type == nil || *decoded.Type != "cpu" { + t.Fatal("type mismatch") + } + if decoded.Count == nil || *decoded.Count != 4 { + t.Fatal("count mismatch") + } +} + +func TestAssocShortRoundTrip(t *testing.T) { + orig := AssocShort{ + Account: Ptr("default"), + Cluster: Ptr("test-cluster"), + User: Ptr("testuser"), + ID: Ptr(int32(42)), + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded AssocShort + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if *decoded.Account != "default" || *decoded.User != "testuser" || *decoded.ID != 42 { + t.Fatal("round-trip mismatch") + } +} + +func TestQOSRoundTrip(t *testing.T) { + orig := QOS{ + Name: Ptr("normal"), + Description: Ptr("Default QOS"), + ID: Ptr(int32(1)), + Flags: []string{"NOT_SET"}, + Priority: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(100))}, + UsageFactor: &Float64NoVal{Set: Ptr(true), Number: Ptr(1.0)}, + Limits: &QOSLimits{ + GraceTime: Ptr(int32(30)), + Max: &QOSLimitsMax{ + ActiveJobs: &QOSLimitsMaxActiveJobs{ + Count: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(100))}, + }, + WallClock: &QOSLimitsMaxWallClock{ + Per: &QOSLimitsMaxWallClockPer{ + Job: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(1440))}, + }, + }, + }, + }, + Preempt: &QOSPreempt{ + Mode: []string{"REQUEUE"}, + ExemptTime: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(300))}, + }, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded QOS + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Name == nil || *decoded.Name != "normal" { + t.Fatal("name mismatch") + } + if decoded.Limits == nil || decoded.Limits.GraceTime == nil || *decoded.Limits.GraceTime != 30 { + t.Fatal("limits.grace_time mismatch") + } + if decoded.Limits.Max == nil || decoded.Limits.Max.ActiveJobs == nil { + t.Fatal("limits.max.active_jobs should not be nil") + } + if decoded.Preempt == nil || len(decoded.Preempt.Mode) != 1 || decoded.Preempt.Mode[0] != "REQUEUE" { + t.Fatal("preempt.mode mismatch") + } +} + +func TestUserRoundTrip(t *testing.T) { + orig := User{ + Name: Ptr("testuser"), + AdministratorLevel: []string{"None"}, + Associations: AssocShortList{ + {Account: Ptr("default"), Cluster: Ptr("test"), User: Ptr("testuser")}, + }, + Default: &UserDefault{ + Account: Ptr("default"), + Wckey: Ptr("*"), + }, + Flags: []string{"NONE"}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded User + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Name == nil || *decoded.Name != "testuser" { + t.Fatal("name mismatch") + } + if len(decoded.Associations) != 1 { + t.Fatalf("expected 1 association, got %d", len(decoded.Associations)) + } + if decoded.Default == nil || decoded.Default.Account == nil || *decoded.Default.Account != "default" { + t.Fatal("default.account mismatch") + } +} + +func TestAccountRoundTrip(t *testing.T) { + orig := Account{ + Name: Ptr("testacct"), + Description: Ptr("Test account"), + Organization: Ptr("testorg"), + Coordinators: CoordList{ + {Name: Ptr("admin1"), Direct: Ptr(true)}, + }, + Associations: AssocShortList{ + {Account: Ptr("testacct"), User: Ptr("user1")}, + }, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded Account + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Name == nil || *decoded.Name != "testacct" { + t.Fatal("name mismatch") + } + if len(decoded.Coordinators) != 1 { + t.Fatalf("expected 1 coordinator, got %d", len(decoded.Coordinators)) + } +} + +func TestClusterRecRoundTrip(t *testing.T) { + orig := ClusterRec{ + Name: Ptr("test-cluster"), + Controller: &ClusterRecController{ + Host: Ptr("ctrl-node"), + Port: Ptr(int32(6817)), + }, + Flags: []string{"MULTIPLE_SLURMD"}, + Tres: TresList{ + {Type: Ptr("cpu"), Count: Ptr(int64(100))}, + {Type: Ptr("mem"), Count: Ptr(int64(512000))}, + }, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded ClusterRec + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Name == nil || *decoded.Name != "test-cluster" { + t.Fatal("name mismatch") + } + if decoded.Controller == nil || decoded.Controller.Port == nil || *decoded.Controller.Port != 6817 { + t.Fatal("controller.port mismatch") + } + if len(decoded.Tres) != 2 { + t.Fatalf("expected 2 tres, got %d", len(decoded.Tres)) + } +} + +func TestAssocRoundTrip(t *testing.T) { + orig := Assoc{ + Account: Ptr("testacct"), + Cluster: Ptr("test-cluster"), + User: Ptr("testuser"), + ID: &AssocShort{ + Account: Ptr("testacct"), + Cluster: Ptr("test-cluster"), + User: Ptr("testuser"), + ID: Ptr(int32(1)), + }, + Default: &AssocDefault{Qos: Ptr("normal")}, + IsDefault: Ptr(true), + Qos: QosStringIdList{"normal", "high"}, + Max: &AssocMax{ + Jobs: &AssocMaxJobs{ + Total: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(500))}, + }, + }, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded Assoc + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if *decoded.Account != "testacct" { + t.Fatal("account mismatch") + } + if len(decoded.Qos) != 2 || decoded.Qos[0] != "normal" { + t.Fatalf("qos mismatch: %v", decoded.Qos) + } + if decoded.Max == nil || decoded.Max.Jobs == nil { + t.Fatal("max.jobs should not be nil") + } +} + +func TestWckeyRoundTrip(t *testing.T) { + orig := Wckey{ + Name: Ptr("mykey"), + Cluster: Ptr("test-cluster"), + User: Ptr("testuser"), + ID: Ptr(int32(1)), + Flags: []string{"DELETED"}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded Wckey + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if *decoded.Name != "mykey" { + t.Fatal("name mismatch") + } +} + +func TestInstanceRoundTrip(t *testing.T) { + orig := Instance{ + Cluster: Ptr("test-cluster"), + InstanceID: Ptr("i-12345"), + InstanceType: Ptr("c5.large"), + NodeName: Ptr("node01"), + Time: &InstanceTime{ + TimeStart: Ptr(int64(1234567890)), + TimeEnd: Ptr(int64(1234567900)), + }, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded Instance + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if *decoded.InstanceID != "i-12345" { + t.Fatal("instance_id mismatch") + } +} + +func TestWckeyTagStructRoundTrip(t *testing.T) { + orig := WckeyTagStruct{ + Wckey: Ptr("mykey"), + Flags: []string{"ASSIGNED_DEFAULT"}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded WckeyTagStruct + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if *decoded.Wckey != "mykey" { + t.Fatal("wckey mismatch") + } + if len(decoded.Flags) != 1 || decoded.Flags[0] != "ASSIGNED_DEFAULT" { + t.Fatal("flags mismatch") + } +} + +func TestAccountingRoundTrip(t *testing.T) { + orig := Accounting{ + ID: Ptr(int32(1)), + Start: Ptr(int64(1234567890)), + Allocated: &AccountingAllocated{ + Seconds: Ptr(int64(3600)), + }, + TRES: &Tres{Type: Ptr("cpu"), Count: Ptr(int64(4))}, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded Accounting + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if *decoded.ID != 1 || *decoded.Start != 1234567890 { + t.Fatal("id/start mismatch") + } + if decoded.Allocated == nil || *decoded.Allocated.Seconds != 3600 { + t.Fatal("allocated.seconds mismatch") + } +} + +func TestCoordRoundTrip(t *testing.T) { + orig := Coord{ + Name: Ptr("admin"), + Direct: Ptr(true), + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded Coord + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if *decoded.Name != "admin" || !*decoded.Direct { + t.Fatal("round-trip mismatch") + } +} + +func TestStepRoundTrip(t *testing.T) { + orig := Step{ + Step: &StepIDName{ + ID: Ptr("0"), + Name: Ptr("step0"), + }, + State: []string{"COMPLETED"}, + ExitCode: &ProcessExitCodeVerbose{ + Status: []string{"EXITED"}, + ReturnCode: &Uint32NoVal{Set: Ptr(true), Number: Ptr(int64(0))}, + }, + Nodes: &StepNodes{ + Count: Ptr(int32(2)), + Range: Ptr("node[01-02]"), + List: Hostlist{"node01", "node02"}, + }, + Tasks: &StepTasks{Count: Ptr(int32(4))}, + Time: &StepTime{ + Elapsed: Ptr(int32(60)), + Start: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567890))}, + End: &Uint64NoVal{Set: Ptr(true), Number: Ptr(int64(1234567950))}, + }, + Tres: &StepTres{ + Allocated: TresList{ + {Type: Ptr("cpu"), Count: Ptr(int64(4))}, + }, + }, + } + data, err := json.Marshal(orig) + if err != nil { + t.Fatalf("marshal: %v", err) + } + var decoded Step + if err := json.Unmarshal(data, &decoded); err != nil { + t.Fatalf("unmarshal: %v", err) + } + if decoded.Step == nil || decoded.Step.ID == nil || *decoded.Step.ID != "0" { + t.Fatal("step.id mismatch") + } + if decoded.Nodes == nil || decoded.Nodes.Count == nil || *decoded.Nodes.Count != 2 { + t.Fatal("nodes.count mismatch") + } + if len(decoded.Nodes.List) != 2 { + t.Fatalf("nodes.list mismatch: %v", decoded.Nodes.List) + } +}