diff --git a/CHANGELOG.md b/CHANGELOG.md index 11a682281b..1f3a827bd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ Changelog for NeoFS Node ### Added - Embedded Neo contracts in `contracts` dir (#2391) +- `dump-names` command for adm +- `renew-domain` command for adm ### Fixed diff --git a/cmd/neofs-adm/internal/modules/morph/dump_hashes.go b/cmd/neofs-adm/internal/modules/morph/dump_hashes.go index 3378d8a967..139cd7c950 100644 --- a/cmd/neofs-adm/internal/modules/morph/dump_hashes.go +++ b/cmd/neofs-adm/internal/modules/morph/dump_hashes.go @@ -269,20 +269,29 @@ func fillContractExpiration(cmd *cobra.Command, c Client, infos []contractDumpIn if err != nil { continue // OK for NNS itself, for example. } - elems := props.Value().([]stackitem.MapElement) - for _, e := range elems { - k, err := e.Key.TryBytes() - if err != nil { - continue - } + exp, err := expirationFromProperties(props) + if err != nil { + continue // Should be there, but who knows. + } + infos[i].expiration = exp + } +} + +func expirationFromProperties(props *stackitem.Map) (int64, error) { + elems := props.Value().([]stackitem.MapElement) + for _, e := range elems { + k, err := e.Key.TryBytes() + if err != nil { + continue + } - if string(k) == "expiration" { - v, err := e.Value.TryInteger() - if err != nil || !v.IsInt64() { - continue - } - infos[i].expiration = v.Int64() + if string(k) == "expiration" { + v, err := e.Value.TryInteger() + if err != nil || !v.IsInt64() { + continue } + return v.Int64(), nil } } + return 0, errors.New("not found") } diff --git a/cmd/neofs-adm/internal/modules/morph/dump_names.go b/cmd/neofs-adm/internal/modules/morph/dump_names.go new file mode 100644 index 0000000000..00b07b6267 --- /dev/null +++ b/cmd/neofs-adm/internal/modules/morph/dump_names.go @@ -0,0 +1,90 @@ +package morph + +import ( + "bytes" + "fmt" + "sort" + "strings" + "text/tabwriter" + "time" + + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + nameDomainFlag = "domain" +) + +type nameExp struct { + name string + exp int64 +} + +func dumpNames(cmd *cobra.Command, _ []string) error { + c, err := getN3Client(viper.GetViper()) + if err != nil { + return fmt.Errorf("can't create N3 client: %w", err) + } + cs, err := c.GetContractStateByID(1) // NNS. + if err != nil { + return err + } + var n11r = nep11.NewNonDivisibleReader(invoker.New(c, nil), cs.Hash) + tokIter, err := n11r.Tokens() + if err != nil { + return err + } + zone, _ := cmd.Flags().GetString(nameDomainFlag) + var res = make([]nameExp, 0) + for toks, err := tokIter.Next(10); len(toks) != 0 && err == nil; toks, err = tokIter.Next(10) { + for i := range toks { + var name = string(toks[i]) + if zone != "" && name != zone && !strings.HasSuffix(name, "."+zone) { + continue + } + props, err := n11r.Properties(toks[i]) + if err != nil { + cmd.PrintErrf("Error getting properties for %s: %v\n", name, err) + continue + } + exp, err := expirationFromProperties(props) + if err != nil { + cmd.PrintErrf("Error getting expiration from properties for %s: %v\n", name, err) + continue + } + res = append(res, nameExp{name: name, exp: exp}) + } + } + + sort.Slice(res, func(i, j int) bool { + var ( + iParts = strings.Split(res[i].name, ".") + jParts = strings.Split(res[j].name, ".") + ) + if len(iParts) != len(jParts) { + return len(iParts) < len(jParts) + } + for k := len(iParts) - 1; k >= 0; k-- { + var c = strings.Compare(iParts[k], jParts[k]) + if c != 0 { + return c == -1 + } + } + return false + }) + + buf := bytes.NewBuffer(nil) + tw := tabwriter.NewWriter(buf, 0, 2, 2, ' ', 0) + for i := range res { + _, _ = tw.Write([]byte(fmt.Sprintf("%s\t%s\n", + res[i].name, time.UnixMilli(res[i].exp).String()))) + } + _ = tw.Flush() + + cmd.Print(buf.String()) + + return nil +} diff --git a/cmd/neofs-adm/internal/modules/morph/renew_domain.go b/cmd/neofs-adm/internal/modules/morph/renew_domain.go new file mode 100644 index 0000000000..dfbb073ad1 --- /dev/null +++ b/cmd/neofs-adm/internal/modules/morph/renew_domain.go @@ -0,0 +1,73 @@ +package morph + +import ( + "errors" + "strings" + + "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +const ( + recursiveFlag = "recursive" +) + +func renewDomain(cmd *cobra.Command, _ []string) error { + dom, err := cmd.Flags().GetString(nameDomainFlag) + if err != nil { + return err + } + recursive, _ := cmd.Flags().GetBool(recursiveFlag) + wCtx, err := newInitializeContext(cmd, viper.GetViper()) + if err != nil { + return err + } + defer wCtx.close() + nns, err := wCtx.Client.GetContractStateByID(1) + if err != nil { + return err + } + var domains = make([]string, 0, 1) + if recursive { + var n11r = nep11.NewNonDivisibleReader(wCtx.ReadOnlyInvoker, nns.Hash) + tokIter, err := n11r.Tokens() + if err != nil { + return err + } + for toks, err := tokIter.Next(10); len(toks) != 0 && err == nil; toks, err = tokIter.Next(10) { + for i := range toks { + var name = string(toks[i]) + if name != dom && !strings.HasSuffix(name, "."+dom) { + continue + } + domains = append(domains, name) + } + } + } else { + avail, err := unwrap.Bool(wCtx.ReadOnlyInvoker.Call(nns.Hash, "isAvailable")) + if err == nil && avail { + return errors.New("domain is not registered or expired") + } + domains = append(domains, dom) + } + + bw := io.NewBufBinWriter() + for i := range domains { + emit.AppCall(bw.BinWriter, nns.Hash, "renew", callflag.All, domains[i]) + if bw.Err != nil { + return bw.Err + } + // Default registration price is 10 GAS, adding more domains + // into the script makes test execution to fail. + if err := wCtx.sendConsensusTx(bw.Bytes()); err != nil { + return err + } + bw.Reset() + } + return wCtx.awaitTx() +} diff --git a/cmd/neofs-adm/internal/modules/morph/root.go b/cmd/neofs-adm/internal/modules/morph/root.go index a33a99ef76..057390e33a 100644 --- a/cmd/neofs-adm/internal/modules/morph/root.go +++ b/cmd/neofs-adm/internal/modules/morph/root.go @@ -162,6 +162,15 @@ var ( RunE: dumpContractHashes, } + dumpNamesCmd = &cobra.Command{ + Use: "dump-names", + Short: "Dump known registred NNS names and expirations", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + RunE: dumpNames, + } + dumpNetworkConfigCmd = &cobra.Command{ Use: "dump-config", Short: "Dump NeoFS network config", @@ -199,6 +208,16 @@ var ( RunE: dumpContainers, } + renewDomainCmd = &cobra.Command{ + Use: "renew-domain", + Short: "Renew NNS domain", + PreRun: func(cmd *cobra.Command, _ []string) { + _ = viper.BindPFlag(alphabetWalletsFlag, cmd.Flags().Lookup(alphabetWalletsFlag)) + _ = viper.BindPFlag(endpointFlag, cmd.Flags().Lookup(endpointFlag)) + }, + RunE: renewDomain, + } + restoreContainersCmd = &cobra.Command{ Use: "restore-containers", Short: "Restore NeoFS containers from file", @@ -271,6 +290,10 @@ func init() { dumpContractHashesCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint") dumpContractHashesCmd.Flags().String(customZoneFlag, "", "Custom zone to search.") + RootCmd.AddCommand(dumpNamesCmd) + dumpNamesCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint") + dumpNamesCmd.Flags().StringP(nameDomainFlag, "d", "", "Filter by domain") + RootCmd.AddCommand(dumpNetworkConfigCmd) dumpNetworkConfigCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint") @@ -297,6 +320,12 @@ func init() { dumpContainersCmd.Flags().String(containerContractFlag, "", "Container contract hash (for networks without NNS)") dumpContainersCmd.Flags().StringSlice(containerIDsFlag, nil, "Containers to dump") + RootCmd.AddCommand(renewDomainCmd) + renewDomainCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir") + renewDomainCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint") + renewDomainCmd.Flags().StringP(nameDomainFlag, "d", "", "Domain") + renewDomainCmd.Flags().BoolP(recursiveFlag, "u", false, "Recursive (renew all subdomain as well)") + RootCmd.AddCommand(restoreContainersCmd) restoreContainersCmd.Flags().String(alphabetWalletsFlag, "", "Path to alphabet wallets dir") restoreContainersCmd.Flags().StringP(endpointFlag, "r", "", "N3 RPC node endpoint") diff --git a/docs/cli-adm.md b/docs/cli-adm.md index a80d114e23..e94e9c1dbe 100644 --- a/docs/cli-adm.md +++ b/docs/cli-adm.md @@ -70,11 +70,15 @@ credentials: # passwords for consensus node / alphabet wallets #### Network maintenance +- `dump-names` allows to walk through NNS names and see their expirations. + - `set-config` add/update configuration values in the Netmap contract. - `force-new-epoch` increments NeoFS epoch number and executes new epoch handlers in NeoFS nodes. +- `renew-domain` updates expiration date of the given domain for one year. + - `refill-gas` transfers sidechain GAS to the specified wallet. - `update-contracts` updates contracts to a new version.