github.com/MetalBlockchain/metalgo@v1.11.9/api/admin/service.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package admin
     5  
     6  import (
     7  	"errors"
     8  	"net/http"
     9  	"path"
    10  	"sync"
    11  
    12  	"github.com/gorilla/rpc/v2"
    13  	"go.uber.org/zap"
    14  
    15  	"github.com/MetalBlockchain/metalgo/api"
    16  	"github.com/MetalBlockchain/metalgo/api/server"
    17  	"github.com/MetalBlockchain/metalgo/chains"
    18  	"github.com/MetalBlockchain/metalgo/database"
    19  	"github.com/MetalBlockchain/metalgo/database/rpcdb"
    20  	"github.com/MetalBlockchain/metalgo/ids"
    21  	"github.com/MetalBlockchain/metalgo/utils"
    22  	"github.com/MetalBlockchain/metalgo/utils/constants"
    23  	"github.com/MetalBlockchain/metalgo/utils/formatting"
    24  	"github.com/MetalBlockchain/metalgo/utils/json"
    25  	"github.com/MetalBlockchain/metalgo/utils/logging"
    26  	"github.com/MetalBlockchain/metalgo/utils/perms"
    27  	"github.com/MetalBlockchain/metalgo/utils/profiler"
    28  	"github.com/MetalBlockchain/metalgo/vms"
    29  	"github.com/MetalBlockchain/metalgo/vms/registry"
    30  
    31  	rpcdbpb "github.com/MetalBlockchain/metalgo/proto/pb/rpcdb"
    32  )
    33  
    34  const (
    35  	maxAliasLength = 512
    36  
    37  	// Name of file that stacktraces are written to
    38  	stacktraceFile = "stacktrace.txt"
    39  )
    40  
    41  var (
    42  	errAliasTooLong = errors.New("alias length is too long")
    43  	errNoLogLevel   = errors.New("need to specify either displayLevel or logLevel")
    44  )
    45  
    46  type Config struct {
    47  	Log          logging.Logger
    48  	ProfileDir   string
    49  	LogFactory   logging.Factory
    50  	NodeConfig   interface{}
    51  	DB           database.Database
    52  	ChainManager chains.Manager
    53  	HTTPServer   server.PathAdderWithReadLock
    54  	VMRegistry   registry.VMRegistry
    55  	VMManager    vms.Manager
    56  }
    57  
    58  // Admin is the API service for node admin management
    59  type Admin struct {
    60  	Config
    61  	lock     sync.RWMutex
    62  	profiler profiler.Profiler
    63  }
    64  
    65  // NewService returns a new admin API service.
    66  // All of the fields in [config] must be set.
    67  func NewService(config Config) (http.Handler, error) {
    68  	server := rpc.NewServer()
    69  	codec := json.NewCodec()
    70  	server.RegisterCodec(codec, "application/json")
    71  	server.RegisterCodec(codec, "application/json;charset=UTF-8")
    72  	return server, server.RegisterService(
    73  		&Admin{
    74  			Config:   config,
    75  			profiler: profiler.New(config.ProfileDir),
    76  		},
    77  		"admin",
    78  	)
    79  }
    80  
    81  // StartCPUProfiler starts a cpu profile writing to the specified file
    82  func (a *Admin) StartCPUProfiler(_ *http.Request, _ *struct{}, _ *api.EmptyReply) error {
    83  	a.Log.Debug("API called",
    84  		zap.String("service", "admin"),
    85  		zap.String("method", "startCPUProfiler"),
    86  	)
    87  
    88  	a.lock.Lock()
    89  	defer a.lock.Unlock()
    90  
    91  	return a.profiler.StartCPUProfiler()
    92  }
    93  
    94  // StopCPUProfiler stops the cpu profile
    95  func (a *Admin) StopCPUProfiler(_ *http.Request, _ *struct{}, _ *api.EmptyReply) error {
    96  	a.Log.Debug("API called",
    97  		zap.String("service", "admin"),
    98  		zap.String("method", "stopCPUProfiler"),
    99  	)
   100  
   101  	a.lock.Lock()
   102  	defer a.lock.Unlock()
   103  
   104  	return a.profiler.StopCPUProfiler()
   105  }
   106  
   107  // MemoryProfile runs a memory profile writing to the specified file
   108  func (a *Admin) MemoryProfile(_ *http.Request, _ *struct{}, _ *api.EmptyReply) error {
   109  	a.Log.Debug("API called",
   110  		zap.String("service", "admin"),
   111  		zap.String("method", "memoryProfile"),
   112  	)
   113  
   114  	a.lock.Lock()
   115  	defer a.lock.Unlock()
   116  
   117  	return a.profiler.MemoryProfile()
   118  }
   119  
   120  // LockProfile runs a mutex profile writing to the specified file
   121  func (a *Admin) LockProfile(_ *http.Request, _ *struct{}, _ *api.EmptyReply) error {
   122  	a.Log.Debug("API called",
   123  		zap.String("service", "admin"),
   124  		zap.String("method", "lockProfile"),
   125  	)
   126  
   127  	a.lock.Lock()
   128  	defer a.lock.Unlock()
   129  
   130  	return a.profiler.LockProfile()
   131  }
   132  
   133  // AliasArgs are the arguments for calling Alias
   134  type AliasArgs struct {
   135  	Endpoint string `json:"endpoint"`
   136  	Alias    string `json:"alias"`
   137  }
   138  
   139  // Alias attempts to alias an HTTP endpoint to a new name
   140  func (a *Admin) Alias(_ *http.Request, args *AliasArgs, _ *api.EmptyReply) error {
   141  	a.Log.Debug("API called",
   142  		zap.String("service", "admin"),
   143  		zap.String("method", "alias"),
   144  		logging.UserString("endpoint", args.Endpoint),
   145  		logging.UserString("alias", args.Alias),
   146  	)
   147  
   148  	if len(args.Alias) > maxAliasLength {
   149  		return errAliasTooLong
   150  	}
   151  
   152  	return a.HTTPServer.AddAliasesWithReadLock(args.Endpoint, args.Alias)
   153  }
   154  
   155  // AliasChainArgs are the arguments for calling AliasChain
   156  type AliasChainArgs struct {
   157  	Chain string `json:"chain"`
   158  	Alias string `json:"alias"`
   159  }
   160  
   161  // AliasChain attempts to alias a chain to a new name
   162  func (a *Admin) AliasChain(_ *http.Request, args *AliasChainArgs, _ *api.EmptyReply) error {
   163  	a.Log.Debug("API called",
   164  		zap.String("service", "admin"),
   165  		zap.String("method", "aliasChain"),
   166  		logging.UserString("chain", args.Chain),
   167  		logging.UserString("alias", args.Alias),
   168  	)
   169  
   170  	if len(args.Alias) > maxAliasLength {
   171  		return errAliasTooLong
   172  	}
   173  	chainID, err := a.ChainManager.Lookup(args.Chain)
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	a.lock.Lock()
   179  	defer a.lock.Unlock()
   180  
   181  	if err := a.ChainManager.Alias(chainID, args.Alias); err != nil {
   182  		return err
   183  	}
   184  
   185  	endpoint := path.Join(constants.ChainAliasPrefix, chainID.String())
   186  	alias := path.Join(constants.ChainAliasPrefix, args.Alias)
   187  	return a.HTTPServer.AddAliasesWithReadLock(endpoint, alias)
   188  }
   189  
   190  // GetChainAliasesArgs are the arguments for calling GetChainAliases
   191  type GetChainAliasesArgs struct {
   192  	Chain string `json:"chain"`
   193  }
   194  
   195  // GetChainAliasesReply are the aliases of the given chain
   196  type GetChainAliasesReply struct {
   197  	Aliases []string `json:"aliases"`
   198  }
   199  
   200  // GetChainAliases returns the aliases of the chain
   201  func (a *Admin) GetChainAliases(_ *http.Request, args *GetChainAliasesArgs, reply *GetChainAliasesReply) error {
   202  	a.Log.Debug("API called",
   203  		zap.String("service", "admin"),
   204  		zap.String("method", "getChainAliases"),
   205  		logging.UserString("chain", args.Chain),
   206  	)
   207  
   208  	id, err := ids.FromString(args.Chain)
   209  	if err != nil {
   210  		return err
   211  	}
   212  
   213  	reply.Aliases, err = a.ChainManager.Aliases(id)
   214  	return err
   215  }
   216  
   217  // Stacktrace returns the current global stacktrace
   218  func (a *Admin) Stacktrace(_ *http.Request, _ *struct{}, _ *api.EmptyReply) error {
   219  	a.Log.Debug("API called",
   220  		zap.String("service", "admin"),
   221  		zap.String("method", "stacktrace"),
   222  	)
   223  
   224  	stacktrace := []byte(utils.GetStacktrace(true))
   225  
   226  	a.lock.Lock()
   227  	defer a.lock.Unlock()
   228  
   229  	return perms.WriteFile(stacktraceFile, stacktrace, perms.ReadWrite)
   230  }
   231  
   232  type SetLoggerLevelArgs struct {
   233  	LoggerName   string         `json:"loggerName"`
   234  	LogLevel     *logging.Level `json:"logLevel"`
   235  	DisplayLevel *logging.Level `json:"displayLevel"`
   236  }
   237  
   238  type LogAndDisplayLevels struct {
   239  	LogLevel     logging.Level `json:"logLevel"`
   240  	DisplayLevel logging.Level `json:"displayLevel"`
   241  }
   242  
   243  type LoggerLevelReply struct {
   244  	LoggerLevels map[string]LogAndDisplayLevels `json:"loggerLevels"`
   245  }
   246  
   247  // SetLoggerLevel sets the log level and/or display level for loggers.
   248  // If len([args.LoggerName]) == 0, sets the log/display level of all loggers.
   249  // Otherwise, sets the log/display level of the loggers named in that argument.
   250  // Sets the log level of these loggers to args.LogLevel.
   251  // If args.LogLevel == nil, doesn't set the log level of these loggers.
   252  // If args.LogLevel != nil, must be a valid string representation of a log level.
   253  // Sets the display level of these loggers to args.LogLevel.
   254  // If args.DisplayLevel == nil, doesn't set the display level of these loggers.
   255  // If args.DisplayLevel != nil, must be a valid string representation of a log level.
   256  func (a *Admin) SetLoggerLevel(_ *http.Request, args *SetLoggerLevelArgs, reply *LoggerLevelReply) error {
   257  	a.Log.Debug("API called",
   258  		zap.String("service", "admin"),
   259  		zap.String("method", "setLoggerLevel"),
   260  		logging.UserString("loggerName", args.LoggerName),
   261  		zap.Stringer("logLevel", args.LogLevel),
   262  		zap.Stringer("displayLevel", args.DisplayLevel),
   263  	)
   264  
   265  	if args.LogLevel == nil && args.DisplayLevel == nil {
   266  		return errNoLogLevel
   267  	}
   268  
   269  	a.lock.Lock()
   270  	defer a.lock.Unlock()
   271  
   272  	loggerNames := a.getLoggerNames(args.LoggerName)
   273  	for _, name := range loggerNames {
   274  		if args.LogLevel != nil {
   275  			if err := a.LogFactory.SetLogLevel(name, *args.LogLevel); err != nil {
   276  				return err
   277  			}
   278  		}
   279  		if args.DisplayLevel != nil {
   280  			if err := a.LogFactory.SetDisplayLevel(name, *args.DisplayLevel); err != nil {
   281  				return err
   282  			}
   283  		}
   284  	}
   285  
   286  	var err error
   287  	reply.LoggerLevels, err = a.getLogLevels(loggerNames)
   288  	return err
   289  }
   290  
   291  type GetLoggerLevelArgs struct {
   292  	LoggerName string `json:"loggerName"`
   293  }
   294  
   295  // GetLoggerLevel returns the log level and display level of all loggers.
   296  func (a *Admin) GetLoggerLevel(_ *http.Request, args *GetLoggerLevelArgs, reply *LoggerLevelReply) error {
   297  	a.Log.Debug("API called",
   298  		zap.String("service", "admin"),
   299  		zap.String("method", "getLoggerLevels"),
   300  		logging.UserString("loggerName", args.LoggerName),
   301  	)
   302  
   303  	a.lock.RLock()
   304  	defer a.lock.RUnlock()
   305  
   306  	loggerNames := a.getLoggerNames(args.LoggerName)
   307  
   308  	var err error
   309  	reply.LoggerLevels, err = a.getLogLevels(loggerNames)
   310  	return err
   311  }
   312  
   313  // GetConfig returns the config that the node was started with.
   314  func (a *Admin) GetConfig(_ *http.Request, _ *struct{}, reply *interface{}) error {
   315  	a.Log.Debug("API called",
   316  		zap.String("service", "admin"),
   317  		zap.String("method", "getConfig"),
   318  	)
   319  	*reply = a.NodeConfig
   320  	return nil
   321  }
   322  
   323  // LoadVMsReply contains the response metadata for LoadVMs
   324  type LoadVMsReply struct {
   325  	// VMs and their aliases which were successfully loaded
   326  	NewVMs map[ids.ID][]string `json:"newVMs"`
   327  	// VMs that failed to be loaded and the error message
   328  	FailedVMs map[ids.ID]string `json:"failedVMs,omitempty"`
   329  }
   330  
   331  // LoadVMs loads any new VMs available to the node and returns the added VMs.
   332  func (a *Admin) LoadVMs(r *http.Request, _ *struct{}, reply *LoadVMsReply) error {
   333  	a.Log.Debug("API called",
   334  		zap.String("service", "admin"),
   335  		zap.String("method", "loadVMs"),
   336  	)
   337  
   338  	a.lock.Lock()
   339  	defer a.lock.Unlock()
   340  
   341  	ctx := r.Context()
   342  	loadedVMs, failedVMs, err := a.VMRegistry.Reload(ctx)
   343  	if err != nil {
   344  		return err
   345  	}
   346  
   347  	// extract the inner error messages
   348  	failedVMsParsed := make(map[ids.ID]string)
   349  	for vmID, err := range failedVMs {
   350  		failedVMsParsed[vmID] = err.Error()
   351  	}
   352  
   353  	reply.FailedVMs = failedVMsParsed
   354  	reply.NewVMs, err = ids.GetRelevantAliases(a.VMManager, loadedVMs)
   355  	return err
   356  }
   357  
   358  func (a *Admin) getLoggerNames(loggerName string) []string {
   359  	if len(loggerName) == 0 {
   360  		// Empty name means all loggers
   361  		return a.LogFactory.GetLoggerNames()
   362  	}
   363  	return []string{loggerName}
   364  }
   365  
   366  func (a *Admin) getLogLevels(loggerNames []string) (map[string]LogAndDisplayLevels, error) {
   367  	loggerLevels := make(map[string]LogAndDisplayLevels)
   368  	for _, name := range loggerNames {
   369  		logLevel, err := a.LogFactory.GetLogLevel(name)
   370  		if err != nil {
   371  			return nil, err
   372  		}
   373  		displayLevel, err := a.LogFactory.GetDisplayLevel(name)
   374  		if err != nil {
   375  			return nil, err
   376  		}
   377  		loggerLevels[name] = LogAndDisplayLevels{
   378  			LogLevel:     logLevel,
   379  			DisplayLevel: displayLevel,
   380  		}
   381  	}
   382  	return loggerLevels, nil
   383  }
   384  
   385  type DBGetArgs struct {
   386  	Key string `json:"key"`
   387  }
   388  
   389  type DBGetReply struct {
   390  	Value     string        `json:"value"`
   391  	ErrorCode rpcdbpb.Error `json:"errorCode"`
   392  }
   393  
   394  //nolint:stylecheck // renaming this method to DBGet would change the API method from "dbGet" to "dBGet"
   395  func (a *Admin) DbGet(_ *http.Request, args *DBGetArgs, reply *DBGetReply) error {
   396  	a.Log.Debug("API called",
   397  		zap.String("service", "admin"),
   398  		zap.String("method", "dbGet"),
   399  		logging.UserString("key", args.Key),
   400  	)
   401  
   402  	key, err := formatting.Decode(formatting.HexNC, args.Key)
   403  	if err != nil {
   404  		return err
   405  	}
   406  
   407  	value, err := a.DB.Get(key)
   408  	if err != nil {
   409  		reply.ErrorCode = rpcdb.ErrorToErrEnum[err]
   410  		return rpcdb.ErrorToRPCError(err)
   411  	}
   412  
   413  	reply.Value, err = formatting.Encode(formatting.HexNC, value)
   414  	return err
   415  }