Skip to content

Commit

Permalink
cmd/tools/placement-test: cli to test placement configuration
Browse files Browse the repository at this point in the history
Change-Id: I7308fbf8fcd740fc136e87d9c2c08eaeb461a106
  • Loading branch information
elek authored and Storj Robot committed Sep 28, 2023
1 parent 41799ef commit b28439b
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 1 deletion.
148 changes: 148 additions & 0 deletions cmd/tools/placement-test/main.go
@@ -0,0 +1,148 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.

package main

import (
"context"
"encoding/json"
"fmt"
"strings"
"time"

"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/zeebo/errs"
"go.uber.org/zap"

"storj.io/common/storj"
"storj.io/common/storj/location"
"storj.io/private/process"
"storj.io/storj/satellite/nodeselection"
"storj.io/storj/satellite/overlay"
)

var (
rootCmd = &cobra.Command{
Use: "placement-test <countrycode:...,lastipport:...,lastnet:...,tag:signer/key/value,tag:signer/key/value...>",
Short: "Test placement settings",
Long: `"This command helps testing placement configuration.
You can define a custom node with attributes, and all available placement configuration will be tested against the node.
Supported node attributes:
* countrycode
* lastipport
* lastnet
* tag (value should be in the form of signer/key/value)
EXAMPLES:
placement-test --placement '10:country("GB");12:country("DE")' countrycode=11
placement-test --placement /tmp/proposal.txt countrycode=US,tag=12Q8q2PofHPwycSwAVCpjNxxzWiDJhi8UV4ceZBo4hmNARpYcR7/soc2/true
Where /tmp/proposal.txt contains definitions, for example:
10:tag("12Q8q2PofHPwycSwAVCpjNxxzWiDJhi8UV4ceZBo4hmNARpYcR7","selected",notEmpty());
1:country("EU") && exclude(placement(10)) && annotation("location","eu-1");
2:country("EEA") && exclude(placement(10)) && annotation("location","eea-1");
3:country("US") && exclude(placement(10)) && annotation("location","us-1");
4:country("DE") && exclude(placement(10)) && annotation("location","de-1");
6:country("*","!BY", "!RU", "!NONE") && exclude(placement(10)) && annotation("location","custom-1")
`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx, _ := process.Ctx(cmd)
return testPlacement(ctx, args[0])
},
}

config Config
)

func testPlacement(ctx context.Context, fakeNode string) error {
node := &nodeselection.SelectedNode{}
for _, part := range strings.Split(fakeNode, ",") {
kv := strings.SplitN(part, "=", 2)
switch strings.ToLower(kv[0]) {
case "countrycode":
node.CountryCode = location.ToCountryCode(kv[1])
case "lastipport":
node.LastIPPort = kv[1]
case "lastnet":
node.LastNet = kv[1]
case "tag":
tkv := strings.SplitN(kv[1], "/", 3)
signer, err := storj.NodeIDFromString(tkv[0])
if err != nil {
return err
}
node.Tags = append(node.Tags, nodeselection.NodeTag{
Name: tkv[1],
Value: []byte(tkv[2]),
Signer: signer,
SignedAt: time.Now(),
NodeID: node.ID,
})
default:
panic("Unsupported field of SelectedNode: " + kv[0])
}

}

placement, err := config.Placement.Parse()
if err != nil {
return errs.Wrap(err)
}

fmt.Println("Node:")
jsonNode, err := json.MarshalIndent(node, " ", " ")
if err != nil {
return errs.Wrap(err)
}

fmt.Println(string(jsonNode))

for _, placementNum := range placement.SupportedPlacements() {
fmt.Printf("\n--------- Evaluating placement rule %d ---------\n", placementNum)
filter := placement.CreateFilters(placementNum)

fmt.Printf("Placement: %s\n", filter)
result := filter.Match(node)
fmt.Println("MATCH: ", result)
fmt.Println("Annotations: ")
if annotated, ok := filter.(nodeselection.NodeFilterWithAnnotation); ok {
fmt.Println(" location:", annotated.GetAnnotation("location"))
fmt.Println(" "+nodeselection.AutoExcludeSubnet+":", annotated.GetAnnotation(nodeselection.AutoExcludeSubnet))
} else {
fmt.Println(" no annotation presents")
}
}
return nil
}

// Config contains configuration of placement.
type Config struct {
Placement overlay.ConfigurablePlacementRule `help:"detailed placement rules in the form 'id:definition;id:definition;...' where id is a 16 bytes integer (use >10 for backward compatibility), definition is a combination of the following functions:country(2 letter country codes,...), tag(nodeId, key, bytes(value)) all(...,...)."`
}

func init() {
process.Bind(rootCmd, &config)
}

func main() {
process.ExecWithCustomOptions(rootCmd, process.ExecOptions{
LoadConfig: func(cmd *cobra.Command, vip *viper.Viper) error {
return nil
},
InitTracing: false,
LoggerFactory: func(logger *zap.Logger) *zap.Logger {
newLogger, level, err := process.NewLogger("placement-test")
if err != nil {
panic(err)
}
level.SetLevel(zap.WarnLevel)
return newLogger
},
})
}
25 changes: 24 additions & 1 deletion satellite/overlay/placement.go
Expand Up @@ -5,6 +5,7 @@ package overlay

import (
"bytes"
"os"
"strconv"
"strings"

Expand Down Expand Up @@ -48,9 +49,20 @@ func (c *ConfigurablePlacementRule) Type() string {

// Parse creates the PlacementDefinitions from the string rules.
func (c ConfigurablePlacementRule) Parse() (*PlacementDefinitions, error) {
rules := c.PlacementRules
if _, err := os.Stat(rules); err == nil {
ruleBytes, err := os.ReadFile(rules)
if err != nil {
return nil, errs.New("Placement definition file couldn't be read: %s %v", rules, err)
}
rules = string(ruleBytes)
}
if strings.HasPrefix(rules, "/") || strings.HasPrefix(rules, "./") || strings.HasPrefix(rules, "../") {
return nil, errs.New("Placement definition (%s) looks to be a path, but file doesn't exist at that place", rules)
}
d := NewPlacementDefinitions()
d.AddLegacyStaticRules()
err := d.AddPlacementFromString(c.PlacementRules)
err := d.AddPlacementFromString(rules)
return d, err
}

Expand Down Expand Up @@ -160,6 +172,9 @@ func (d *PlacementDefinitions) AddPlacementFromString(definitions string) error
}
idDef := strings.SplitN(definition, ":", 2)

if len(idDef) != 2 {
return errs.New("placement definition should be in the form ID:definition (but it was %s)", definition)
}
val, err := mito.Eval(idDef[1], env)
if err != nil {
return errs.Wrap(err)
Expand All @@ -182,3 +197,11 @@ func (d *PlacementDefinitions) CreateFilters(constraint storj.PlacementConstrain
nodeselection.ExcludeAllFilter{},
}
}

// SupportedPlacements returns all the IDs, which have associated placement rules.
func (d *PlacementDefinitions) SupportedPlacements() (res []storj.PlacementConstraint) {
for id := range d.placements {
res = append(res, id)
}
return res
}

0 comments on commit b28439b

Please sign in to comment.