cmd/multinode: add add command to multinode
This change adds an add command to the multinode CLI. The add command takes a json <file> as argument. If dash (-) is specified, it reads data from stdin. The <file> specified can be json file containing array of nodes data or a single node object. Change-Id: I44d68486dc9aea0bd0311a40e84d3262a0303aef
This commit is contained in:
parent
7fba79be3f
commit
60c8280565
@ -4,7 +4,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@ -13,10 +16,15 @@ import (
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/fpath"
|
||||
"storj.io/common/peertls/tlsopts"
|
||||
"storj.io/common/rpc"
|
||||
"storj.io/common/storj"
|
||||
"storj.io/private/cfgstruct"
|
||||
"storj.io/private/process"
|
||||
"storj.io/storj/multinode"
|
||||
"storj.io/storj/multinode/multinodedb"
|
||||
"storj.io/storj/multinode/nodes"
|
||||
"storj.io/storj/private/multinodeauth"
|
||||
)
|
||||
|
||||
// Config defines multinode configuration.
|
||||
@ -43,8 +51,33 @@ var (
|
||||
Annotations: map[string]string{"type": "setup"},
|
||||
}
|
||||
|
||||
runCfg Config
|
||||
setupCfg Config
|
||||
addCmd = &cobra.Command{
|
||||
Use: "add [file]",
|
||||
Short: "Add storage node(s) from file or stdin to multinode dashboard",
|
||||
RunE: cmdAdd,
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Example: `
|
||||
# add nodes from json file containing array of nodes data
|
||||
$ multinode add nodes.json
|
||||
|
||||
# add node from json file containing a single node object
|
||||
$ multinode add node.json
|
||||
|
||||
# read nodes data from stdin
|
||||
$ cat nodes.json | multinode add -
|
||||
`,
|
||||
}
|
||||
|
||||
runCfg Config
|
||||
setupCfg Config
|
||||
addCfg struct {
|
||||
NodeID string `help:"ID of the storage node" default:""`
|
||||
Name string `help:"Name of the storage node" default:""`
|
||||
APISecret string `help:"API Secret of the storage node" default:""`
|
||||
PublicAddress string `help:"Public IP Address of the storage node" default:""`
|
||||
|
||||
Config
|
||||
}
|
||||
confDir string
|
||||
identityDir string
|
||||
)
|
||||
@ -62,9 +95,11 @@ func init() {
|
||||
|
||||
rootCmd.AddCommand(setupCmd)
|
||||
rootCmd.AddCommand(runCmd)
|
||||
rootCmd.AddCommand(addCmd)
|
||||
|
||||
process.Bind(runCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
process.Bind(setupCmd, &setupCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir), cfgstruct.SetupMode())
|
||||
process.Bind(addCmd, &addCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
||||
}
|
||||
|
||||
func cmdSetup(cmd *cobra.Command, args []string) (err error) {
|
||||
@ -118,3 +153,124 @@ func cmdRun(cmd *cobra.Command, args []string) (err error) {
|
||||
closeError := peer.Close()
|
||||
return errs.Combine(runError, closeError)
|
||||
}
|
||||
|
||||
type nodeInfo struct {
|
||||
NodeID storj.NodeID `json:"id"`
|
||||
PublicAddress string `json:"publicAddress"`
|
||||
APISecret string `json:"apiSecret"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func cmdAdd(cmd *cobra.Command, args []string) (err error) {
|
||||
ctx, _ := process.Ctx(cmd)
|
||||
log := zap.L()
|
||||
|
||||
identity, err := addCfg.Identity.Load()
|
||||
if err != nil {
|
||||
return errs.New("failed to load identity: %+v", err)
|
||||
}
|
||||
|
||||
db, err := multinodedb.Open(ctx, log.Named("db"), addCfg.Database)
|
||||
if err != nil {
|
||||
return errs.New("error connecting to master database on multinode: %+v", err)
|
||||
}
|
||||
|
||||
tlsConfig := tlsopts.Config{
|
||||
UsePeerCAWhitelist: false,
|
||||
PeerIDVersions: "0",
|
||||
}
|
||||
|
||||
tlsOptions, err := tlsopts.NewOptions(identity, tlsConfig, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dialer := rpc.NewDefaultDialer(tlsOptions)
|
||||
|
||||
var nodeList []nodeInfo
|
||||
|
||||
hasRequiredFlags := addCfg.NodeID != "" && addCfg.APISecret != "" && addCfg.PublicAddress != ""
|
||||
|
||||
if len(args) == 0 && !hasRequiredFlags {
|
||||
return errs.New("--node-id, --api-secret and --public-address flags are required if no file is provided")
|
||||
}
|
||||
|
||||
if hasRequiredFlags {
|
||||
nodeID, err := storj.NodeIDFromString(addCfg.NodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nodeList = []nodeInfo{
|
||||
{
|
||||
NodeID: nodeID,
|
||||
PublicAddress: addCfg.PublicAddress,
|
||||
APISecret: addCfg.APISecret,
|
||||
Name: addCfg.Name,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
path := args[0]
|
||||
var nodesData []byte
|
||||
if path == "-" {
|
||||
stdin := cmd.InOrStdin()
|
||||
data, err := ioutil.ReadAll(stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nodesData = data
|
||||
} else {
|
||||
nodesData, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
nodeList, err = unmarshalJSONNodes(nodesData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, node := range nodeList {
|
||||
if _, err := db.Nodes().Get(ctx, node.NodeID); err == nil {
|
||||
return errs.New("Node with ID %s is already added to the multinode dashboard", node.NodeID)
|
||||
}
|
||||
|
||||
apiSecret, err := multinodeauth.SecretFromBase64(node.APISecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
service := nodes.NewService(log, dialer, db.Nodes())
|
||||
err = service.Add(ctx, node.NodeID, apiSecret[:], node.PublicAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalJSONNodes(nodesData []byte) ([]nodeInfo, error) {
|
||||
var nodes []nodeInfo
|
||||
nodesData = bytes.TrimLeft(nodesData, " \t\r\n")
|
||||
|
||||
switch {
|
||||
case len(nodesData) > 0 && nodesData[0] == '[': // data is json array
|
||||
err := json.Unmarshal(nodesData, &nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case len(nodesData) > 0 && nodesData[0] == '{': // data is json object
|
||||
var singleNode nodeInfo
|
||||
err := json.Unmarshal(nodesData, &singleNode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes = []nodeInfo{singleNode}
|
||||
default:
|
||||
return nil, errs.New("invalid JSON format")
|
||||
}
|
||||
|
||||
return nodes, nil
|
||||
}
|
||||
|
67
cmd/multinode/main_test.go
Normal file
67
cmd/multinode/main_test.go
Normal file
@ -0,0 +1,67 @@
|
||||
// Copyright (C) 2021 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"storj.io/common/storj"
|
||||
)
|
||||
|
||||
func Test_unmarshalJSONNodes(t *testing.T) {
|
||||
nodeID, err := storj.NodeIDFromString("1MJ7R1cqGrFnELPY3YKd62TBJ6vE8x9yPKPwUFHUx6G8oypezR")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("valid json object", func(t *testing.T) {
|
||||
nodesJSONData := `
|
||||
{
|
||||
"name": "Storagenode 1",
|
||||
"id":"1MJ7R1cqGrFnELPY3YKd62TBJ6vE8x9yPKPwUFHUx6G8oypezR",
|
||||
"publicAddress": "awn7k09ts6mxbgau.myfritz.net:13010",
|
||||
"apiSecret": "b_yeI0OBKBusBVN4_dHxpxlwdTyoFPwtEuHv9ACl9jI="
|
||||
}
|
||||
`
|
||||
expectedNodeInfo := []nodeInfo{
|
||||
{
|
||||
NodeID: nodeID,
|
||||
PublicAddress: "awn7k09ts6mxbgau.myfritz.net:13010",
|
||||
APISecret: "b_yeI0OBKBusBVN4_dHxpxlwdTyoFPwtEuHv9ACl9jI=",
|
||||
Name: "Storagenode 1",
|
||||
},
|
||||
}
|
||||
|
||||
got, err := unmarshalJSONNodes([]byte(nodesJSONData))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedNodeInfo, got)
|
||||
})
|
||||
|
||||
t.Run("valid json array", func(t *testing.T) {
|
||||
nodesJSONData := `
|
||||
[
|
||||
{
|
||||
"name": "Storagenode 1",
|
||||
"id":"1MJ7R1cqGrFnELPY3YKd62TBJ6vE8x9yPKPwUFHUx6G8oypezR",
|
||||
"publicAddress": "awn7k09ts6mxbgau.myfritz.net:13010",
|
||||
"apiSecret": "b_yeI0OBKBusBVN4_dHxpxlwdTyoFPwtEuHv9ACl9jI="
|
||||
}
|
||||
]
|
||||
`
|
||||
expectedNodeInfo := []nodeInfo{
|
||||
{
|
||||
NodeID: nodeID,
|
||||
PublicAddress: "awn7k09ts6mxbgau.myfritz.net:13010",
|
||||
APISecret: "b_yeI0OBKBusBVN4_dHxpxlwdTyoFPwtEuHv9ACl9jI=",
|
||||
Name: "Storagenode 1",
|
||||
},
|
||||
}
|
||||
|
||||
got, err := unmarshalJSONNodes([]byte(nodesJSONData))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, expectedNodeInfo, got)
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user