commit
c3f0554dd3
@ -445,7 +445,7 @@ func GetFullNodeAPIV1LotusProvider(ctx *cli.Context, ainfoCfg []string, opts ...
|
||||
for _, head := range heads {
|
||||
v1api, closer, err := client.NewFullNodeRPCV1(ctx.Context, head.addr, head.header, rpcOpts...)
|
||||
if err != nil {
|
||||
log.Warnf("Not able to establish connection to node with addr: %s", head.addr)
|
||||
log.Warnf("Not able to establish connection to node with addr: %s, Reason: %s", head.addr, err.Error())
|
||||
continue
|
||||
}
|
||||
fullNodes = append(fullNodes, v1api)
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/filecoin-project/lotus/cmd/lotus-provider/deps"
|
||||
"github.com/filecoin-project/lotus/lib/harmony/harmonydb"
|
||||
"github.com/filecoin-project/lotus/node/config"
|
||||
)
|
||||
|
||||
@ -108,9 +109,8 @@ var configSetCmd = &cli.Command{
|
||||
}
|
||||
_ = lp
|
||||
|
||||
_, err = db.Exec(context.Background(),
|
||||
`INSERT INTO harmony_config (title, config) VALUES ($1, $2)
|
||||
ON CONFLICT (title) DO UPDATE SET config = excluded.config`, name, string(bytes))
|
||||
err = setConfig(db, name, string(bytes))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to save config layer: %w", err)
|
||||
}
|
||||
@ -120,6 +120,13 @@ var configSetCmd = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func setConfig(db *harmonydb.DB, name, config string) error {
|
||||
_, err := db.Exec(context.Background(),
|
||||
`INSERT INTO harmony_config (title, config) VALUES ($1, $2)
|
||||
ON CONFLICT (title) DO UPDATE SET config = excluded.config`, name, config)
|
||||
return err
|
||||
}
|
||||
|
||||
var configGetCmd = &cli.Command{
|
||||
Name: "get",
|
||||
Aliases: []string{"cat", "show"},
|
||||
@ -135,8 +142,7 @@ var configGetCmd = &cli.Command{
|
||||
return err
|
||||
}
|
||||
|
||||
var cfg string
|
||||
err = db.QueryRow(context.Background(), `SELECT config FROM harmony_config WHERE title=$1`, args.First()).Scan(&cfg)
|
||||
cfg, err := getConfig(db, args.First())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -146,6 +152,15 @@ var configGetCmd = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
func getConfig(db *harmonydb.DB, layer string) (string, error) {
|
||||
var cfg string
|
||||
err := db.QueryRow(context.Background(), `SELECT config FROM harmony_config WHERE title=$1`, layer).Scan(&cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
var configListCmd = &cli.Command{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
@ -176,7 +177,11 @@ func (deps *Deps) PopulateRemainingDeps(ctx context.Context, cctx *cli.Context,
|
||||
|
||||
if deps.Full == nil {
|
||||
var fullCloser func()
|
||||
deps.Full, fullCloser, err = cliutil.GetFullNodeAPIV1LotusProvider(cctx, deps.Cfg.Apis.ChainApiInfo)
|
||||
cfgApiInfo := deps.Cfg.Apis.ChainApiInfo
|
||||
if v := os.Getenv("FULLNODE_API_INFO"); v != "" {
|
||||
cfgApiInfo = []string{v}
|
||||
}
|
||||
deps.Full, fullCloser, err = cliutil.GetFullNodeAPIV1LotusProvider(cctx, cfgApiInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -267,6 +272,7 @@ func GetConfig(cctx *cli.Context, db *harmonydb.DB) (*config.LotusProviderConfig
|
||||
for _, k := range meta.Keys() {
|
||||
have = append(have, strings.Join(k, " "))
|
||||
}
|
||||
log.Infow("Using layer", "layer", layer, "config", lp)
|
||||
}
|
||||
_ = have // FUTURE: verify that required fields are here.
|
||||
// If config includes 3rd-party config, consider JSONSchema as a way that
|
||||
|
@ -46,6 +46,7 @@ func main() {
|
||||
stopCmd,
|
||||
configCmd,
|
||||
testCmd,
|
||||
webCmd,
|
||||
//backupCmd,
|
||||
//lcli.WithCategory("chain", actorCmd),
|
||||
//lcli.WithCategory("storage", sectorsCmd),
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
logging "github.com/ipfs/go-log/v2"
|
||||
"go.opencensus.io/tag"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/filecoin-project/go-jsonrpc"
|
||||
@ -20,6 +21,7 @@ import (
|
||||
|
||||
"github.com/filecoin-project/lotus/api"
|
||||
"github.com/filecoin-project/lotus/cmd/lotus-provider/deps"
|
||||
"github.com/filecoin-project/lotus/cmd/lotus-provider/web"
|
||||
"github.com/filecoin-project/lotus/lib/rpcenc"
|
||||
"github.com/filecoin-project/lotus/metrics"
|
||||
"github.com/filecoin-project/lotus/metrics/proxy"
|
||||
@ -126,15 +128,29 @@ func ListenAndServe(ctx context.Context, dependencies *deps.Deps, shutdownChan c
|
||||
Addr: dependencies.ListenAddr,
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
log.Warn("Shutting down...")
|
||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
||||
log.Errorf("shutting down RPC server failed: %s", err)
|
||||
}
|
||||
log.Warn("Graceful shutdown successful")
|
||||
}()
|
||||
|
||||
log.Infof("Setting up RPC server at %s", dependencies.ListenAddr)
|
||||
return srv.ListenAndServe()
|
||||
eg := errgroup.Group{}
|
||||
eg.Go(srv.ListenAndServe)
|
||||
|
||||
if dependencies.Cfg.Subsystems.EnableWebGui {
|
||||
web, err := web.GetSrv(ctx, dependencies)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
log.Warn("Shutting down...")
|
||||
if err := srv.Shutdown(context.TODO()); err != nil {
|
||||
log.Errorf("shutting down RPC server failed: %s", err)
|
||||
}
|
||||
if err := web.Shutdown(context.Background()); err != nil {
|
||||
log.Errorf("shutting down web server failed: %s", err)
|
||||
}
|
||||
log.Warn("Graceful shutdown successful")
|
||||
}()
|
||||
log.Infof("Setting up web server at %s", dependencies.Cfg.Subsystems.GuiAddress)
|
||||
eg.Go(web.ListenAndServe)
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
"go.opencensus.io/stats"
|
||||
@ -19,6 +22,7 @@ import (
|
||||
"github.com/filecoin-project/lotus/lib/ulimit"
|
||||
"github.com/filecoin-project/lotus/metrics"
|
||||
"github.com/filecoin-project/lotus/node"
|
||||
"github.com/filecoin-project/lotus/node/config"
|
||||
)
|
||||
|
||||
type stackTracer interface {
|
||||
@ -113,10 +117,8 @@ var runCmd = &cli.Command{
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("before populateRemainingDeps")
|
||||
dependencies := &deps.Deps{}
|
||||
err = dependencies.PopulateRemainingDeps(ctx, cctx, true)
|
||||
fmt.Println("after popdeps")
|
||||
if err != nil {
|
||||
fmt.Println("err", err)
|
||||
return err
|
||||
@ -142,3 +144,51 @@ var runCmd = &cli.Command{
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var webCmd = &cli.Command{
|
||||
Name: "web",
|
||||
Usage: "Start lotus provider web interface",
|
||||
Description: `Start an instance of lotus provider web interface.
|
||||
This creates the 'web' layer if it does not exist, then calls run with that layer.`,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "listen",
|
||||
Usage: "Address to listen on",
|
||||
Value: "127.0.0.1:4701",
|
||||
},
|
||||
&cli.StringSliceFlag{
|
||||
Name: "layers",
|
||||
Usage: "list of layers to be interpreted (atop defaults). Default: base. Web will be added",
|
||||
Value: cli.NewStringSlice("base"),
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "nosync",
|
||||
Usage: "don't check full-node sync status",
|
||||
},
|
||||
},
|
||||
Action: func(cctx *cli.Context) error {
|
||||
db, err := deps.MakeDB(cctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
webtxt, err := getConfig(db, "web")
|
||||
if err != nil || webtxt == "" {
|
||||
cfg := config.DefaultLotusProvider()
|
||||
cfg.Subsystems.EnableWebGui = true
|
||||
var b bytes.Buffer
|
||||
if err = toml.NewEncoder(&b).Encode(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = setConfig(db, "web", b.String()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
layers := append([]string{"web"}, cctx.StringSlice("layers")...)
|
||||
err = cctx.Set("layers", strings.Join(layers, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runCmd.Action(cctx)
|
||||
},
|
||||
}
|
||||
|
229
cmd/lotus-provider/web/api/debug/debug.go
Normal file
229
cmd/lotus-provider/web/api/debug/debug.go
Normal file
@ -0,0 +1,229 @@
|
||||
// Package debug provides the API for various debug endpoints in lotus-provider.
|
||||
package debug
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/gorilla/mux"
|
||||
logging "github.com/ipfs/go-log/v2"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/filecoin-project/lotus/api/client"
|
||||
"github.com/filecoin-project/lotus/build"
|
||||
cliutil "github.com/filecoin-project/lotus/cli/util"
|
||||
"github.com/filecoin-project/lotus/cmd/lotus-provider/deps"
|
||||
)
|
||||
|
||||
var log = logging.Logger("lp/web/debug")
|
||||
|
||||
type debug struct {
|
||||
*deps.Deps
|
||||
}
|
||||
|
||||
func Routes(r *mux.Router, deps *deps.Deps) {
|
||||
d := debug{deps}
|
||||
r.HandleFunc("/chain-state-sse", d.chainStateSSE)
|
||||
}
|
||||
|
||||
type rpcInfo struct {
|
||||
Address string
|
||||
CLayers []string
|
||||
Reachable bool
|
||||
SyncState string
|
||||
Version string
|
||||
}
|
||||
|
||||
func (d *debug) chainStateSSE(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
for {
|
||||
|
||||
type minimalApiInfo struct {
|
||||
Apis struct {
|
||||
ChainApiInfo []string
|
||||
}
|
||||
}
|
||||
|
||||
rpcInfos := map[string]minimalApiInfo{} // config name -> api info
|
||||
confNameToAddr := map[string]string{} // config name -> api address
|
||||
|
||||
err := forEachConfig[minimalApiInfo](d, func(name string, info minimalApiInfo) error {
|
||||
if len(info.Apis.ChainApiInfo) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rpcInfos[name] = info
|
||||
|
||||
for _, addr := range info.Apis.ChainApiInfo {
|
||||
ai := cliutil.ParseApiInfo(addr)
|
||||
confNameToAddr[name] = ai.Addr
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorw("getting api info", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
dedup := map[string]bool{} // for dedup by address
|
||||
|
||||
infos := map[string]rpcInfo{} // api address -> rpc info
|
||||
var infosLk sync.Mutex
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, info := range rpcInfos {
|
||||
ai := cliutil.ParseApiInfo(info.Apis.ChainApiInfo[0])
|
||||
if dedup[ai.Addr] {
|
||||
continue
|
||||
}
|
||||
dedup[ai.Addr] = true
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var clayers []string
|
||||
for layer, a := range confNameToAddr {
|
||||
if a == ai.Addr {
|
||||
clayers = append(clayers, layer)
|
||||
}
|
||||
}
|
||||
|
||||
myinfo := rpcInfo{
|
||||
Address: ai.Addr,
|
||||
Reachable: false,
|
||||
CLayers: clayers,
|
||||
}
|
||||
defer func() {
|
||||
infosLk.Lock()
|
||||
defer infosLk.Unlock()
|
||||
infos[ai.Addr] = myinfo
|
||||
}()
|
||||
da, err := ai.DialArgs("v1")
|
||||
if err != nil {
|
||||
log.Warnw("DialArgs", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
ah := ai.AuthHeader()
|
||||
|
||||
v1api, closer, err := client.NewFullNodeRPCV1(ctx, da, ah)
|
||||
if err != nil {
|
||||
log.Warnf("Not able to establish connection to node with addr: %s", ai.Addr)
|
||||
return
|
||||
}
|
||||
defer closer()
|
||||
|
||||
ver, err := v1api.Version(ctx)
|
||||
if err != nil {
|
||||
log.Warnw("Version", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
head, err := v1api.ChainHead(ctx)
|
||||
if err != nil {
|
||||
log.Warnw("ChainHead", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
var syncState string
|
||||
switch {
|
||||
case time.Now().Unix()-int64(head.MinTimestamp()) < int64(build.BlockDelaySecs*3/2): // within 1.5 epochs
|
||||
syncState = "ok"
|
||||
case time.Now().Unix()-int64(head.MinTimestamp()) < int64(build.BlockDelaySecs*5): // within 5 epochs
|
||||
syncState = fmt.Sprintf("slow (%s behind)", time.Since(time.Unix(int64(head.MinTimestamp()), 0)).Truncate(time.Second))
|
||||
default:
|
||||
syncState = fmt.Sprintf("behind (%s behind)", time.Since(time.Unix(int64(head.MinTimestamp()), 0)).Truncate(time.Second))
|
||||
}
|
||||
|
||||
myinfo = rpcInfo{
|
||||
Address: ai.Addr,
|
||||
CLayers: clayers,
|
||||
Reachable: true,
|
||||
Version: ver.Version,
|
||||
SyncState: syncState,
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
var infoList []rpcInfo
|
||||
for _, i := range infos {
|
||||
infoList = append(infoList, i)
|
||||
}
|
||||
sort.Slice(infoList, func(i, j int) bool {
|
||||
return infoList[i].Address < infoList[j].Address
|
||||
})
|
||||
|
||||
fmt.Fprintf(w, "data: ")
|
||||
err = json.NewEncoder(w).Encode(&infoList)
|
||||
if err != nil {
|
||||
log.Warnw("json encode", "error", err)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "\n\n")
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
time.Sleep(time.Duration(build.BlockDelaySecs) * time.Second)
|
||||
|
||||
select { // stop running if there is reader.
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func forEachConfig[T any](a *debug, cb func(name string, v T) error) error {
|
||||
confs, err := a.loadConfigs(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, tomlStr := range confs { // todo for-each-config
|
||||
var info T
|
||||
if err := toml.Unmarshal([]byte(tomlStr), &info); err != nil {
|
||||
return xerrors.Errorf("unmarshaling %s config: %w", name, err)
|
||||
}
|
||||
|
||||
if err := cb(name, info); err != nil {
|
||||
return xerrors.Errorf("cb: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *debug) loadConfigs(ctx context.Context) (map[string]string, error) {
|
||||
//err := db.QueryRow(cctx.Context, `SELECT config FROM harmony_config WHERE title=$1`, layer).Scan(&text)
|
||||
|
||||
rows, err := d.DB.Query(ctx, `SELECT title, config FROM harmony_config`)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("getting db configs: %w", err)
|
||||
}
|
||||
|
||||
configs := make(map[string]string)
|
||||
for rows.Next() {
|
||||
var title, config string
|
||||
if err := rows.Scan(&title, &config); err != nil {
|
||||
return nil, xerrors.Errorf("scanning db configs: %w", err)
|
||||
}
|
||||
configs[title] = config
|
||||
}
|
||||
|
||||
return configs, nil
|
||||
}
|
13
cmd/lotus-provider/web/api/routes.go
Normal file
13
cmd/lotus-provider/web/api/routes.go
Normal file
@ -0,0 +1,13 @@
|
||||
// Package api provides the HTTP API for the lotus provider web gui.
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/filecoin-project/lotus/cmd/lotus-provider/deps"
|
||||
"github.com/filecoin-project/lotus/cmd/lotus-provider/web/api/debug"
|
||||
)
|
||||
|
||||
func Routes(r *mux.Router, deps *deps.Deps) {
|
||||
debug.Routes(r.PathPrefix("/debug").Subrouter(), deps)
|
||||
}
|
35
cmd/lotus-provider/web/hapi/routes.go
Normal file
35
cmd/lotus-provider/web/hapi/routes.go
Normal file
@ -0,0 +1,35 @@
|
||||
package hapi
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
logging "github.com/ipfs/go-log/v2"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/filecoin-project/lotus/cmd/lotus-provider/deps"
|
||||
)
|
||||
|
||||
//go:embed web/*
|
||||
var templateFS embed.FS
|
||||
|
||||
func Routes(r *mux.Router, deps *deps.Deps) error {
|
||||
t, err := template.ParseFS(templateFS, "web/*")
|
||||
if err != nil {
|
||||
return xerrors.Errorf("parse templates: %w", err)
|
||||
}
|
||||
|
||||
a := &app{
|
||||
db: deps.DB,
|
||||
t: t,
|
||||
}
|
||||
|
||||
r.HandleFunc("/simpleinfo/actorsummary", a.actorSummary)
|
||||
r.HandleFunc("/simpleinfo/machines", a.indexMachines)
|
||||
r.HandleFunc("/simpleinfo/tasks", a.indexTasks)
|
||||
r.HandleFunc("/simpleinfo/taskhistory", a.indexTasksHistory)
|
||||
return nil
|
||||
}
|
||||
|
||||
var log = logging.Logger("lpweb")
|
187
cmd/lotus-provider/web/hapi/simpleinfo.go
Normal file
187
cmd/lotus-provider/web/hapi/simpleinfo.go
Normal file
@ -0,0 +1,187 @@
|
||||
package hapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/filecoin-project/lotus/lib/harmony/harmonydb"
|
||||
)
|
||||
|
||||
type app struct {
|
||||
db *harmonydb.DB
|
||||
t *template.Template
|
||||
|
||||
actorInfoLk sync.Mutex
|
||||
actorInfos []actorInfo
|
||||
}
|
||||
|
||||
type actorInfo struct {
|
||||
Address string
|
||||
CLayers []string
|
||||
|
||||
QualityAdjustedPower string
|
||||
RawBytePower string
|
||||
|
||||
Deadlines []actorDeadline
|
||||
}
|
||||
|
||||
type actorDeadline struct {
|
||||
Empty bool
|
||||
Current bool
|
||||
Proven bool
|
||||
PartFaulty bool
|
||||
Faulty bool
|
||||
}
|
||||
|
||||
func (a *app) actorSummary(w http.ResponseWriter, r *http.Request) {
|
||||
a.actorInfoLk.Lock()
|
||||
defer a.actorInfoLk.Unlock()
|
||||
|
||||
a.executeTemplate(w, "actor_summary", a.actorInfos)
|
||||
}
|
||||
|
||||
func (a *app) indexMachines(w http.ResponseWriter, r *http.Request) {
|
||||
s, err := a.clusterMachineSummary(r.Context())
|
||||
if err != nil {
|
||||
log.Errorf("cluster machine summary: %v", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
a.executeTemplate(w, "cluster_machines", s)
|
||||
}
|
||||
|
||||
func (a *app) indexTasks(w http.ResponseWriter, r *http.Request) {
|
||||
s, err := a.clusterTaskSummary(r.Context())
|
||||
if err != nil {
|
||||
log.Errorf("cluster task summary: %v", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
a.executeTemplate(w, "cluster_tasks", s)
|
||||
}
|
||||
|
||||
func (a *app) indexTasksHistory(w http.ResponseWriter, r *http.Request) {
|
||||
s, err := a.clusterTaskHistorySummary(r.Context())
|
||||
if err != nil {
|
||||
log.Errorf("cluster task history summary: %v", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
a.executeTemplate(w, "cluster_task_history", s)
|
||||
}
|
||||
|
||||
var templateDev = os.Getenv("LOTUS_WEB_DEV") == "1"
|
||||
|
||||
func (a *app) executeTemplate(w http.ResponseWriter, name string, data interface{}) {
|
||||
if templateDev {
|
||||
fs := os.DirFS("./cmd/lotus-provider/web/hapi/web")
|
||||
a.t = template.Must(template.ParseFS(fs, "web/*"))
|
||||
}
|
||||
if err := a.t.ExecuteTemplate(w, name, data); err != nil {
|
||||
log.Errorf("execute template %s: %v", name, err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
type machineSummary struct {
|
||||
Address string
|
||||
ID int64
|
||||
SinceContact string
|
||||
}
|
||||
|
||||
type taskSummary struct {
|
||||
Name string
|
||||
SincePosted string
|
||||
Owner *string
|
||||
ID int64
|
||||
}
|
||||
|
||||
type taskHistorySummary struct {
|
||||
Name string
|
||||
TaskID int64
|
||||
|
||||
Posted, Start, End string
|
||||
|
||||
Result bool
|
||||
Err string
|
||||
|
||||
CompletedBy string
|
||||
}
|
||||
|
||||
func (a *app) clusterMachineSummary(ctx context.Context) ([]machineSummary, error) {
|
||||
rows, err := a.db.Query(ctx, "SELECT id, host_and_port, last_contact FROM harmony_machines")
|
||||
if err != nil {
|
||||
return nil, err // Handle error
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var summaries []machineSummary
|
||||
for rows.Next() {
|
||||
var m machineSummary
|
||||
var lastContact time.Time
|
||||
|
||||
if err := rows.Scan(&m.ID, &m.Address, &lastContact); err != nil {
|
||||
return nil, err // Handle error
|
||||
}
|
||||
|
||||
m.SinceContact = time.Since(lastContact).Round(time.Second).String()
|
||||
|
||||
summaries = append(summaries, m)
|
||||
}
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
func (a *app) clusterTaskSummary(ctx context.Context) ([]taskSummary, error) {
|
||||
rows, err := a.db.Query(ctx, "SELECT id, name, update_time, owner_id FROM harmony_task")
|
||||
if err != nil {
|
||||
return nil, err // Handle error
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var summaries []taskSummary
|
||||
for rows.Next() {
|
||||
var t taskSummary
|
||||
var posted time.Time
|
||||
|
||||
if err := rows.Scan(&t.ID, &t.Name, &posted, &t.Owner); err != nil {
|
||||
return nil, err // Handle error
|
||||
}
|
||||
|
||||
t.SincePosted = time.Since(posted).Round(time.Second).String()
|
||||
|
||||
summaries = append(summaries, t)
|
||||
}
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
func (a *app) clusterTaskHistorySummary(ctx context.Context) ([]taskHistorySummary, error) {
|
||||
rows, err := a.db.Query(ctx, "SELECT id, name, task_id, posted, work_start, work_end, result, err, completed_by_host_and_port FROM harmony_task_history ORDER BY work_end DESC LIMIT 15")
|
||||
if err != nil {
|
||||
return nil, err // Handle error
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var summaries []taskHistorySummary
|
||||
for rows.Next() {
|
||||
var t taskHistorySummary
|
||||
var posted, start, end time.Time
|
||||
|
||||
if err := rows.Scan(&t.TaskID, &t.Name, &t.TaskID, &posted, &start, &end, &t.Result, &t.Err, &t.CompletedBy); err != nil {
|
||||
return nil, err // Handle error
|
||||
}
|
||||
|
||||
t.Posted = posted.Round(time.Second).Format("02 Jan 06 15:04")
|
||||
t.Start = start.Round(time.Second).Format("02 Jan 06 15:04")
|
||||
t.End = end.Round(time.Second).Format("02 Jan 06 15:04")
|
||||
|
||||
summaries = append(summaries, t)
|
||||
}
|
||||
return summaries, nil
|
||||
}
|
20
cmd/lotus-provider/web/hapi/web/actor_summary.gohtml
Normal file
20
cmd/lotus-provider/web/hapi/web/actor_summary.gohtml
Normal file
@ -0,0 +1,20 @@
|
||||
{{define "actor_summary"}}
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td>{{.Address}}</td>
|
||||
<td>
|
||||
{{range .CLayers}}
|
||||
<span>{{.}} </span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>{{.QualityAdjustedPower}}</td>
|
||||
<td>
|
||||
<div class="deadline-box">
|
||||
{{range .Deadlines}}
|
||||
<div class="deadline-entry{{if .Current}} deadline-entry-cur{{end}}{{if .Proven}} deadline-proven{{end}}{{if .PartFaulty}} deadline-partially-faulty{{end}}{{if .Faulty}} deadline-faulty{{end}}"></div>
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
15
cmd/lotus-provider/web/hapi/web/chain_rpcs.gohtml
Normal file
15
cmd/lotus-provider/web/hapi/web/chain_rpcs.gohtml
Normal file
@ -0,0 +1,15 @@
|
||||
{{define "chain_rpcs"}}
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td>{{.Address}}</td>
|
||||
<td>
|
||||
{{range .CLayers}}
|
||||
<span>{{.}} </span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>{{if .Reachable}}<span class="success">ok</span>{{else}}<span class="error">FAIL</span>{{end}}</td>
|
||||
<td>{{if eq "ok" .SyncState}}<span class="success">ok</span>{{else}}<span class="warning">{{.SyncState}}</span>{{end}}</td>
|
||||
<td>{{.Version}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
10
cmd/lotus-provider/web/hapi/web/cluster_machines.gohtml
Normal file
10
cmd/lotus-provider/web/hapi/web/cluster_machines.gohtml
Normal file
@ -0,0 +1,10 @@
|
||||
{{define "cluster_machines"}}
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td>{{.Address}}</td>
|
||||
<td>{{.ID}}</td>
|
||||
<td>todo</td>
|
||||
<td>{{.SinceContact}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
14
cmd/lotus-provider/web/hapi/web/cluster_task_history.gohtml
Normal file
14
cmd/lotus-provider/web/hapi/web/cluster_task_history.gohtml
Normal file
@ -0,0 +1,14 @@
|
||||
{{define "cluster_task_history"}}
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{.TaskID}}</td>
|
||||
<td>{{.CompletedBy}}</td>
|
||||
<td>{{.Posted}}</td>
|
||||
<td>{{.Start}}</td>
|
||||
<td>{{.End}}</td>
|
||||
<td>{{if .Result}}<span class="success">success</span>{{else}}<span class="error">error</span>{{end}}</td>
|
||||
<td>{{.Err}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
10
cmd/lotus-provider/web/hapi/web/cluster_tasks.gohtml
Normal file
10
cmd/lotus-provider/web/hapi/web/cluster_tasks.gohtml
Normal file
@ -0,0 +1,10 @@
|
||||
{{define "cluster_tasks"}}
|
||||
{{range .}}
|
||||
<tr>
|
||||
<td>{{.Name}}</td>
|
||||
<td>{{.ID}}</td>
|
||||
<td>{{.SincePosted}}</td>
|
||||
<td>{{.Owner}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{end}}
|
84
cmd/lotus-provider/web/srv.go
Normal file
84
cmd/lotus-provider/web/srv.go
Normal file
@ -0,0 +1,84 @@
|
||||
// Package web defines the HTTP web server for static files and endpoints.
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"go.opencensus.io/tag"
|
||||
|
||||
"github.com/filecoin-project/lotus/cmd/lotus-provider/deps"
|
||||
"github.com/filecoin-project/lotus/cmd/lotus-provider/web/api"
|
||||
"github.com/filecoin-project/lotus/cmd/lotus-provider/web/hapi"
|
||||
"github.com/filecoin-project/lotus/metrics"
|
||||
)
|
||||
|
||||
//go:embed static
|
||||
var static embed.FS
|
||||
|
||||
var basePath = "/static/"
|
||||
|
||||
// An dev mode hack for no-restart changes to static and templates.
|
||||
// You still need to recomplie the binary for changes to go code.
|
||||
var webDev = os.Getenv("LOTUS_WEB_DEV") == "1"
|
||||
|
||||
func GetSrv(ctx context.Context, deps *deps.Deps) (*http.Server, error) {
|
||||
mx := mux.NewRouter()
|
||||
err := hapi.Routes(mx.PathPrefix("/hapi").Subrouter(), deps)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api.Routes(mx.PathPrefix("/api").Subrouter(), deps)
|
||||
|
||||
basePath := basePath
|
||||
|
||||
var static fs.FS = static
|
||||
if webDev {
|
||||
basePath = "cmd/lotus-provider/web/static"
|
||||
static = os.DirFS(basePath)
|
||||
}
|
||||
|
||||
mx.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// If the request is for a directory, redirect to the index file.
|
||||
if strings.HasSuffix(r.URL.Path, "/") {
|
||||
r.URL.Path += "index.html"
|
||||
}
|
||||
|
||||
file, err := static.Open(path.Join(basePath, r.URL.Path)[1:])
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte("404 Not Found"))
|
||||
return
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("500 Internal Server Error"))
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, fileInfo.Name(), fileInfo.ModTime(), file.(io.ReadSeeker))
|
||||
})
|
||||
|
||||
return &http.Server{
|
||||
Handler: http.HandlerFunc(mx.ServeHTTP),
|
||||
BaseContext: func(listener net.Listener) context.Context {
|
||||
ctx, _ := tag.New(context.Background(), tag.Upsert(metrics.APIInterface, "lotus-provider"))
|
||||
return ctx
|
||||
},
|
||||
Addr: deps.Cfg.Subsystems.GuiAddress,
|
||||
ReadTimeout: time.Minute * 3,
|
||||
ReadHeaderTimeout: time.Minute * 3, // lint
|
||||
}, nil
|
||||
}
|
73
cmd/lotus-provider/web/static/chain-connectivity.js
Normal file
73
cmd/lotus-provider/web/static/chain-connectivity.js
Normal file
@ -0,0 +1,73 @@
|
||||
import { LitElement, html, css } from 'https://cdn.jsdelivr.net/gh/lit/dist@3/all/lit-all.min.js';
|
||||
window.customElements.define('chain-connectivity', class MyElement extends LitElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.data = [];
|
||||
this.loadData();
|
||||
}
|
||||
loadData() {
|
||||
const eventSource = new EventSource('/api/debug/chain-state-sse');
|
||||
eventSource.onmessage = (event) => {
|
||||
this.data = JSON.parse(event.data);
|
||||
super.requestUpdate();
|
||||
};
|
||||
eventSource.onerror = (error) => {
|
||||
console.error('Error:', error);
|
||||
loadData();
|
||||
};
|
||||
};
|
||||
|
||||
static get styles() {
|
||||
return [css`
|
||||
:host {
|
||||
box-sizing: border-box; /* Don't forgert this to include padding/border inside width calculation */
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
border-left: 1px solid #f0f0f0;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
table tr td:first-child, table tr th:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: green;
|
||||
}
|
||||
.warning {
|
||||
color: yellow;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
`];
|
||||
}
|
||||
render = () => html`
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>RPC Address</th>
|
||||
<th>Reachability</th>
|
||||
<th>Sync Status</th>
|
||||
<th>Version</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${this.data.map(item => html`
|
||||
<tr>
|
||||
<td>${item.Address}</td>
|
||||
<td>${item.Reachable ? html`<span class="success">ok</span>` : html`<span class="error">FAIL</span>`}</td>
|
||||
<td>${item.SyncState === "ok" ? html`<span class="success">ok</span>` : html`<span class="warning">${item.SyncState}</span>`}</td>
|
||||
<td>${item.Version}</td>
|
||||
</tr>
|
||||
`)}
|
||||
<tr>
|
||||
<td colspan="4">Data incoming...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>`
|
||||
});
|
193
cmd/lotus-provider/web/static/index.html
Normal file
193
cmd/lotus-provider/web/static/index.html
Normal file
@ -0,0 +1,193 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Lotus Provider Cluster Overview</title>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.5" integrity="sha384-xcuj3WpfgjlKF+FXhSQFQ0ZNr39ln+hwjN3npfM9VBnUskLolQAcN80McRIVOPuO" crossorigin="anonymous"></script>
|
||||
<script type="module" src="chain-connectivity.js"></script>
|
||||
<style>
|
||||
html, body {
|
||||
background: #0f0f0f;
|
||||
color: #ffffff;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.app-head {
|
||||
width: 100%;
|
||||
}
|
||||
.head-left {
|
||||
display: inline-block;
|
||||
}
|
||||
.head-right {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
border-left: 1px solid #f0f0f0;
|
||||
padding: 1px 5px;
|
||||
}
|
||||
|
||||
table tr td:first-child, table tr th:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
a:link {
|
||||
color: #cfc;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #dfa;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #af7;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: green;
|
||||
}
|
||||
.warning {
|
||||
color: yellow;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.dash-tile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.75rem;
|
||||
background: #3f3f3f;
|
||||
|
||||
& b {
|
||||
padding-bottom: 0.5rem;
|
||||
color: deeppink;
|
||||
}
|
||||
}
|
||||
|
||||
.deadline-box {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(16, auto);
|
||||
grid-template-rows: repeat(3, auto);
|
||||
grid-gap: 1px;
|
||||
}
|
||||
|
||||
.deadline-entry {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: grey;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
.deadline-entry-cur {
|
||||
border-bottom: 3px solid deepskyblue;
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.deadline-proven {
|
||||
background-color: green;
|
||||
}
|
||||
.deadline-partially-faulty {
|
||||
background-color: yellow;
|
||||
}
|
||||
.deadline-faulty {
|
||||
background-color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-head">
|
||||
<div class="head-left">
|
||||
<h1>Lotus Provider Cluster</h1>
|
||||
</div>
|
||||
<div class="head-right">
|
||||
version [todo]
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="page">
|
||||
<div class="info-block">
|
||||
<h2>Chain Connectivity</h2>
|
||||
<chain-connectivity></chain-connectivity>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="info-block">
|
||||
<h2>Actor Summary</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Address</th>
|
||||
<th>Config Layers</th>
|
||||
<th>QaP</th>
|
||||
<th>Deadlines</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody hx-get="/hapi/simpleinfo/actorsummary" hx-trigger="load,every 5s">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="info-block">
|
||||
<h2>Cluster Machines</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Host</th>
|
||||
<th>ID</th>
|
||||
<th>Config Layers</th>
|
||||
<th>Last Contact</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody hx-get="/hapi/simpleinfo/machines" hx-trigger="load,every 5s">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="info-block">
|
||||
<h2>Recently Finished Tasks</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>ID</th>
|
||||
<th>Executor</th>
|
||||
<th>Posted</th>
|
||||
<th>Start</th>
|
||||
<th>End</th>
|
||||
<th>Outcome</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody hx-get="/hapi/simpleinfo/taskhistory" hx-trigger="load, every 5s">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="info-block">
|
||||
<h2>Cluster Tasks</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Task</th>
|
||||
<th>ID</th>
|
||||
<th>Posted</th>
|
||||
<th>Owner</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody hx-get="/hapi/simpleinfo/tasks" hx-trigger="load,every 5s">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -14,6 +14,7 @@ COMMANDS:
|
||||
stop Stop a running lotus provider
|
||||
config Manage node config by layers. The layer 'base' will always be applied.
|
||||
test Utility functions for testing
|
||||
web Start lotus provider web interface
|
||||
version Print version
|
||||
help, h Shows a list of commands or help for one command
|
||||
DEVELOPER:
|
||||
@ -247,6 +248,25 @@ OPTIONS:
|
||||
--help, -h show help
|
||||
```
|
||||
|
||||
## lotus-provider web
|
||||
```
|
||||
NAME:
|
||||
lotus-provider web - Start lotus provider web interface
|
||||
|
||||
USAGE:
|
||||
lotus-provider web [command options] [arguments...]
|
||||
|
||||
DESCRIPTION:
|
||||
Start an instance of lotus provider web interface.
|
||||
This creates the 'web' layer if it does not exist, then calls run with that layer.
|
||||
|
||||
OPTIONS:
|
||||
--listen value Address to listen on (default: "127.0.0.1:4701")
|
||||
--layers value [ --layers value ] list of layers to be interpreted (atop defaults). Default: base. Web will be added (default: "base")
|
||||
--nosync don't check full-node sync status (default: false)
|
||||
--help, -h show help
|
||||
```
|
||||
|
||||
## lotus-provider version
|
||||
```
|
||||
NAME:
|
||||
|
@ -11,6 +11,14 @@
|
||||
# type: int
|
||||
#WinningPostMaxTasks = 0
|
||||
|
||||
# type: bool
|
||||
#EnableWebGui = false
|
||||
|
||||
# The address that should listen for Web GUI requests.
|
||||
#
|
||||
# type: string
|
||||
#GuiAddress = ":4701"
|
||||
|
||||
|
||||
[Fees]
|
||||
# type: types.FIL
|
||||
|
@ -351,7 +351,9 @@ func DefaultUserRaftConfig() *UserRaftConfig {
|
||||
|
||||
func DefaultLotusProvider() *LotusProviderConfig {
|
||||
return &LotusProviderConfig{
|
||||
Subsystems: ProviderSubsystemsConfig{},
|
||||
Subsystems: ProviderSubsystemsConfig{
|
||||
GuiAddress: ":4701",
|
||||
},
|
||||
Fees: LotusProviderFees{
|
||||
DefaultMaxFee: DefaultDefaultMaxFee,
|
||||
MaxPreCommitGasFee: types.MustParseFIL("0.025"),
|
||||
|
@ -1013,6 +1013,18 @@ block rewards will be missed!`,
|
||||
|
||||
Comment: ``,
|
||||
},
|
||||
{
|
||||
Name: "EnableWebGui",
|
||||
Type: "bool",
|
||||
|
||||
Comment: ``,
|
||||
},
|
||||
{
|
||||
Name: "GuiAddress",
|
||||
Type: "string",
|
||||
|
||||
Comment: `The address that should listen for Web GUI requests.`,
|
||||
},
|
||||
},
|
||||
"ProvingConfig": {
|
||||
{
|
||||
|
@ -96,6 +96,10 @@ type ProviderSubsystemsConfig struct {
|
||||
WindowPostMaxTasks int
|
||||
EnableWinningPost bool
|
||||
WinningPostMaxTasks int
|
||||
|
||||
EnableWebGui bool
|
||||
// The address that should listen for Web GUI requests.
|
||||
GuiAddress string
|
||||
}
|
||||
|
||||
type DAGStoreConfig struct {
|
||||
|
Loading…
Reference in New Issue
Block a user