// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. // All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package routerConfig import ( "fmt" "net/http" "net/url" "strconv" "strings" "time" "github.com/ClusterCockpit/cc-backend/internal/api" "github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/repository" "github.com/ClusterCockpit/cc-backend/pkg/archive" "github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/web" "github.com/gorilla/mux" ) type InfoType map[string]interface{} type Route struct { Route string Template string Title string Filter bool Setup func(i InfoType, r *http.Request) InfoType } var routes []Route = []Route{ {"/", "home.tmpl", "ClusterCockpit", false, setupHomeRoute}, {"/config", "config.tmpl", "Settings", false, func(i InfoType, r *http.Request) InfoType { return i }}, {"/monitoring/jobs/", "monitoring/jobs.tmpl", "Jobs - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { return i }}, {"/monitoring/job/{id:[0-9]+}", "monitoring/job.tmpl", "Job - ClusterCockpit", false, setupJobRoute}, {"/monitoring/users/", "monitoring/list.tmpl", "Users - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "USER"; return i }}, {"/monitoring/projects/", "monitoring/list.tmpl", "Projects - ClusterCockpit", true, func(i InfoType, r *http.Request) InfoType { i["listType"] = "PROJECT"; return i }}, {"/monitoring/tags/", "monitoring/taglist.tmpl", "Tags - ClusterCockpit", false, setupTaglistRoute}, {"/monitoring/user/{id}", "monitoring/user.tmpl", "User - ClusterCockpit", true, setupUserRoute}, {"/monitoring/systems/{cluster}", "monitoring/systems.tmpl", "Cluster - ClusterCockpit", false, setupClusterRoute}, {"/monitoring/node/{cluster}/{hostname}", "monitoring/node.tmpl", "Node - ClusterCockpit", false, setupNodeRoute}, {"/monitoring/analysis/{cluster}", "monitoring/analysis.tmpl", "Analysis - ClusterCockpit", true, setupAnalysisRoute}, {"/monitoring/status/{cluster}", "monitoring/status.tmpl", "Status of - ClusterCockpit", false, setupClusterRoute}, } func setupHomeRoute(i InfoType, r *http.Request) InfoType { type cluster struct { Name string RunningJobs int TotalJobs int RecentShortJobs int } jobRepo := repository.GetJobRepository() runningJobs, err := jobRepo.CountGroupedJobs(r.Context(), model.AggregateCluster, []*model.JobFilter{{ State: []schema.JobState{schema.JobStateRunning}, }}, nil, nil) if err != nil { log.Warnf("failed to count jobs: %s", err.Error()) runningJobs = map[string]int{} } totalJobs, err := jobRepo.CountGroupedJobs(r.Context(), model.AggregateCluster, nil, nil, nil) if err != nil { log.Warnf("failed to count jobs: %s", err.Error()) totalJobs = map[string]int{} } from := time.Now().Add(-24 * time.Hour) recentShortJobs, err := jobRepo.CountGroupedJobs(r.Context(), model.AggregateCluster, []*model.JobFilter{{ StartTime: &schema.TimeRange{From: &from, To: nil}, Duration: &schema.IntRange{From: 0, To: repository.ShortJobDuration}, }}, nil, nil) if err != nil { log.Warnf("failed to count jobs: %s", err.Error()) recentShortJobs = map[string]int{} } clusters := make([]cluster, 0) for _, c := range archive.Clusters { clusters = append(clusters, cluster{ Name: c.Name, RunningJobs: runningJobs[c.Name], TotalJobs: totalJobs[c.Name], RecentShortJobs: recentShortJobs[c.Name], }) } i["clusters"] = clusters return i } func setupJobRoute(i InfoType, r *http.Request) InfoType { i["id"] = mux.Vars(r)["id"] return i } func setupUserRoute(i InfoType, r *http.Request) InfoType { jobRepo := repository.GetJobRepository() username := mux.Vars(r)["id"] i["id"] = username i["username"] = username // TODO: If forbidden (== err exists), redirect to error page if user, _ := auth.FetchUser(r.Context(), jobRepo.DB, username); user != nil { i["name"] = user.Name i["email"] = user.Email } return i } func setupClusterRoute(i InfoType, r *http.Request) InfoType { vars := mux.Vars(r) i["id"] = vars["cluster"] i["cluster"] = vars["cluster"] from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") if from != "" || to != "" { i["from"] = from i["to"] = to } return i } func setupNodeRoute(i InfoType, r *http.Request) InfoType { vars := mux.Vars(r) i["cluster"] = vars["cluster"] i["hostname"] = vars["hostname"] i["id"] = fmt.Sprintf("%s (%s)", vars["cluster"], vars["hostname"]) from, to := r.URL.Query().Get("from"), r.URL.Query().Get("to") if from != "" || to != "" { i["from"] = from i["to"] = to } return i } func setupAnalysisRoute(i InfoType, r *http.Request) InfoType { i["cluster"] = mux.Vars(r)["cluster"] return i } func setupTaglistRoute(i InfoType, r *http.Request) InfoType { var username *string = nil var projects *[]string jobRepo := repository.GetJobRepository() user := auth.GetUser(r.Context()) if user != nil && user.HasNotRoles([]string{auth.RoleAdmin, auth.RoleSupport, auth.RoleManager}) { username = &user.Username } else if user != nil && user.HasRole(auth.RoleManager) { username = &user.Username projects = &user.Projects } // ADMINS && SUPPORT w/o additional conditions tags, counts, err := jobRepo.CountTags(username, projects) tagMap := make(map[string][]map[string]interface{}) if err != nil { log.Warnf("GetTags failed: %s", err.Error()) i["tagmap"] = tagMap return i } for _, tag := range tags { tagItem := map[string]interface{}{ "id": tag.ID, "name": tag.Name, "count": counts[tag.Name], } tagMap[tag.Type] = append(tagMap[tag.Type], tagItem) } i["tagmap"] = tagMap return i } func buildFilterPresets(query url.Values) map[string]interface{} { filterPresets := map[string]interface{}{} if query.Get("cluster") != "" { filterPresets["cluster"] = query.Get("cluster") } if query.Get("partition") != "" { filterPresets["partition"] = query.Get("partition") } if query.Get("project") != "" { filterPresets["project"] = query.Get("project") filterPresets["projectMatch"] = "eq" } if query.Get("jobName") != "" { filterPresets["jobName"] = query.Get("jobName") } if len(query["user"]) != 0 { if len(query["user"]) == 1 { filterPresets["user"] = query.Get("user") filterPresets["userMatch"] = "contains" } else { filterPresets["user"] = query["user"] filterPresets["userMatch"] = "in" } } if len(query["state"]) != 0 { filterPresets["state"] = query["state"] } if rawtags, ok := query["tag"]; ok { tags := make([]int, len(rawtags)) for i, tid := range rawtags { var err error tags[i], err = strconv.Atoi(tid) if err != nil { tags[i] = -1 } } filterPresets["tags"] = tags } if query.Get("duration") != "" { parts := strings.Split(query.Get("duration"), "-") if len(parts) == 2 { a, e1 := strconv.Atoi(parts[0]) b, e2 := strconv.Atoi(parts[1]) if e1 == nil && e2 == nil { filterPresets["duration"] = map[string]int{"from": a, "to": b} } } } if query.Get("numNodes") != "" { parts := strings.Split(query.Get("numNodes"), "-") if len(parts) == 2 { a, e1 := strconv.Atoi(parts[0]) b, e2 := strconv.Atoi(parts[1]) if e1 == nil && e2 == nil { filterPresets["numNodes"] = map[string]int{"from": a, "to": b} } } } if query.Get("numAccelerators") != "" { parts := strings.Split(query.Get("numAccelerators"), "-") if len(parts) == 2 { a, e1 := strconv.Atoi(parts[0]) b, e2 := strconv.Atoi(parts[1]) if e1 == nil && e2 == nil { filterPresets["numAccelerators"] = map[string]int{"from": a, "to": b} } } } if query.Get("jobId") != "" { filterPresets["jobId"] = query.Get("jobId") } if query.Get("arrayJobId") != "" { if num, err := strconv.Atoi(query.Get("arrayJobId")); err == nil { filterPresets["arrayJobId"] = num } } if query.Get("startTime") != "" { parts := strings.Split(query.Get("startTime"), "-") if len(parts) == 2 { a, e1 := strconv.ParseInt(parts[0], 10, 64) b, e2 := strconv.ParseInt(parts[1], 10, 64) if e1 == nil && e2 == nil { filterPresets["startTime"] = map[string]string{ "from": time.Unix(a, 0).Format(time.RFC3339), "to": time.Unix(b, 0).Format(time.RFC3339), } } } } return filterPresets } func SetupRoutes(router *mux.Router, version string, hash string, buildTime string) { userCfgRepo := repository.GetUserCfgRepo() for _, route := range routes { route := route router.HandleFunc(route.Route, func(rw http.ResponseWriter, r *http.Request) { conf, err := userCfgRepo.GetUIConfig(auth.GetUser(r.Context())) if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } title := route.Title infos := route.Setup(map[string]interface{}{}, r) if id, ok := infos["id"]; ok { title = strings.Replace(route.Title, "", id.(string), 1) } username, authLevel := "", 0 if user := auth.GetUser(r.Context()); user != nil { username = user.Username authLevel = user.GetAuthLevel() } page := web.Page{ Title: title, User: web.User{Username: username, AuthLevel: authLevel}, Build: web.Build{Version: version, Hash: hash, Buildtime: buildTime}, Config: conf, Infos: infos, } if route.Filter { page.FilterPresets = buildFilterPresets(r.URL.Query()) } web.RenderTemplate(rw, r, route.Template, &page) }) } } func HandleSearchBar(rw http.ResponseWriter, r *http.Request, api *api.RestApi) { if search := r.URL.Query().Get("searchId"); search != "" { splitSearch := strings.Split(search, ":") if len(splitSearch) == 2 { switch strings.Trim(splitSearch[0], " ") { case "jobId": http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery return case "jobName": http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // All Users: Redirect to Tablequery return case "projectId": project, _ := api.JobRepository.FindProject(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: projectId if project != "" { http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(project), http.StatusTemporaryRedirect) return } else { http.Redirect(rw, r, "/monitoring/jobs/?jobId=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table } case "username": usernames, _ := api.JobRepository.FindUsers(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: usernames if len(usernames) == 1 { http.Redirect(rw, r, "/monitoring/user/"+usernames[0], http.StatusTemporaryRedirect) // One Match: Redirect to User View return } else if len(usernames) > 1 { http.Redirect(rw, r, "/monitoring/users/?user="+url.QueryEscape(strings.Trim(splitSearch[1], " ")), http.StatusTemporaryRedirect) // > 1 Matches: Redirect to user table return } else { http.Redirect(rw, r, "/monitoring/users/?user=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table } case "name": usernames, _ := api.JobRepository.FindUsersByName(r.Context(), strings.Trim(splitSearch[1], " ")) // Restricted: usernames queried by name if len(usernames) == 1 { http.Redirect(rw, r, "/monitoring/user/"+usernames[0], http.StatusTemporaryRedirect) return } else if len(usernames) > 1 { joinedNames := strings.Join(usernames, "&user=") http.Redirect(rw, r, "/monitoring/users/?user="+joinedNames, http.StatusTemporaryRedirect) // > 1 Matches: Redirect to user table return } else { http.Redirect(rw, r, "/monitoring/users/?user=NotFound", http.StatusTemporaryRedirect) // Workaround to display correctly empty table } default: http.Error(rw, "'searchId' type parameter unknown", http.StatusBadRequest) } } else if len(splitSearch) == 1 { jobname, username, project, err := api.JobRepository.FindJobnameOrUserOrProject(r.Context(), strings.Trim(search, " ")) // Determine Access within if err != nil { http.Error(rw, err.Error(), http.StatusInternalServerError) return } if username != "" { http.Redirect(rw, r, "/monitoring/user/"+username, http.StatusTemporaryRedirect) // User: Redirect to user page return } else if project != "" { http.Redirect(rw, r, "/monitoring/jobs/?projectMatch=eq&project="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // projectId (equal) return } else if jobname != "" { http.Redirect(rw, r, "/monitoring/jobs/?jobName="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // JobName (contains) return } else { http.Redirect(rw, r, "/monitoring/jobs/?jobId="+url.QueryEscape(strings.Trim(search, " ")), http.StatusTemporaryRedirect) // No Result: Probably jobId return } } else { http.Error(rw, "'searchId' query parameter malformed", http.StatusBadRequest) } } else { http.Redirect(rw, r, "/monitoring/jobs/?", http.StatusTemporaryRedirect) } }