package storagecfg import ( "bytes" "context" "encoding/hex" "errors" "fmt" "math/rand" "net" "net/url" "os" "path/filepath" "strconv" "strings" "text/template" "time" netutil "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network" "github.com/chzyer/readline" "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" "github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/spf13/cobra" ) const ( walletFlag = "wallet" accountFlag = "account" ) const ( defaultControlEndpoint = "localhost:8090" defaultDataEndpoint = "localhost" ) // RootCmd is a root command of config section. var RootCmd = &cobra.Command{ Use: "storage-config [-w wallet] [-a acccount] []", Short: "Section for storage node configuration commands", Run: storageConfig, } func init() { fs := RootCmd.Flags() fs.StringP(walletFlag, "w", "", "Path to wallet") fs.StringP(accountFlag, "a", "", "Wallet account") } type config struct { AnnouncedAddress string AuthorizedKeys []string ControlEndpoint string Endpoint string TLSCert string TLSKey string MorphRPC []string Attribute struct { Locode string } Wallet struct { Path string Account string Password string } Relay bool BlobstorPath string MetabasePath string } func storageConfig(cmd *cobra.Command, args []string) { outPath := getOutputPath(args) historyPath := filepath.Join(os.TempDir(), "frostfs-adm.history") readline.SetHistoryPath(historyPath) var c config c.Wallet.Path, _ = cmd.Flags().GetString(walletFlag) if c.Wallet.Path == "" { c.Wallet.Path = getPath("Path to the storage node wallet: ") } w, err := wallet.NewWalletFromFile(c.Wallet.Path) fatalOnErr(err) fillWalletAccount(cmd, &c, w) accH, err := flags.ParseAddress(c.Wallet.Account) fatalOnErr(err) acc := w.GetAccount(accH) if acc == nil { fatalOnErr(errors.New("can't find account in wallet")) } c.Wallet.Password, err = input.ReadPassword(fmt.Sprintf("Account password for %s: ", c.Wallet.Account)) fatalOnErr(err) err = acc.Decrypt(c.Wallet.Password, keys.NEP2ScryptParams()) fatalOnErr(err) c.AuthorizedKeys = append(c.AuthorizedKeys, hex.EncodeToString(acc.PrivateKey().PublicKey().Bytes())) network := readNetwork(cmd) c.MorphRPC = n3config[network].MorphRPC depositGas(cmd, acc, network) c.Attribute.Locode = getString("UN-LOCODE attribute in [XX YYY] format: ") endpoint := getDefaultEndpoint(cmd, &c) c.Endpoint = getString(fmt.Sprintf("Listening address [%s]: ", endpoint)) if c.Endpoint == "" { c.Endpoint = endpoint } c.ControlEndpoint = getString(fmt.Sprintf("Listening address (control endpoint) [%s]: ", defaultControlEndpoint)) if c.ControlEndpoint == "" { c.ControlEndpoint = defaultControlEndpoint } c.TLSCert = getPath("TLS Certificate (optional): ") if c.TLSCert != "" { c.TLSKey = getPath("TLS Key: ") } c.Relay = getConfirmation(false, "Use node as a relay? yes/[no]: ") if !c.Relay { p := getPath("Path to the storage directory (all available storage will be used): ") c.BlobstorPath = filepath.Join(p, "blob") c.MetabasePath = filepath.Join(p, "meta") } out := applyTemplate(c) fatalOnErr(os.WriteFile(outPath, out, 0o644)) cmd.Println("Node is ready for work! Run `frostfs-node -config " + outPath + "`") } func getDefaultEndpoint(cmd *cobra.Command, c *config) string { var addr, port string for { c.AnnouncedAddress = getString("Publicly announced address: ") validator := netutil.Address{} err := validator.FromString(c.AnnouncedAddress) if err != nil { cmd.Println("Incorrect address format. See https://git.frostfs.info/TrueCloudLab/frostfs-node/src/branch/master/pkg/network/address.go for details.") continue } uriAddr, err := url.Parse(validator.URIAddr()) if err != nil { panic(fmt.Errorf("unexpected error: %w", err)) } addr = uriAddr.Hostname() port = uriAddr.Port() ip, err := net.ResolveIPAddr("ip", addr) if err != nil { cmd.Printf("Can't resolve IP address %s: %v\n", addr, err) continue } if !ip.IP.IsGlobalUnicast() { cmd.Println("IP must be global unicast.") continue } cmd.Printf("Resolved IP address: %s\n", ip.String()) _, err = strconv.ParseUint(port, 10, 16) if err != nil { cmd.Println("Port must be an integer.") continue } break } return net.JoinHostPort(defaultDataEndpoint, port) } func fillWalletAccount(cmd *cobra.Command, c *config, w *wallet.Wallet) { c.Wallet.Account, _ = cmd.Flags().GetString(accountFlag) if c.Wallet.Account == "" { addr := address.Uint160ToString(w.GetChangeAddress()) c.Wallet.Account = getWalletAccount(w, fmt.Sprintf("Wallet account [%s]: ", addr)) if c.Wallet.Account == "" { c.Wallet.Account = addr } } } func readNetwork(cmd *cobra.Command) string { var network string for { network = getString("Choose network [mainnet]/testnet: ") switch network { case "": network = "mainnet" case "testnet", "mainnet": default: cmd.Println(`Network must be either "mainnet" or "testnet"`) continue } break } return network } func getOutputPath(args []string) string { if len(args) != 0 { return args[0] } outPath := getPath("File to write config at [./config.yml]: ") if outPath == "" { outPath = "./config.yml" } return outPath } func getWalletAccount(w *wallet.Wallet, prompt string) string { addrs := make([]readline.PrefixCompleterInterface, len(w.Accounts)) for i := range w.Accounts { addrs[i] = readline.PcItem(w.Accounts[i].Address) } readline.SetAutoComplete(readline.NewPrefixCompleter(addrs...)) defer readline.SetAutoComplete(nil) s, err := readline.Line(prompt) fatalOnErr(err) return strings.TrimSpace(s) // autocompleter can return a string with a trailing space } func getString(prompt string) string { s, err := readline.Line(prompt) fatalOnErr(err) if s != "" { _ = readline.AddHistory(s) } return s } type filenameCompleter struct{} func (filenameCompleter) Do(line []rune, pos int) (newLine [][]rune, length int) { prefix := string(line[:pos]) dir := filepath.Dir(prefix) de, err := os.ReadDir(dir) if err != nil { return nil, 0 } for i := range de { name := filepath.Join(dir, de[i].Name()) if strings.HasPrefix(name, prefix) { tail := []rune(strings.TrimPrefix(name, prefix)) if de[i].IsDir() { tail = append(tail, filepath.Separator) } newLine = append(newLine, tail) } } if pos != 0 { return newLine, pos - len([]rune(dir)) } return newLine, 0 } func getPath(prompt string) string { readline.SetAutoComplete(filenameCompleter{}) defer readline.SetAutoComplete(nil) p, err := readline.Line(prompt) fatalOnErr(err) if p == "" { return p } _ = readline.AddHistory(p) abs, err := filepath.Abs(p) if err != nil { fatalOnErr(fmt.Errorf("can't create an absolute path: %w", err)) } return abs } func getConfirmation(def bool, prompt string) bool { for { s, err := readline.Line(prompt) fatalOnErr(err) switch strings.ToLower(s) { case "y", "yes": return true case "n", "no": return false default: if len(s) == 0 { return def } } } } func applyTemplate(c config) []byte { tmpl, err := template.New("config").Parse(configTemplate) fatalOnErr(err) b := bytes.NewBuffer(nil) fatalOnErr(tmpl.Execute(b, c)) return b.Bytes() } func fatalOnErr(err error) { if err != nil { _, _ = fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } } func depositGas(cmd *cobra.Command, acc *wallet.Account, network string) { sideClient := initClient(n3config[network].MorphRPC) balanceHash, _ := util.Uint160DecodeStringLE(n3config[network].BalanceContract) sideActor, err := actor.NewSimple(sideClient, acc) if err != nil { fatalOnErr(fmt.Errorf("creating actor over side chain client: %w", err)) } sideGas := nep17.NewReader(sideActor, balanceHash) accSH := acc.Contract.ScriptHash() balance, err := sideGas.BalanceOf(accSH) if err != nil { fatalOnErr(fmt.Errorf("side chain balance: %w", err)) } ok := getConfirmation(false, fmt.Sprintf("Current NeoFS balance is %s, make a deposit? y/[n]: ", fixedn.ToString(balance, 12))) if !ok { return } amountStr := getString("Enter amount in GAS: ") amount, err := fixedn.FromString(amountStr, 8) if err != nil { fatalOnErr(fmt.Errorf("invalid amount: %w", err)) } mainClient := initClient(n3config[network].RPC) neofsHash, _ := util.Uint160DecodeStringLE(n3config[network].NeoFSContract) mainActor, err := actor.NewSimple(mainClient, acc) if err != nil { fatalOnErr(fmt.Errorf("creating actor over main chain client: %w", err)) } mainGas := nep17.New(mainActor, gas.Hash) txHash, _, err := mainGas.Transfer(accSH, neofsHash, amount, nil) if err != nil { fatalOnErr(fmt.Errorf("sending TX to the NeoFS contract: %w", err)) } cmd.Print("Waiting for transactions to persist.") tick := time.NewTicker(time.Second / 2) defer tick.Stop() timer := time.NewTimer(time.Second * 20) defer timer.Stop() at := trigger.Application loop: for { select { case <-tick.C: _, err := mainClient.GetApplicationLog(txHash, &at) if err == nil { cmd.Print("\n") break loop } cmd.Print(".") case <-timer.C: cmd.Printf("\nTimeout while waiting for transaction to persist.\n") if getConfirmation(false, "Continue configuration? yes/[no]: ") { return } os.Exit(1) } } } func initClient(rpc []string) *rpcclient.Client { var c *rpcclient.Client var err error shuffled := make([]string, len(rpc)) copy(shuffled, rpc) rand.Shuffle(len(shuffled), func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] }) for _, endpoint := range shuffled { c, err = rpcclient.New(context.Background(), "https://"+endpoint, rpcclient.Options{ DialTimeout: time.Second * 2, RequestTimeout: time.Second * 5, }) if err != nil { continue } if err = c.Init(); err != nil { continue } return c } fatalOnErr(fmt.Errorf("can't create N3 client: %w", err)) panic("unreachable") }