From 1dc9269d8918d5771f555c54f211acc40cf08430 Mon Sep 17 00:00:00 2001 From: Andrey Butusov Date: Wed, 28 Jan 2026 17:39:46 +0300 Subject: [PATCH 1/2] cli/container: add `policy check` command Add a CLI utility to validate container policies and display nodes that are policy-compliant in the current epoch. Closes #3626. Signed-off-by: Andrey Butusov --- CHANGELOG.md | 1 + cmd/neofs-cli/modules/container/create.go | 35 +----- .../modules/container/policy/check.go | 109 ++++++++++++++++++ .../modules/container/policy/root.go | 22 ++++ .../modules/container/policy/util.go | 42 +++++++ cmd/neofs-cli/modules/container/root.go | 2 + docs/cli-commands/neofs-cli_container.md | 1 + .../neofs-cli_container_policy.md | 26 +++++ .../neofs-cli_container_policy_check.md | 35 ++++++ 9 files changed, 240 insertions(+), 33 deletions(-) create mode 100644 cmd/neofs-cli/modules/container/policy/check.go create mode 100644 cmd/neofs-cli/modules/container/policy/root.go create mode 100644 cmd/neofs-cli/modules/container/policy/util.go create mode 100644 docs/cli-commands/neofs-cli_container_policy.md create mode 100644 docs/cli-commands/neofs-cli_container_policy_check.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7600ee38..e09170a589 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Changelog for NeoFS Node ## [Unreleased] ### Added +- `neofs-cli container policy check` command (#3790) ### Fixed diff --git a/cmd/neofs-cli/modules/container/create.go b/cmd/neofs-cli/modules/container/create.go index ead6c91b04..2c85fdf3fe 100644 --- a/cmd/neofs-cli/modules/container/create.go +++ b/cmd/neofs-cli/modules/container/create.go @@ -3,20 +3,18 @@ package container import ( "errors" "fmt" - "os" "strings" "time" internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client" - "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/common" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/key" + containerpolicy "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/container/policy" "github.com/nspcc-dev/neofs-sdk-go/client" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" "github.com/nspcc-dev/neofs-sdk-go/container" "github.com/nspcc-dev/neofs-sdk-go/container/acl" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/session" sessionv2 "github.com/nspcc-dev/neofs-sdk-go/session/v2" "github.com/nspcc-dev/neofs-sdk-go/user" @@ -46,7 +44,7 @@ It will be stored in FS chain when inner ring will accepts it.`, return errors.New("--global-name requires a name attribute") } - placementPolicy, err := parseContainerPolicy(cmd, containerPolicy) + placementPolicy, err := containerpolicy.ParseContainerPolicy(cmd, containerPolicy) if err != nil { return err } @@ -202,35 +200,6 @@ func initContainerCreateCmd() { flags.BoolVar(&containerGlobalName, "global-name", false, "Name becomes a domain name, that is registered with the default zone in NNS contract. Requires name attribute.") } -func parseContainerPolicy(cmd *cobra.Command, policyString string) (*netmap.PlacementPolicy, error) { - _, err := os.Stat(policyString) // check if `policyString` is a path to file with placement policy - if err == nil { - common.PrintVerbose(cmd, "Reading placement policy from file: %s", policyString) - - data, err := os.ReadFile(policyString) - if err != nil { - return nil, fmt.Errorf("can't read file with placement policy: %w", err) - } - - policyString = string(data) - } - - var result netmap.PlacementPolicy - - err = result.DecodeString(policyString) - if err == nil { - common.PrintVerbose(cmd, "Parsed QL encoded policy") - return &result, nil - } - - if err = result.UnmarshalJSON([]byte(policyString)); err == nil { - common.PrintVerbose(cmd, "Parsed JSON encoded policy") - return &result, nil - } - - return nil, errors.New("can't parse placement policy") -} - func parseAttributes(dst *container.Container, attributes []string) error { for i := range attributes { k, v, found := strings.Cut(attributes[i], attributeDelimiter) diff --git a/cmd/neofs-cli/modules/container/policy/check.go b/cmd/neofs-cli/modules/container/policy/check.go new file mode 100644 index 0000000000..d096cb1467 --- /dev/null +++ b/cmd/neofs-cli/modules/container/policy/check.go @@ -0,0 +1,109 @@ +package policy + +import ( + "fmt" + + "github.com/nspcc-dev/neofs-node/cmd/internal/cmdprinter" + internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" + iec "github.com/nspcc-dev/neofs-node/internal/ec" + "github.com/nspcc-dev/neofs-sdk-go/client" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/netmap" + "github.com/spf13/cobra" +) + +var ( + policyFlag string + short bool +) + +var checkCmd = &cobra.Command{ + Use: "check", + Short: "Check placement policy", + Long: `Check placement policy parsing and validation. +Policy can be provided as QL-encoded string, JSON-encoded string or path to file with it. +Shows nodes that will be used for container placement based on current network map snapshot.`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + placementPolicy, err := ParseContainerPolicy(cmd, policyFlag) + if err != nil { + return err + } + + ctx, cancel := commonflags.GetCommandContext(cmd) + defer cancel() + + cli, err := internalclient.GetSDKClientByFlag(ctx, commonflags.RPC) + if err != nil { + return err + } + defer cli.Close() + + nm, err := cli.NetMapSnapshot(ctx, client.PrmNetMapSnapshot{}) + if err != nil { + return fmt.Errorf("unable to get netmap snapshot to validate container placement: %w", err) + } + + placementNodes, err := nm.ContainerNodes(*placementPolicy, cid.ID{}) + if err != nil { + return fmt.Errorf("could not build container nodes based on given placement policy: %w", err) + } + + repRuleNum := placementPolicy.NumberOfReplicas() + for i := range repRuleNum { + if placementPolicy.ReplicaNumberByIndex(i) > uint32(len(placementNodes[i])) { + return fmt.Errorf( + "the number of nodes '%d' in selector is not enough for the number of replicas '%d'", + len(placementNodes[i]), + placementPolicy.ReplicaNumberByIndex(i), + ) + } + } + + ecRules := placementPolicy.ECRules() + for i := range ecRules { + d := ecRules[i].DataPartNum() + p := ecRules[i].ParityPartNum() + n := uint32(len(placementNodes[repRuleNum+i])) + if d > n || p > n || d+p > n { + return fmt.Errorf( + "the number of nodes '%d' in selector is not enough for EC rule '%d/%d'", n, d, p) + } + } + + printPolicyNodes(cmd, placementNodes, *placementPolicy, short) + return nil + }, +} + +func initCheckCmd() { + flags := checkCmd.Flags() + + flags.StringP(commonflags.RPC, commonflags.RPCShorthand, commonflags.RPCDefault, commonflags.RPCUsage) + flags.DurationP(commonflags.Timeout, commonflags.TimeoutShorthand, commonflags.TimeoutDefault, commonflags.TimeoutUsage) + flags.StringVarP(&policyFlag, "policy", "p", "", "QL-encoded or JSON-encoded placement policy or path to file with it") + flags.BoolVar(&short, "short", false, "Shortens output of node info") +} + +func printPolicyNodes(cmd *cobra.Command, policyNodes [][]netmap.NodeInfo, policy netmap.PlacementPolicy, short bool) { + repRuleNum := policy.NumberOfReplicas() + for i := range repRuleNum { + cmd.Printf("Descriptor #%d, REP %d:\n", i+1, policy.ReplicaNumberByIndex(i)) + for j := range policyNodes[i] { + cmdprinter.PrettyPrintNodeInfo(cmd, policyNodes[i][j], j, "\t", short) + } + } + + policyNodes = policyNodes[repRuleNum:] + ecRules := policy.ECRules() + for i := range ecRules { + cmd.Printf("EC descriptor #%d, EC %s:\n", i+1, iec.Rule{ + DataPartNum: uint8(ecRules[i].DataPartNum()), + ParityPartNum: uint8(ecRules[i].ParityPartNum()), + }) + for j := range policyNodes[i] { + cmdprinter.PrettyPrintNodeInfo(cmd, policyNodes[i][j], j, "\t", short) + } + } +} diff --git a/cmd/neofs-cli/modules/container/policy/root.go b/cmd/neofs-cli/modules/container/policy/root.go new file mode 100644 index 0000000000..422555bcd0 --- /dev/null +++ b/cmd/neofs-cli/modules/container/policy/root.go @@ -0,0 +1,22 @@ +package policy + +import ( + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" + "github.com/spf13/cobra" +) + +// Cmd represents the policy command. +var Cmd = &cobra.Command{ + Use: "policy", + Short: "Operations with container placement policy", + Long: "Operations with container placement policy", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + commonflags.Bind(cmd) + commonflags.BindAPI(cmd) + }, +} + +func init() { + Cmd.AddCommand(checkCmd) + initCheckCmd() +} diff --git a/cmd/neofs-cli/modules/container/policy/util.go b/cmd/neofs-cli/modules/container/policy/util.go new file mode 100644 index 0000000000..e3ebee0e57 --- /dev/null +++ b/cmd/neofs-cli/modules/container/policy/util.go @@ -0,0 +1,42 @@ +package policy + +import ( + "errors" + "fmt" + "os" + + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/common" + "github.com/nspcc-dev/neofs-sdk-go/netmap" + "github.com/spf13/cobra" +) + +// ParseContainerPolicy tries to parse the provided string as a path to file with placement policy, +// then as QL and JSON encoded policies. Returns an error if all attempts fail. +func ParseContainerPolicy(cmd *cobra.Command, policyString string) (*netmap.PlacementPolicy, error) { + _, err := os.Stat(policyString) // check if `policyString` is a path to file with placement policy + if err == nil { + common.PrintVerbose(cmd, "Reading placement policy from file: %s", policyString) + + data, err := os.ReadFile(policyString) + if err != nil { + return nil, fmt.Errorf("can't read file with placement policy: %w", err) + } + + policyString = string(data) + } + + var result netmap.PlacementPolicy + + err = result.DecodeString(policyString) + if err == nil { + common.PrintVerbose(cmd, "Parsed QL encoded policy") + return &result, nil + } + + if err = result.UnmarshalJSON([]byte(policyString)); err == nil { + common.PrintVerbose(cmd, "Parsed JSON encoded policy") + return &result, nil + } + + return nil, errors.New("can't parse placement policy") +} diff --git a/cmd/neofs-cli/modules/container/root.go b/cmd/neofs-cli/modules/container/root.go index d5d0852817..560b87e271 100644 --- a/cmd/neofs-cli/modules/container/root.go +++ b/cmd/neofs-cli/modules/container/root.go @@ -2,6 +2,7 @@ package container import ( "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" + "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/modules/container/policy" "github.com/spf13/cobra" ) @@ -33,6 +34,7 @@ func init() { } Cmd.AddCommand(containerChildCommand...) + Cmd.AddCommand(policy.Cmd) initContainerListContainersCmd() initContainerCreateCmd() diff --git a/docs/cli-commands/neofs-cli_container.md b/docs/cli-commands/neofs-cli_container.md index 10c752a96d..6346bf8da3 100644 --- a/docs/cli-commands/neofs-cli_container.md +++ b/docs/cli-commands/neofs-cli_container.md @@ -29,6 +29,7 @@ Operations with containers * [neofs-cli container list](neofs-cli_container_list.md) - List all created containers * [neofs-cli container list-objects](neofs-cli_container_list-objects.md) - List existing objects in container * [neofs-cli container nodes](neofs-cli_container_nodes.md) - Show nodes for container +* [neofs-cli container policy](neofs-cli_container_policy.md) - Operations with container placement policy * [neofs-cli container remove-attribute](neofs-cli_container_remove-attribute.md) - Remove container attribute * [neofs-cli container set-attribute](neofs-cli_container_set-attribute.md) - Set attribute for container * [neofs-cli container set-eacl](neofs-cli_container_set-eacl.md) - Set new extended ACL table for container diff --git a/docs/cli-commands/neofs-cli_container_policy.md b/docs/cli-commands/neofs-cli_container_policy.md new file mode 100644 index 0000000000..a8dde38ce3 --- /dev/null +++ b/docs/cli-commands/neofs-cli_container_policy.md @@ -0,0 +1,26 @@ +## neofs-cli container policy + +Operations with container placement policy + +### Synopsis + +Operations with container placement policy + +### Options + +``` + -h, --help help for policy +``` + +### Options inherited from parent commands + +``` + -c, --config string Config file (default is $HOME/.config/neofs-cli/config.yaml) + -v, --verbose Verbose output +``` + +### SEE ALSO + +* [neofs-cli container](neofs-cli_container.md) - Operations with containers +* [neofs-cli container policy check](neofs-cli_container_policy_check.md) - Check placement policy + diff --git a/docs/cli-commands/neofs-cli_container_policy_check.md b/docs/cli-commands/neofs-cli_container_policy_check.md new file mode 100644 index 0000000000..63bac8a204 --- /dev/null +++ b/docs/cli-commands/neofs-cli_container_policy_check.md @@ -0,0 +1,35 @@ +## neofs-cli container policy check + +Check placement policy + +### Synopsis + +Check placement policy parsing and validation. +Policy can be provided as QL-encoded string, JSON-encoded string or path to file with it. +Shows nodes that will be used for container placement based on current network map snapshot. + +``` +neofs-cli container policy check [flags] +``` + +### Options + +``` + -h, --help help for check + -p, --policy string QL-encoded or JSON-encoded placement policy or path to file with it + -r, --rpc-endpoint string Remote node address (as 'multiaddr' or ':') + --short Shortens output of node info + -t, --timeout duration Timeout for the operation (default 15s) +``` + +### Options inherited from parent commands + +``` + -c, --config string Config file (default is $HOME/.config/neofs-cli/config.yaml) + -v, --verbose Verbose output +``` + +### SEE ALSO + +* [neofs-cli container policy](neofs-cli_container_policy.md) - Operations with container placement policy + From b430c897437437930a8a3f0fb36b01058b4acb0d Mon Sep 17 00:00:00 2001 From: Andrey Butusov Date: Wed, 28 Jan 2026 17:43:49 +0300 Subject: [PATCH 2/2] cli: move repeated output to a separate internal function Signed-off-by: Andrey Butusov --- cmd/internal/cmdprinter/netmap.go | 24 +++++++++++++++++ cmd/neofs-cli/modules/container/nodes.go | 22 +--------------- .../modules/container/policy/check.go | 26 +------------------ cmd/neofs-cli/modules/object/nodes.go | 23 +--------------- 4 files changed, 27 insertions(+), 68 deletions(-) diff --git a/cmd/internal/cmdprinter/netmap.go b/cmd/internal/cmdprinter/netmap.go index a6d2b0b366..4d8ffbe1d3 100644 --- a/cmd/internal/cmdprinter/netmap.go +++ b/cmd/internal/cmdprinter/netmap.go @@ -3,6 +3,7 @@ package cmdprinter import ( "encoding/hex" + iec "github.com/nspcc-dev/neofs-node/internal/ec" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/spf13/cobra" ) @@ -47,3 +48,26 @@ func PrettyPrintNetMap(cmd *cobra.Command, nm netmap.NetMap) { PrettyPrintNodeInfo(cmd, nodes[i], i, "", false) } } + +// PrettyPrintPlacementPolicyNodes print information about placement policy nodes. +func PrettyPrintPlacementPolicyNodes(cmd *cobra.Command, policyNodes [][]netmap.NodeInfo, policy netmap.PlacementPolicy, short bool) { + repRuleNum := policy.NumberOfReplicas() + for i := range repRuleNum { + cmd.Printf("Descriptor #%d, REP %d:\n", i+1, policy.ReplicaNumberByIndex(i)) + for j := range policyNodes[i] { + PrettyPrintNodeInfo(cmd, policyNodes[i][j], j, "\t", short) + } + } + + policyNodes = policyNodes[repRuleNum:] + ecRules := policy.ECRules() + for i := range ecRules { + cmd.Printf("EC descriptor #%d, EC %s:\n", i+1, iec.Rule{ + DataPartNum: uint8(ecRules[i].DataPartNum()), // FIXME: do not cast to smaller integer + ParityPartNum: uint8(ecRules[i].ParityPartNum()), + }) + for j := range policyNodes[i] { + PrettyPrintNodeInfo(cmd, policyNodes[i][j], j, "\t", short) + } + } +} diff --git a/cmd/neofs-cli/modules/container/nodes.go b/cmd/neofs-cli/modules/container/nodes.go index 50f2b81fb7..2471701deb 100644 --- a/cmd/neofs-cli/modules/container/nodes.go +++ b/cmd/neofs-cli/modules/container/nodes.go @@ -6,7 +6,6 @@ import ( "github.com/nspcc-dev/neofs-node/cmd/internal/cmdprinter" internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" - iec "github.com/nspcc-dev/neofs-node/internal/ec" "github.com/nspcc-dev/neofs-sdk-go/client" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/netmap" @@ -52,26 +51,7 @@ var containerNodesCmd = &cobra.Command{ return fmt.Errorf("could not build container nodes for given container: %w", err) } - repRuleNum := policy.NumberOfReplicas() - for i := range repRuleNum { - cmd.Printf("Descriptor #%d, REP %d:\n", i+1, policy.ReplicaNumberByIndex(i)) - for j := range cnrNodes[i] { - cmdprinter.PrettyPrintNodeInfo(cmd, cnrNodes[i][j], j, "\t", short) - } - } - - cnrNodes = cnrNodes[repRuleNum:] - - ecRules := policy.ECRules() - for i := range ecRules { - cmd.Printf("EC descriptor #%d, EC %s:\n", i+1, iec.Rule{ - DataPartNum: uint8(ecRules[i].DataPartNum()), // FIXME: do not cast to smaller integer - ParityPartNum: uint8(ecRules[i].ParityPartNum()), - }) - for j := range cnrNodes[i] { - cmdprinter.PrettyPrintNodeInfo(cmd, cnrNodes[i][j], j, "\t", short) - } - } + cmdprinter.PrettyPrintPlacementPolicyNodes(cmd, cnrNodes, policy, short) return nil }, } diff --git a/cmd/neofs-cli/modules/container/policy/check.go b/cmd/neofs-cli/modules/container/policy/check.go index d096cb1467..292b2e9d3e 100644 --- a/cmd/neofs-cli/modules/container/policy/check.go +++ b/cmd/neofs-cli/modules/container/policy/check.go @@ -6,10 +6,8 @@ import ( "github.com/nspcc-dev/neofs-node/cmd/internal/cmdprinter" internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" - iec "github.com/nspcc-dev/neofs-node/internal/ec" "github.com/nspcc-dev/neofs-sdk-go/client" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" - "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/spf13/cobra" ) @@ -72,7 +70,7 @@ Shows nodes that will be used for container placement based on current network m } } - printPolicyNodes(cmd, placementNodes, *placementPolicy, short) + cmdprinter.PrettyPrintPlacementPolicyNodes(cmd, placementNodes, *placementPolicy, short) return nil }, } @@ -85,25 +83,3 @@ func initCheckCmd() { flags.StringVarP(&policyFlag, "policy", "p", "", "QL-encoded or JSON-encoded placement policy or path to file with it") flags.BoolVar(&short, "short", false, "Shortens output of node info") } - -func printPolicyNodes(cmd *cobra.Command, policyNodes [][]netmap.NodeInfo, policy netmap.PlacementPolicy, short bool) { - repRuleNum := policy.NumberOfReplicas() - for i := range repRuleNum { - cmd.Printf("Descriptor #%d, REP %d:\n", i+1, policy.ReplicaNumberByIndex(i)) - for j := range policyNodes[i] { - cmdprinter.PrettyPrintNodeInfo(cmd, policyNodes[i][j], j, "\t", short) - } - } - - policyNodes = policyNodes[repRuleNum:] - ecRules := policy.ECRules() - for i := range ecRules { - cmd.Printf("EC descriptor #%d, EC %s:\n", i+1, iec.Rule{ - DataPartNum: uint8(ecRules[i].DataPartNum()), - ParityPartNum: uint8(ecRules[i].ParityPartNum()), - }) - for j := range policyNodes[i] { - cmdprinter.PrettyPrintNodeInfo(cmd, policyNodes[i][j], j, "\t", short) - } - } -} diff --git a/cmd/neofs-cli/modules/object/nodes.go b/cmd/neofs-cli/modules/object/nodes.go index 06f188f748..3019bbbc71 100644 --- a/cmd/neofs-cli/modules/object/nodes.go +++ b/cmd/neofs-cli/modules/object/nodes.go @@ -6,7 +6,6 @@ import ( "github.com/nspcc-dev/neofs-node/cmd/internal/cmdprinter" internalclient "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/client" "github.com/nspcc-dev/neofs-node/cmd/neofs-cli/internal/commonflags" - iec "github.com/nspcc-dev/neofs-node/internal/ec" "github.com/nspcc-dev/neofs-node/pkg/morph/client/netmap" "github.com/nspcc-dev/neofs-sdk-go/client" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -72,27 +71,7 @@ var objectNodesCmd = &cobra.Command{ short, _ := cmd.Flags().GetBool(shortFlag) - repRuleNum := policy.NumberOfReplicas() - for i := range repRuleNum { - cmd.Printf("Descriptor #%d, REP %d:\n", i+1, policy.ReplicaNumberByIndex(i)) - for j := range placementNodes[i] { - cmdprinter.PrettyPrintNodeInfo(cmd, placementNodes[i][j], j, "\t", short) - } - } - - placementNodes = placementNodes[repRuleNum:] - - ecRules := policy.ECRules() - for i := range ecRules { - cmd.Printf("EC descriptor #%d, EC %s:\n", i+1, iec.Rule{ - DataPartNum: uint8(ecRules[i].DataPartNum()), - ParityPartNum: uint8(ecRules[i].ParityPartNum()), - }) - for j := range placementNodes[i] { - cmdprinter.PrettyPrintNodeInfo(cmd, placementNodes[i][j], j, "\t", short) - } - } - + cmdprinter.PrettyPrintPlacementPolicyNodes(cmd, placementNodes, policy, short) return nil }, }