Add scheduling_map field to ParameterSchema so Application creators can declare that a parameter (e.g. NP) maps to a scheduling field (e.g. cpus). The backend auto-injects the scheduling value into script template variables before rendering, eliminating duplicate user input. The frontend hides mapped parameters from the form and injects their values on submit.
151 lines
4.3 KiB
Go
151 lines
4.3 KiB
Go
package service
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gcy_hpc_server/internal/model"
|
|
)
|
|
|
|
var paramNameRegex = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
|
|
|
// ValidateParams checks that all required parameters are present and values match their types.
|
|
// Parameters not in the schema are silently ignored.
|
|
func ValidateParams(params []model.ParameterSchema, values map[string]string) error {
|
|
var errs []string
|
|
|
|
for _, p := range params {
|
|
if !paramNameRegex.MatchString(p.Name) {
|
|
errs = append(errs, fmt.Sprintf("invalid parameter name %q: must match ^[A-Za-z_][A-Za-z0-9_]*$", p.Name))
|
|
continue
|
|
}
|
|
|
|
val, ok := values[p.Name]
|
|
|
|
if p.Required && !ok {
|
|
errs = append(errs, fmt.Sprintf("required parameter %q is missing", p.Name))
|
|
continue
|
|
}
|
|
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
switch p.Type {
|
|
case model.ParamTypeInteger:
|
|
if _, err := strconv.Atoi(val); err != nil {
|
|
errs = append(errs, fmt.Sprintf("parameter %q must be an integer, got %q", p.Name, val))
|
|
}
|
|
case model.ParamTypeBoolean:
|
|
if val != "true" && val != "false" && val != "1" && val != "0" {
|
|
errs = append(errs, fmt.Sprintf("parameter %q must be a boolean (true/false/1/0), got %q", p.Name, val))
|
|
}
|
|
case model.ParamTypeEnum:
|
|
if len(p.Options) > 0 {
|
|
found := false
|
|
for _, opt := range p.Options {
|
|
if val == opt {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
errs = append(errs, fmt.Sprintf("parameter %q must be one of %v, got %q", p.Name, p.Options, val))
|
|
}
|
|
}
|
|
case model.ParamTypeFile, model.ParamTypeDirectory:
|
|
if _, err := strconv.ParseInt(val, 10, 64); err != nil {
|
|
errs = append(errs, fmt.Sprintf("parameter %q must be a valid file ID (integer), got %q", p.Name, val))
|
|
}
|
|
case model.ParamTypeString:
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("parameter validation failed: %s", strings.Join(errs, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RenderScript replaces $PARAM tokens in the template with user-provided values.
|
|
// Only tokens defined in the schema are replaced. Replacement is done longest-name-first
|
|
// to avoid partial matches (e.g., $JOB_NAME before $JOB).
|
|
// String, integer, boolean, and enum values are shell-escaped using single-quote wrapping.
|
|
// File and directory values (including WORK_DIR) are inserted raw (no escaping) because
|
|
// they are paths used in SBATCH directives and command arguments.
|
|
func RenderScript(template string, params []model.ParameterSchema, values map[string]string) string {
|
|
sorted := make([]model.ParameterSchema, len(params))
|
|
copy(sorted, params)
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|
return len(sorted[i].Name) > len(sorted[j].Name)
|
|
})
|
|
|
|
// Add virtual "WORK_DIR" entry so $WORK_DIR is replaced without escaping.
|
|
sorted = append(sorted, model.ParameterSchema{Name: "WORK_DIR", Type: model.ParamTypeFile})
|
|
|
|
result := template
|
|
for _, p := range sorted {
|
|
val, ok := values[p.Name]
|
|
if !ok {
|
|
if p.Default != "" {
|
|
val = p.Default
|
|
} else {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if p.Type == model.ParamTypeFile || p.Type == model.ParamTypeDirectory {
|
|
result = strings.ReplaceAll(result, "$"+p.Name, val)
|
|
} else {
|
|
escaped := "'" + strings.ReplaceAll(val, "'", "'\\''") + "'"
|
|
result = strings.ReplaceAll(result, "$"+p.Name, escaped)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// SanitizeDirName sanitizes a directory name.
|
|
func SanitizeDirName(name string) string {
|
|
replacer := strings.NewReplacer(" ", "_", "/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_")
|
|
return replacer.Replace(name)
|
|
}
|
|
|
|
// RandomSuffix generates a random suffix of length n.
|
|
func RandomSuffix(n int) string {
|
|
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
|
|
b := make([]byte, n)
|
|
for i := range b {
|
|
b[i] = charset[rand.Intn(len(charset))]
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func ResolveSchedulingMap(field string, task *model.Task) string {
|
|
switch field {
|
|
case "cpus":
|
|
return derefInt32ToStr(task.Cpus)
|
|
case "memory_per_node":
|
|
return derefInt64ToStr(task.MemoryPerNode)
|
|
case "memory_per_cpu":
|
|
return derefInt64ToStr(task.MemoryPerCpu)
|
|
case "nodes":
|
|
return derefStr(task.Nodes)
|
|
case "tasks":
|
|
return derefInt32ToStr(task.Tasks)
|
|
case "cpus_per_task":
|
|
return derefInt32ToStr(task.CpusPerTask)
|
|
case "partition":
|
|
return task.Partition
|
|
case "time_limit":
|
|
return derefInt32ToStr(task.TimeLimit)
|
|
case "qos":
|
|
return derefStr(task.QOS)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|