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 }