package controllers import ( "apiserver/backend" "apiserver/common" "apiserver/models" "apiserver/services" "apiserver/store" "apiserver/utils" logger "apiserver/utils" "encoding/json" "errors" "reflect" "strings" "github.com/gin-gonic/gin" "go.uber.org/zap" ) type HostHandler struct { } // GetHosts godoc // @Summary Get host information // @Description Get host information // @Tags hosts // @Accept json // @Produce json // @Success 200 // @Failure 400 {object} models.Error // @Router /api/v1/hosts [get] func (h *HostHandler) GetHostsInfo(ctx *gin.Context) { logger.Debug("Enter GetAllHosts() function from ", zap.String("client_ip", ctx.ClientIP())) var fields []string filters := make([]utils.FilterCondition, 0) sortOrders := make([]utils.SortOrder, 0) // ---- check parameters ---- for param := range ctx.Request.URL.Query() { if !utils.ContainsField(utils.SupportedQueryParams, param) { err := errors.New("invalid host info query parameters of : " + param) logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } } // Get query parameters filterStr := ctx.Query("filter") fieldsStr := ctx.Query("fields") sortStr := ctx.Query("sort") // sort=host_name:asc,submit_time:desc pagetokenStr := ctx.Query("page_token") // page_token=Aafjakjf0023A limitStr := ctx.Query("limit") // eg: limit=100 logger.Debug("Host query values are:", zap.String("fieldsStr", fieldsStr), zap.String("filterStr", filterStr), zap.String("sortStr", sortStr), zap.String("limitStr", limitStr), zap.String("pagetokenStr", pagetokenStr)) // ---- check limit value ---- limit, err := utils.ParseLimit(limitStr) if err != nil { logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, err.Error(), nil) return } // ---- check filter ---- if filterStr != "" { filters = utils.ParseFilters(filterStr) if filters == nil { err := errors.New("filter " + filterStr + " has syntax error") logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } } //filtercds := make([]utils.Condition, len(filters)) for _, filter := range filters { // check the filter is supported. if !utils.ContainsField(models.HOST_SUPPORT_FILTERS, filter.Field) { err := errors.New("unsupported filter: " + filter.Field) logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } } var hosts models.HostInfo filtercds, err := utils.ConvertToNewFilter(filters, &hosts) if err != nil { logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } // ---- check fields ---- // get fields and do check, if not set, use default display columns if fieldsStr != "" { fields = utils.ParseFields(fieldsStr) found := false // verify the fields is in supported list supportedFields := utils.GetSupportedFields(models.HostInfo{}) logger.Debug("host supportedFields: %v", zap.Any("supportedFields", supportedFields)) for _, field := range fields { if !utils.ContainsField(supportedFields, field) { err := errors.New("unsupported query field: " + field) logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } // if user not specify host-name in fields, add host_name as default field. if field == "host_name" { found = true } } if !found { logger.Debug("user doesn't specify hostname in query fields, add it.") fields = append(fields, "host_name") } } else { fields = models.HOST_DEFAULT_COLUMN } // ---- check sort ---- // handle sort, when no sortStr specified, need add the primary key of host_info // table into sortStr as default value if sortStr == "" { logger.Debug("sort is not specified, set sort=host_name:asc as default") sortStr = "host_name:asc" } // parse sortStr sorts, err := utils.ParseSortParam(sortStr, models.HOST_SUPPORT_SORT) if err != nil { logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } // ---- check paging --- if pagetokenStr != "" { pagetokenStr, err = utils.ParsePageToken(pagetokenStr) if err != nil { logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } } //the primary key of host_info table host_id should always be added into sortStr if user doesn't specify. found := false for _, sortent := range sorts { if sortent.Column == "host_name" { found = true break } } // user not specify the host_name, only append host_name at the end. if !found { sorts = append(sorts, utils.SortOrder{ Column: "host_name", Ascending: true, }) } //check all sort column is in fields err = utils.CheckAllMapKeyInList(sorts, fields) if err != nil { logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } sortOrders = sorts //fmt.Println("filtercds:", filtercds, "sortOrders:", sortOrders, "limit:", limit, "pagetokenStr:", pagetokenStr) queryOpts, err := utils.BuildQueryOptions(filtercds, nil, sortOrders, limit, pagetokenStr) if err != nil { logger.Error(err.Error()) ReturnError(ctx, common.ERROR_CODE, common.ERROR_QUERY_HOSTINFO_MSG+": "+err.Error(), nil) return } hqueryReply, pagetoken, err := services.QueryData(services.TableHostInfo, queryOpts, fields) if err != nil { logger.Error("Failed to query host info.", zap.Error(err)) ReturnError(ctx, common.ERROR_CODE, common.ERROR_QUERY_HOSTINFO_MSG+": "+err.Error(), nil) return } logger.Debug("the host info query reply:", zap.Any("hqueryReply", hqueryReply), zap.String("pagetoken", pagetoken)) //convert to readable string. queryResult := processHostData(hqueryReply, pagetoken) ReturnSuccess(ctx, common.SUCCESS_CODE, common.SUCCESS_MSG, queryResult) } func selectFields(host interface{}, fields []string) map[string]interface{} { result := make(map[string]interface{}) v := reflect.ValueOf(host) t := v.Type() // Create a map of lowercase field names to actual field names fieldMap := make(map[string]string) // Handle embedded BasicHost fields if t.Kind() == reflect.Struct { for i := 0; i < t.NumField(); i++ { field := t.Field(i) // If it's an embedded struct (BasicHost) if field.Anonymous { embedValue := v.Field(i) embedType := embedValue.Type() for j := 0; j < embedType.NumField(); j++ { embedField := embedType.Field(j) fieldMap[strings.ToLower(embedField.Name)] = embedField.Name } } // Add the field itself fieldMap[strings.ToLower(field.Name)] = field.Name } } for _, field := range fields { lowerField := strings.ToLower(field) if actualField, ok := fieldMap[lowerField]; ok { var value interface{} var jsonTag string // If not found in BasicHost, try the main struct if v.FieldByName(actualField).IsValid() { value = v.FieldByName(actualField).Interface() f, _ := t.FieldByName(actualField) jsonTag = f.Tag.Get("json") } if value != nil { result[jsonTag] = value } } } return result } func (h HostHandler) GetHostByHostname(ctx *gin.Context) { logger.Debug("Enter GetHostByHostname() function from ", zap.String("client_ip", ctx.ClientIP())) // ---- check parameters ---- for param := range ctx.Request.URL.Query() { if !utils.ContainsField(utils.SupportedQueryParams, param) { err := errors.New("invalid specific host info query parameters of : " + param) logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } } hostNamesStr := ctx.Param("hostname") fieldsStr := ctx.Query("fields") logger.Debug("origin query string hostname, fieldsStr values are:", zap.String("hostname", hostNamesStr), zap.String("fields", fieldsStr)) //add host_name list as filter. hostNames := strings.Split(hostNamesStr, ",") mergeHqueryReply := make([]utils.OrderMap, 0) var page_token = "" for _, hostname := range hostNames { hostname = strings.TrimSpace(hostname) filters := make([]utils.FilterCondition, 0) sortOrders := make([]utils.SortOrder, 0) filters = append(filters, utils.FilterCondition{ Field: "host_name", Operator: "eq", Value: hostname, }) //filtercds := make([]utils.Condition, len(filters)) for _, filter := range filters { // check the filter is supported. if !utils.ContainsField(models.HOST_SUPPORT_FILTERS, filter.Field) { err := errors.New("unsupported filter: " + filter.Field) logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } } var host models.HostInfo var err error filtercds, err := utils.ConvertToNewFilter(filters, &host) if err != nil { logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } var fields []string if fieldsStr != "" { fields = utils.ParseFields(fieldsStr) found := false // verify the fields is in supported list supportedFields := utils.GetSupportedFields(models.HostInfo{}) logger.Debug("host supportedFields: %v", zap.Any("supportedFields", supportedFields)) for _, field := range fields { if !utils.ContainsField(supportedFields, field) { err := errors.New("unsupported query field: " + field) logger.Error(err.Error()) ReturnError(ctx, common.INVALID_PARAMS_CODE, common.INVALID_PARAMS_MSG+": "+err.Error(), nil) return } // if user not specify host_name in fields, add host_name as default field. if field == "host_name" { found = true } } if !found { logger.Debug("user doesn't specify host_name in query fields, add it.") fields = append(fields, "host_name") } } else { // default display columns fields = models.HOST_DEFAULT_COLUMN } // add a default order: sort=host_name:asc sortOrders = append(sortOrders, utils.SortOrder{ Column: "host_name", Ascending: true, }) queryOpts, err := utils.BuildQueryOptions(filtercds, nil, sortOrders, utils.PER_PAGE_LIMIT, "") if err != nil { logger.Error(err.Error()) ReturnError(ctx, common.ERROR_CODE, common.ERROR_QUERY_JOBINFO_MSG+": "+err.Error(), nil) return } hqueryReply, page_token, err := services.QueryData(services.TableHostInfo, queryOpts, fields) if err != nil { logger.Error("Failed to query the specific host info.", zap.Error(err)) ReturnError(ctx, common.ERROR_CODE, common.ERROR_QUERY_HOSTINFO_MSG+": "+err.Error(), nil) return } logger.Debug("specific host query reply:", zap.Any("hqueryReply", hqueryReply), zap.String("pagetoken", page_token)) mergeHqueryReply = append(mergeHqueryReply, hqueryReply...) } // end for hostname logger.Debug("the specific host query reply:", zap.Any("hqueryReply", mergeHqueryReply)) //convert host_status to readable string. queryResult := processHostData(mergeHqueryReply, page_token) ReturnSuccess(ctx, common.SUCCESS_CODE, common.SUCCESS_MSG, queryResult) } func preCheckHostRequest(req *models.ResReqSelectRequest) error { if strings.TrimSpace(req.Select) != "" { if len(req.Select) > 4094 { return errors.New("Select string exceeds the limit of 4094 characters") } } else { return errors.New("Select string cannot be empty") } return nil } func (h *HostHandler) FilterHostByResReqSelect(ctx *gin.Context) { logger.Debug("Enter FilterHostByResReqSelect() function") // Resource requirement select filtering is not yet implemented without backend. queryResult := processHostData([]utils.OrderMap{}, "") ReturnSuccess(ctx, common.SUCCESS_CODE, common.SUCCESS_MSG, queryResult) } // ControlHost func (h *HostHandler) ControlHost(ctx *gin.Context) { logger.Debug("Enter ControlHost() function") var req models.HostControlRequest // use strict json format check if err := utils.BindJSONStrict(ctx, &req); err != nil { logger.Error("Invalid request parameters", zap.Error(err)) ReturnError(ctx, common.ERROR_CODE, err.Error(), nil) return } allFlag, err := preCheckHostControlRequest(&req) if err != nil { logger.Error("Failed to send host control request", zap.Error(err)) ReturnError(ctx, common.ERROR_CODE, err.Error(), nil) return } baseAction := req.ControlAction logger.Debug("ControlHost baseAction is ", zap.Any("baseAction", baseAction)) // get the request submit user token := ctx.GetHeader("Authorization") _, err = GetUsernameFromToken(token) if err != nil { ReturnError(ctx, common.ERROR_CODE, err.Error(), nil) return } hostActionMap := map[string]int{ "open": models.HOST_OPEN, "close": models.HOST_CLOSE, } actionType, exists := hostActionMap[baseAction] if !exists { logger.Error("Unsupported action type", zap.String("baseAction", baseAction)) ReturnError(ctx, common.INVALID_PARAMS_CODE, "Unsupported action type: "+baseAction, nil) return } successHosts := []map[string]interface{}{} failedHosts := []map[string]interface{}{} validatedSuccessHosts, validatedFailedHosts := validateHost(ctx, actionType, req.Hostname) if ctx.IsAborted() { ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil) return } if allFlag { hostnames := store.GetAllHostNames() validatedSuccessHosts, validatedFailedHosts = validateHost(ctx, actionType, hostnames) } for _, hostname := range validatedSuccessHosts { successHosts = append(successHosts, map[string]interface{}{ "host_name": hostname, "msg": "host is being controlled", }) } for hostname, reason := range validatedFailedHosts { failedHosts = append(failedHosts, map[string]interface{}{ "host_name": hostname, "msg": reason, }) } for _, host := range successHosts { hostname := host["host_name"].(string) err = backend.ControlHost(hostname, actionType) if err != nil { logger.Error("Failed to send host control", zap.Error(err)) failedHosts = append(failedHosts, map[string]interface{}{ "host_name": hostname, "msg": "failed to control host: " + err.Error(), }) } } response, successMsg := BuildBatchOperationResponse(baseAction, "Host", successHosts, failedHosts) overallStatus := response["status"].(string) logger.Info("Finished processing host control request", zap.String("action", baseAction), zap.String("overallStatus", overallStatus), zap.Int("succeededCount", len(successHosts)), zap.Int("failedCount", len(failedHosts)), zap.String("client_ip", ctx.ClientIP())) ReturnSuccess(ctx, common.ACCEPTED, successMsg, response) return } // validateHost validates the hostname in the request func validateHost(ctx *gin.Context, control int, hostnames []string) (successHosts []string, failedHosts map[string]string) { logger.Debug("Validating hostnames: %v", zap.Any("hostnames", hostnames)) // get logon user token := ctx.GetHeader("Authorization") logonUser, err := GetUsernameFromToken(token) if err != nil { ReturnError(ctx, common.ERROR_CODE, err.Error(), nil) return } successHosts = []string{} failedHosts = make(map[string]string) if len(hostnames) == 0 { logger.Error("Hostnames cannot be empty") return } for _, hostname := range hostnames { if hostname == "" { logger.Error("Hostname cannot be empty") continue } hostPermission, err := GetRequireResourcePermission(logonUser, "host:control") if err != nil { logger.Error(err.Error()) ReturnError(ctx, common.ERROR_AUTH_CODE, common.ERROR_ACCESS_FORBIDDEN_MSG, nil) return } if hostPermission.DomainType == models.DomainTypeHostGroup { // TODO: check hostgroup values } if store.Hosts.Has(hostname) { logger.Debug("Found hostname in store", zap.String("hostname", hostname)) successHosts = append(successHosts, hostname) /* no need to check host_status if rawStatus, exists := hostInfo[0].Data["host_status"]; exists { if hostStatus, ok := rawStatus.(int64); ok { db_host_status := int(hostStatus) if isHostControlActionAllowed(control, db_host_status) { successHosts = append(successHosts, hostname) } else { logger.Error("action not allowed for host", zap.String("hostname", hostname)) failedHosts[hostname] = "action not allowed for current host_status" } } else { logger.Error("host_status is not an int64", zap.Any("host_status", rawStatus)) failedHosts[hostname] = "invalid host_status type for host" } } else { logger.Error("host_status not found in hostInfo", zap.String("hostname", hostname)) failedHosts[hostname] = "host_status not found for host" } */ } else { logger.Error("invalid hostname", zap.String("hostname", hostname)) failedHosts[hostname] = common.ERROR_HOST_NOT_FOUND_MSG } } return } func isHostControlActionAllowed(actionType int, status int) bool { isClosed := (status&models.HOSTSTAT["CLOSED"]) != 0 || (status&models.HOSTSTAT["CLOSED_BY_ADMIN"]) != 0 switch actionType { case 1: // open return isClosed case 2: // close return !isClosed default: return false } } func preCheckHostControlRequest(req *models.HostControlRequest) (bool, error) { logger.Debug("Enter preCheckHostControlRequest") if err := utils.ValidateControlAction(req.ControlAction, models.SupportedHostActions); err != nil { return false, err } req.ControlAction = strings.ToLower(strings.TrimSpace(req.ControlAction)) isAll := req.Hostname != nil && len(req.Hostname) == 1 && req.Hostname[0] == "all" if req.Comment != "" { if len(req.Comment) > 512 { return false, errors.New("comment length exceeds the limit of 512 characters") } } return isAll, nil } // hostSlotUsage holds counts of slots currently occupied on one host, // broken down by run/suspend sub-state, mirroring HostInfo.NumRun/ // NumSSusp/NumUSusp. type hostSlotUsage struct { Run, SSusp, USusp int } // execHostsJSON mirrors the JSONB shape backend/convert_pure.go's // buildJobInfoFromReplayJob() writes into JobInfo.ExecHosts. type execHostsJSON struct { HostSlots []struct { HostName string `json:"host_name"` NumSlots int `json:"num_slots"` } `json:"host_slots"` } // computeHostSlotUsage derives per-host slot usage from the live job store, // since store.Hosts' own NumRun/NumSSusp/NumUSusp fields are never written // (bug-1743): a host's occupied-slot count can only be known by scanning // which non-terminal jobs are currently placed on it. func computeHostSlotUsage() map[string]hostSlotUsage { usage := make(map[string]hostSlotUsage) for _, job := range store.Jobs.List() { if job.ExecHosts.String == "" { continue } var parsed execHostsJSON if err := json.Unmarshal([]byte(job.ExecHosts.String), &parsed); err != nil { continue } for _, hs := range parsed.HostSlots { u := usage[hs.HostName] switch { case job.JobStatus&models.JOBSTAT["USUSP"] != 0: u.USusp += hs.NumSlots case job.JobStatus&models.JOBSTAT["SSUSP"] != 0: u.SSusp += hs.NumSlots case job.JobStatus&models.JOBSTAT["RUN"] != 0: u.Run += hs.NumSlots default: continue } usage[hs.HostName] = u } } return usage } func processHostData(rows []utils.OrderMap, page_token string) utils.QueryResult { slotUsage := computeHostSlotUsage() for _, row := range rows { if v, exist := row.Data["host_status"]; exist { if v != nil { hostStatus, _ := utils.IntOrInt64(v) row.Data["host_status"] = models.HostStateStr(hostStatus) } } // bug-1743: HostInfo.NumRun/.NumSSusp/.NumUSusp are never assigned // anywhere else -- derive them here from the live job store so // `hosts list` reflects actual slot usage instead of always 0. if hostName, ok := row.Data["host_name"].(string); ok { usage := slotUsage[hostName] row.Data["num_run"] = usage.Run row.Data["num_ssusp"] = usage.SSusp row.Data["num_ususp"] = usage.USusp } // Process JSONB fields based on predefined list for _, jsonbField := range models.JSONBHFields { if v, exist := row.Data[jsonbField]; exist { if strValue, ok := v.(string); ok { if jsonbData, err := utils.ParseJSONB(strValue); err == nil { row.Data[jsonbField] = jsonbData } else { logger.Error("Failed to parse host info JSONB field.", zap.Error(err)) } } } } //utils.FormatRowsOutput(row.Data) } return utils.QueryResult{ Rows: rows, Count: len(rows), Pagetoken: page_token, } }