2021-03-31 07:23:48 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2021-10-26 10:24:43 +02:00
|
|
|
"encoding/json"
|
2021-10-11 11:11:14 +02:00
|
|
|
"flag"
|
2021-12-08 15:50:03 +01:00
|
|
|
"fmt"
|
2021-03-31 07:23:48 +02:00
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
|
|
|
|
"github.com/99designs/gqlgen/graphql/handler"
|
|
|
|
"github.com/99designs/gqlgen/graphql/playground"
|
2021-12-08 10:15:25 +01:00
|
|
|
"github.com/ClusterCockpit/cc-jobarchive/auth"
|
2021-10-26 10:24:43 +02:00
|
|
|
"github.com/ClusterCockpit/cc-jobarchive/config"
|
2021-03-31 08:50:53 +02:00
|
|
|
"github.com/ClusterCockpit/cc-jobarchive/graph"
|
|
|
|
"github.com/ClusterCockpit/cc-jobarchive/graph/generated"
|
2021-10-26 10:24:43 +02:00
|
|
|
"github.com/ClusterCockpit/cc-jobarchive/metricdata"
|
2021-12-08 10:15:25 +01:00
|
|
|
"github.com/ClusterCockpit/cc-jobarchive/templates"
|
2021-03-31 07:23:48 +02:00
|
|
|
"github.com/gorilla/handlers"
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
)
|
|
|
|
|
2021-11-26 10:34:29 +01:00
|
|
|
var db *sqlx.DB
|
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
type ProgramConfig struct {
|
|
|
|
Addr string `json:"addr"`
|
|
|
|
DisableAuthentication bool `json:"disable-authentication"`
|
|
|
|
StaticFiles string `json:"static-files"`
|
|
|
|
DB string `json:"db"`
|
|
|
|
JobArchive string `json:"job-archive"`
|
|
|
|
LdapConfig *auth.LdapConfig `json:"ldap"`
|
|
|
|
HttpsCertFile string `json:"https-cert-file"`
|
|
|
|
HttpsKeyFile string `json:"https-key-file"`
|
|
|
|
UiDefaults map[string]interface{} `json:"ui-defaults"`
|
|
|
|
}
|
2021-03-31 07:23:48 +02:00
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
var programConfig ProgramConfig = ProgramConfig{
|
|
|
|
Addr: "0.0.0.0:8080",
|
|
|
|
DisableAuthentication: false,
|
|
|
|
StaticFiles: "./frontend/public",
|
|
|
|
DB: "./var/job.db",
|
|
|
|
JobArchive: "./var/job-archive",
|
|
|
|
LdapConfig: &auth.LdapConfig{
|
|
|
|
Url: "ldap://localhost",
|
|
|
|
UserBase: "ou=hpc,dc=rrze,dc=uni-erlangen,dc=de",
|
|
|
|
SearchDN: "cn=admin,dc=rrze,dc=uni-erlangen,dc=de",
|
|
|
|
UserBind: "uid={username},ou=hpc,dc=rrze,dc=uni-erlangen,dc=de",
|
|
|
|
UserFilter: "(&(objectclass=posixAccount)(uid=*))",
|
|
|
|
},
|
|
|
|
HttpsCertFile: "",
|
|
|
|
HttpsKeyFile: "",
|
|
|
|
UiDefaults: map[string]interface{}{
|
|
|
|
"analysis_view_histogramMetrics": []string{"flops_any", "mem_bw", "mem_used"},
|
|
|
|
"analysis_view_scatterPlotMetrics": [][]string{{"flops_any", "mem_bw"}, {"flops_any", "cpu_load"}, {"cpu_load", "mem_bw"}},
|
|
|
|
"job_view_nodestats_selectedMetrics": []string{"flops_any", "mem_bw", "mem_used"},
|
|
|
|
"job_view_polarPlotMetrics": []string{"flops_any", "mem_bw", "mem_used", "net_bw", "file_bw"},
|
|
|
|
"job_view_selectedMetrics": []string{"flops_any", "mem_bw", "mem_used"},
|
|
|
|
"plot_general_colorBackground": true,
|
|
|
|
"plot_general_colorscheme": []string{"#00bfff", "#0000ff", "#ff00ff", "#ff0000", "#ff8000", "#ffff00", "#80ff00"},
|
|
|
|
"plot_general_lineWidth": 1,
|
|
|
|
"plot_list_jobsPerPage": 10,
|
|
|
|
"plot_list_selectedMetrics": []string{"cpu_load", "mem_used", "flops_any", "mem_bw", "clock"},
|
|
|
|
"plot_view_plotsPerRow": 4,
|
|
|
|
"plot_view_showPolarplot": true,
|
|
|
|
"plot_view_showRoofline": true,
|
|
|
|
"plot_view_showStatTable": true,
|
|
|
|
},
|
|
|
|
}
|
2021-10-26 10:24:43 +02:00
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
func main() {
|
|
|
|
var flagReinitDB, flagStopImmediately, flagSyncLDAP bool
|
|
|
|
var flagConfigFile string
|
|
|
|
var flagNewUser, flagDelUser string
|
|
|
|
flag.BoolVar(&flagReinitDB, "init-db", false, "Go through job-archive and re-initialize `job`, `tag`, and `jobtag` tables")
|
|
|
|
flag.BoolVar(&flagSyncLDAP, "sync-ldap", false, "Sync the `user` table with ldap")
|
|
|
|
flag.BoolVar(&flagStopImmediately, "no-server", false, "Do not start a server, stop right after initialization and argument handling")
|
|
|
|
flag.StringVar(&flagConfigFile, "config", "", "Location of the config file for this server (overwrites the defaults)")
|
|
|
|
flag.StringVar(&flagNewUser, "add-user", "", "Add a new user. Argument format: `<username>:[admin]:<password>`")
|
|
|
|
flag.StringVar(&flagDelUser, "del-user", "", "Remove user by username")
|
|
|
|
flag.Parse()
|
2021-03-31 07:23:48 +02:00
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
if flagConfigFile != "" {
|
|
|
|
data, err := os.ReadFile(flagConfigFile)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &programConfig); err != nil {
|
2021-10-11 11:11:14 +02:00
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2021-10-26 10:24:43 +02:00
|
|
|
}
|
2021-10-11 11:11:14 +02:00
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
var err error
|
|
|
|
db, err = sqlx.Open("sqlite3", programConfig.DB)
|
2021-10-26 10:24:43 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
2021-10-11 11:11:14 +02:00
|
|
|
}
|
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
// Initialize sub-modules...
|
2021-11-26 10:34:29 +01:00
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
if !programConfig.DisableAuthentication {
|
|
|
|
if err := auth.Init(db, programConfig.LdapConfig); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2021-03-31 07:23:48 +02:00
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
if flagNewUser != "" {
|
|
|
|
if err := auth.AddUserToDB(db, flagNewUser); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if flagDelUser != "" {
|
|
|
|
if err := auth.DelUserFromDB(db, flagDelUser); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
}
|
2021-11-26 10:34:29 +01:00
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
if flagSyncLDAP {
|
|
|
|
auth.SyncWithLDAP(db)
|
|
|
|
}
|
|
|
|
} else if flagNewUser != "" || flagDelUser != "" {
|
|
|
|
log.Fatalln("arguments --add-user and --del-user can only be used if authentication is enabled")
|
2021-09-21 16:06:41 +02:00
|
|
|
}
|
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
if err := config.Init(db, !programConfig.DisableAuthentication, programConfig.UiDefaults, programConfig.JobArchive); err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
2021-10-26 10:24:43 +02:00
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
if err := metricdata.Init(programConfig.JobArchive); err != nil {
|
|
|
|
log.Fatal(err)
|
2021-10-26 10:24:43 +02:00
|
|
|
}
|
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
if flagReinitDB {
|
|
|
|
if err := initDB(db, programConfig.JobArchive); err != nil {
|
|
|
|
log.Fatal(err)
|
2021-10-26 10:24:43 +02:00
|
|
|
}
|
2021-12-08 10:15:25 +01:00
|
|
|
}
|
2021-10-26 10:24:43 +02:00
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
if flagStopImmediately {
|
|
|
|
return
|
|
|
|
}
|
2021-10-26 10:24:43 +02:00
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
// Build routes...
|
2021-10-26 10:24:43 +02:00
|
|
|
|
2021-12-08 15:50:03 +01:00
|
|
|
resolver := &graph.Resolver{DB: db}
|
|
|
|
graphQLEndpoint := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: resolver}))
|
2021-12-08 10:15:25 +01:00
|
|
|
graphQLPlayground := playground.Handler("GraphQL playground", "/query")
|
|
|
|
|
|
|
|
handleGetLogin := func(rw http.ResponseWriter, r *http.Request) {
|
2021-12-08 15:50:03 +01:00
|
|
|
templates.Render(rw, r, "login", &templates.Page{
|
2021-12-08 10:15:25 +01:00
|
|
|
Title: "Login",
|
|
|
|
Login: &templates.LoginPage{},
|
|
|
|
})
|
2021-10-26 10:24:43 +02:00
|
|
|
}
|
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
r := mux.NewRouter()
|
|
|
|
r.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
2021-12-08 15:50:03 +01:00
|
|
|
templates.Render(rw, r, "404", &templates.Page{
|
2021-12-08 10:15:25 +01:00
|
|
|
Title: "Not found",
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
r.Handle("/playground", graphQLPlayground)
|
|
|
|
r.Handle("/login", auth.Login(db)).Methods(http.MethodPost)
|
|
|
|
r.HandleFunc("/login", handleGetLogin).Methods(http.MethodGet)
|
|
|
|
r.HandleFunc("/logout", auth.Logout).Methods(http.MethodPost)
|
|
|
|
|
|
|
|
secured := r.PathPrefix("/").Subrouter()
|
|
|
|
if !programConfig.DisableAuthentication {
|
|
|
|
secured.Use(auth.Auth)
|
|
|
|
}
|
|
|
|
secured.Handle("/query", graphQLEndpoint)
|
|
|
|
secured.HandleFunc("/api/jobs/start_job/", startJob).Methods(http.MethodPost)
|
|
|
|
secured.HandleFunc("/api/jobs/stop_job/", stopJob).Methods(http.MethodPost, http.MethodPut)
|
2021-12-08 15:50:03 +01:00
|
|
|
secured.HandleFunc("/api/jobs/stop_job/{id:[0-9]+}", stopJob).Methods(http.MethodPost, http.MethodPut)
|
2021-12-08 10:15:25 +01:00
|
|
|
secured.HandleFunc("/config.json", config.ServeConfig).Methods(http.MethodGet)
|
|
|
|
|
2021-12-08 15:50:03 +01:00
|
|
|
secured.HandleFunc("/", func(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
templates.Render(rw, r, "home", &templates.Page{
|
|
|
|
Title: "ClusterCockpit",
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
monitoringRoutes(secured, resolver)
|
|
|
|
|
2021-12-08 10:15:25 +01:00
|
|
|
r.PathPrefix("/").Handler(http.FileServer(http.Dir(programConfig.StaticFiles)))
|
|
|
|
handler := handlers.CORS(
|
|
|
|
handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
|
|
|
|
handlers.AllowedMethods([]string{"GET", "POST", "HEAD", "OPTIONS"}),
|
|
|
|
handlers.AllowedOrigins([]string{"*"}))(handlers.LoggingHandler(os.Stdout, r))
|
|
|
|
|
|
|
|
// Start http or https server
|
|
|
|
if programConfig.HttpsCertFile != "" && programConfig.HttpsKeyFile != "" {
|
|
|
|
log.Printf("HTTPS server running at %s...", programConfig.Addr)
|
|
|
|
err = http.ListenAndServeTLS(programConfig.Addr, programConfig.HttpsCertFile, programConfig.HttpsKeyFile, handler)
|
|
|
|
} else {
|
|
|
|
log.Printf("HTTP server running at %s...", programConfig.Addr)
|
|
|
|
err = http.ListenAndServe(programConfig.Addr, handler)
|
|
|
|
}
|
|
|
|
log.Fatal(err)
|
2021-10-26 10:24:43 +02:00
|
|
|
}
|
2021-12-08 15:50:03 +01:00
|
|
|
|
|
|
|
func monitoringRoutes(router *mux.Router, resolver *graph.Resolver) {
|
|
|
|
router.HandleFunc("/monitoring/jobs/", func(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
conf, err := config.GetUIConfig(r)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
templates.Render(rw, r, "monitoring/jobs/", &templates.Page{
|
|
|
|
Title: "Jobs - ClusterCockpit",
|
|
|
|
Config: conf,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
router.HandleFunc("/monitoring/job/{id:[0-9]+}", func(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
conf, err := config.GetUIConfig(r)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
id := mux.Vars(r)["id"]
|
|
|
|
job, err := resolver.Query().Job(r.Context(), id)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, err.Error(), http.StatusNotFound)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
templates.Render(rw, r, "monitoring/job/", &templates.Page{
|
|
|
|
Title: fmt.Sprintf("Job %s - ClusterCockpit", job.JobID),
|
|
|
|
Config: conf,
|
|
|
|
Infos: map[string]interface{}{
|
|
|
|
"id": id,
|
|
|
|
"jobId": job.JobID,
|
|
|
|
"clusterId": job.ClusterID,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
router.HandleFunc("/monitoring/users/", func(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
conf, err := config.GetUIConfig(r)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
templates.Render(rw, r, "monitoring/users/", &templates.Page{
|
|
|
|
Title: "Users - ClusterCockpit",
|
|
|
|
Config: conf,
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
router.HandleFunc("/monitoring/user/{id}", func(rw http.ResponseWriter, r *http.Request) {
|
|
|
|
conf, err := config.GetUIConfig(r)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(rw, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
id := mux.Vars(r)["id"]
|
|
|
|
// TODO: One could check if the user exists, but that would be unhelpfull if authentication
|
|
|
|
// is disabled or the user does not exist but has started jobs.
|
|
|
|
|
|
|
|
templates.Render(rw, r, "monitoring/user/", &templates.Page{
|
|
|
|
Title: fmt.Sprintf("User %s - ClusterCockpit", id),
|
|
|
|
Config: conf,
|
|
|
|
Infos: map[string]interface{}{
|
|
|
|
"userId": id,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|