github.com/cgcardona/r-subnet-evm@v0.1.5/tests/utils/runner/network_manager.go (about) 1 // Copyright (C) 2023, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package runner 5 6 import ( 7 "context" 8 "fmt" 9 "os" 10 "time" 11 12 runner_sdk "github.com/ava-labs/avalanche-network-runner/client" 13 "github.com/ava-labs/avalanche-network-runner/rpcpb" 14 runner_server "github.com/ava-labs/avalanche-network-runner/server" 15 "github.com/ava-labs/avalanchego/ids" 16 "github.com/ava-labs/avalanchego/utils/logging" 17 "github.com/ava-labs/avalanchego/utils/wrappers" 18 "github.com/cgcardona/r-subnet-evm/plugin/evm" 19 "github.com/ethereum/go-ethereum/log" 20 "github.com/onsi/ginkgo/v2" 21 "github.com/onsi/gomega" 22 ) 23 24 // Subnet provides the basic details of a created subnet 25 // Note: currently assumes one blockchain per subnet 26 type Subnet struct { 27 // SubnetID is the txID of the transaction that created the subnet 28 SubnetID ids.ID 29 // Current ANR assumes one blockchain per subnet, so we have a single blockchainID here 30 BlockchainID ids.ID 31 // ValidatorURIs is the base URIs for each participant of the Subnet 32 ValidatorURIs []string 33 } 34 35 type ANRConfig struct { 36 LogLevel string 37 AvalancheGoExecPath string 38 PluginDir string 39 GlobalNodeConfig string 40 } 41 42 // NetworkManager is a wrapper around the ANR to simplify the setup and teardown code 43 // of tests that rely on the ANR. 44 type NetworkManager struct { 45 ANRConfig ANRConfig 46 47 subnets []*Subnet 48 49 logFactory logging.Factory 50 anrClient runner_sdk.Client 51 anrServer runner_server.Server 52 done chan struct{} 53 serverCtxCancel context.CancelFunc 54 } 55 56 // NewDefaultANRConfig returns a default config for launching the avalanche-network-runner manager 57 // with both a server and client. 58 // By default, it expands $GOPATH/src/github.com/ava-labs/avalanchego/build/ directory to extract 59 // the AvalancheGoExecPath and PluginDir arguments. 60 // If the AVALANCHEGO_BUILD_PATH environment variable is set, it overrides the default location for 61 // the AvalancheGoExecPath and PluginDir arguments. 62 func NewDefaultANRConfig() ANRConfig { 63 defaultConfig := ANRConfig{ 64 LogLevel: "info", 65 AvalancheGoExecPath: os.ExpandEnv("$GOPATH/src/github.com/ava-labs/avalanchego/build/avalanchego"), 66 PluginDir: os.ExpandEnv("$GOPATH/src/github.com/ava-labs/avalanchego/build/plugins"), 67 GlobalNodeConfig: `{ 68 "log-display-level":"info", 69 "proposervm-use-current-height":true 70 }`, 71 } 72 // If AVALANCHEGO_BUILD_PATH is populated, override location set by GOPATH 73 if envBuildPath, exists := os.LookupEnv("AVALANCHEGO_BUILD_PATH"); exists { 74 defaultConfig.AvalancheGoExecPath = fmt.Sprintf("%s/avalanchego", envBuildPath) 75 defaultConfig.PluginDir = fmt.Sprintf("%s/plugins", envBuildPath) 76 } 77 return defaultConfig 78 } 79 80 // NewNetworkManager constructs a new instance of a network manager 81 func NewNetworkManager(config ANRConfig) *NetworkManager { 82 manager := &NetworkManager{ 83 ANRConfig: config, 84 } 85 86 logLevel, err := logging.ToLevel(config.LogLevel) 87 if err != nil { 88 panic(fmt.Errorf("invalid ANR log level: %w", err)) 89 } 90 manager.logFactory = logging.NewFactory(logging.Config{ 91 DisplayLevel: logLevel, 92 LogLevel: logLevel, 93 }) 94 95 return manager 96 } 97 98 // startServer starts a new ANR server and sets/overwrites the anrServer, done channel, and serverCtxCancel function. 99 func (n *NetworkManager) startServer(ctx context.Context) (<-chan struct{}, error) { 100 done := make(chan struct{}) 101 zapServerLog, err := n.logFactory.Make("server") 102 if err != nil { 103 return nil, fmt.Errorf("failed to make server log: %w", err) 104 } 105 106 n.anrServer, err = runner_server.New( 107 runner_server.Config{ 108 Port: ":12352", 109 GwPort: ":12353", 110 GwDisabled: false, 111 DialTimeout: 10 * time.Second, 112 RedirectNodesOutput: true, 113 SnapshotsDir: "", 114 }, 115 zapServerLog, 116 ) 117 if err != nil { 118 return nil, fmt.Errorf("failed to start ANR server: %w", err) 119 } 120 n.done = done 121 122 // Use a separate background context here, since the server should only be canceled by explicit shutdown 123 serverCtx, serverCtxCancel := context.WithCancel(context.Background()) 124 n.serverCtxCancel = serverCtxCancel 125 go func() { 126 if err := n.anrServer.Run(serverCtx); err != nil { 127 log.Error("Error shutting down ANR server", "err", err) 128 } else { 129 log.Info("Terminating ANR Server") 130 } 131 close(done) 132 }() 133 134 return done, nil 135 } 136 137 // startClient starts an ANR Client dialing the ANR server at the expected endpoint. 138 // Note: will overwrite client if it already exists. 139 func (n *NetworkManager) startClient() error { 140 logLevel, err := logging.ToLevel(n.ANRConfig.LogLevel) 141 if err != nil { 142 return fmt.Errorf("failed to parse ANR log level: %w", err) 143 } 144 logFactory := logging.NewFactory(logging.Config{ 145 DisplayLevel: logLevel, 146 LogLevel: logLevel, 147 }) 148 zapLog, err := logFactory.Make("main") 149 if err != nil { 150 return fmt.Errorf("failed to make client log: %w", err) 151 } 152 153 n.anrClient, err = runner_sdk.New(runner_sdk.Config{ 154 Endpoint: "0.0.0.0:12352", 155 DialTimeout: 10 * time.Second, 156 }, zapLog) 157 if err != nil { 158 return fmt.Errorf("failed to start ANR client: %w", err) 159 } 160 161 return nil 162 } 163 164 // initServer starts the ANR server if it is not populated 165 func (n *NetworkManager) initServer() error { 166 if n.anrServer != nil { 167 return nil 168 } 169 170 _, err := n.startServer(context.Background()) 171 return err 172 } 173 174 // initClient starts an ANR client if it not populated 175 func (n *NetworkManager) initClient() error { 176 if n.anrClient != nil { 177 return nil 178 } 179 180 return n.startClient() 181 } 182 183 // init starts the ANR server and client if they are not yet populated 184 func (n *NetworkManager) init() error { 185 if err := n.initServer(); err != nil { 186 return err 187 } 188 return n.initClient() 189 } 190 191 // StartDefaultNetwork constructs a default 5 node network. 192 func (n *NetworkManager) StartDefaultNetwork(ctx context.Context) (<-chan struct{}, error) { 193 if err := n.init(); err != nil { 194 return nil, err 195 } 196 197 log.Info("Sending 'start'", "AvalancheGoExecPath", n.ANRConfig.AvalancheGoExecPath) 198 199 // Start cluster 200 resp, err := n.anrClient.Start( 201 ctx, 202 n.ANRConfig.AvalancheGoExecPath, 203 runner_sdk.WithPluginDir(n.ANRConfig.PluginDir), 204 runner_sdk.WithGlobalNodeConfig(n.ANRConfig.GlobalNodeConfig), 205 ) 206 if err != nil { 207 return nil, fmt.Errorf("failed to start ANR network: %w", err) 208 } 209 log.Info("successfully started cluster", "RootDataDir", resp.ClusterInfo.RootDataDir, "Subnets", resp.GetClusterInfo().GetSubnets()) 210 return n.done, nil 211 } 212 213 // SetupNetwork constructs blockchains with the given [blockchainSpecs] and adds them to the network manager. 214 // Uses [execPath] as the AvalancheGo binary execution path for any started nodes. 215 // Note: this assumes that the default network has already been constructed. 216 func (n *NetworkManager) SetupNetwork(ctx context.Context, execPath string, blockchainSpecs []*rpcpb.BlockchainSpec) error { 217 cctx, cancel := context.WithTimeout(ctx, 2*time.Minute) 218 defer cancel() 219 if err := n.init(); err != nil { 220 return err 221 } 222 sresp, err := n.anrClient.CreateBlockchains( 223 ctx, 224 blockchainSpecs, 225 ) 226 if err != nil { 227 return fmt.Errorf("failed to create blockchains: %w", err) 228 } 229 230 // TODO: network runner health should imply custom VM healthiness 231 // or provide a separate API for custom VM healthiness 232 // "start" is async, so wait some time for cluster health 233 log.Info("waiting for all VMs to report healthy", "VMID", evm.ID) 234 for { 235 v, err := n.anrClient.Health(ctx) 236 log.Info("Pinged CLI Health", "result", v, "err", err) 237 if err != nil { 238 time.Sleep(1 * time.Second) 239 continue 240 } else if ctx.Err() != nil { 241 return fmt.Errorf("failed to await healthy network: %w", ctx.Err()) 242 } 243 break 244 } 245 246 status, err := n.anrClient.Status(cctx) 247 if err != nil { 248 return fmt.Errorf("failed to get ANR status: %w", err) 249 } 250 nodeInfos := status.GetClusterInfo().GetNodeInfos() 251 252 for _, chainSpec := range blockchainSpecs { 253 blockchainIDStr := sresp.ChainIds[0] 254 blockchainID, err := ids.FromString(blockchainIDStr) 255 if err != nil { 256 panic(err) 257 } 258 subnetIDStr := sresp.ClusterInfo.CustomChains[blockchainIDStr].SubnetId 259 subnetID, err := ids.FromString(subnetIDStr) 260 if err != nil { 261 panic(err) 262 } 263 subnet := &Subnet{ 264 SubnetID: subnetID, 265 BlockchainID: blockchainID, 266 } 267 for _, nodeName := range chainSpec.SubnetSpec.Participants { 268 subnet.ValidatorURIs = append(subnet.ValidatorURIs, nodeInfos[nodeName].Uri) 269 } 270 n.subnets = append(n.subnets, subnet) 271 } 272 273 return nil 274 } 275 276 // TeardownNetwork tears down the network constructed by the network manager and cleans up 277 // everything associated with it. 278 func (n *NetworkManager) TeardownNetwork() error { 279 if err := n.initClient(); err != nil { 280 return err 281 } 282 errs := wrappers.Errs{} 283 log.Info("Shutting down cluster") 284 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) 285 _, err := n.anrClient.Stop(ctx) 286 cancel() 287 errs.Add(err) 288 errs.Add(n.anrClient.Close()) 289 if n.serverCtxCancel != nil { 290 n.serverCtxCancel() 291 } 292 return errs.Err 293 } 294 295 // CloseClient closes the connection between the ANR client and server without terminating the 296 // running network. 297 func (n *NetworkManager) CloseClient() error { 298 if n.anrClient == nil { 299 return nil 300 } 301 err := n.anrClient.Close() 302 n.anrClient = nil 303 return err 304 } 305 306 // GetSubnets returns the IDs of the currently running subnets 307 func (n *NetworkManager) GetSubnets() []ids.ID { 308 subnetIDs := make([]ids.ID, 0, len(n.subnets)) 309 for _, subnet := range n.subnets { 310 subnetIDs = append(subnetIDs, subnet.SubnetID) 311 } 312 return subnetIDs 313 } 314 315 // GetSubnet retrieves the subnet details for the requested subnetID 316 func (n *NetworkManager) GetSubnet(subnetID ids.ID) (*Subnet, bool) { 317 for _, subnet := range n.subnets { 318 if subnet.SubnetID == subnetID { 319 return subnet, true 320 } 321 } 322 return nil, false 323 } 324 325 func RegisterFiveNodeSubnetRun() func() *Subnet { 326 var ( 327 config = NewDefaultANRConfig() 328 manager = NewNetworkManager(config) 329 numNodes = 5 330 ) 331 332 _ = ginkgo.BeforeSuite(func() { 333 // Name 10 new validators (which should have BLS key registered) 334 subnetA := make([]string, 0) 335 for i := 1; i <= numNodes; i++ { 336 subnetA = append(subnetA, fmt.Sprintf("node%d-bls", i)) 337 } 338 339 ctx := context.Background() 340 var err error 341 _, err = manager.StartDefaultNetwork(ctx) 342 gomega.Expect(err).Should(gomega.BeNil()) 343 err = manager.SetupNetwork( 344 ctx, 345 config.AvalancheGoExecPath, 346 []*rpcpb.BlockchainSpec{ 347 { 348 VmName: evm.IDStr, 349 Genesis: "./tests/load/genesis/genesis.json", 350 ChainConfig: "", 351 SubnetSpec: &rpcpb.SubnetSpec{ 352 Participants: subnetA, 353 }, 354 }, 355 }, 356 ) 357 gomega.Expect(err).Should(gomega.BeNil()) 358 }) 359 360 var _ = ginkgo.AfterSuite(func() { 361 gomega.Expect(manager).ShouldNot(gomega.BeNil()) 362 gomega.Expect(manager.TeardownNetwork()).Should(gomega.BeNil()) 363 // TODO: bootstrap an additional node to ensure that we can bootstrap the test data correctly 364 }) 365 366 return func() *Subnet { 367 subnetIDs := manager.GetSubnets() 368 gomega.Expect(len(subnetIDs)).Should(gomega.Equal(1)) 369 subnetID := subnetIDs[0] 370 subnetDetails, ok := manager.GetSubnet(subnetID) 371 gomega.Expect(ok).Should(gomega.BeTrue()) 372 return subnetDetails 373 } 374 }