2020-11-10 15:35:43 +00:00
|
|
|
// Copyright (C) 2020 Storj Labs, Inc.
|
|
|
|
// See LICENSE for copying information.
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2021-11-17 12:58:57 +00:00
|
|
|
"bytes"
|
2022-08-26 02:18:17 +01:00
|
|
|
"context"
|
2021-11-17 12:58:57 +00:00
|
|
|
"encoding/json"
|
2020-11-10 15:35:43 +00:00
|
|
|
"fmt"
|
2022-08-22 23:54:16 +01:00
|
|
|
"io"
|
2020-11-10 15:35:43 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/zeebo/errs"
|
|
|
|
"go.uber.org/zap"
|
2022-08-22 23:54:16 +01:00
|
|
|
"golang.org/x/text/encoding/unicode"
|
|
|
|
"golang.org/x/text/transform"
|
2020-11-10 15:35:43 +00:00
|
|
|
|
|
|
|
"storj.io/common/fpath"
|
2022-08-26 02:18:17 +01:00
|
|
|
"storj.io/common/identity"
|
2021-11-17 12:58:57 +00:00
|
|
|
"storj.io/common/peertls/tlsopts"
|
|
|
|
"storj.io/common/rpc"
|
|
|
|
"storj.io/common/storj"
|
2020-11-10 15:35:43 +00:00
|
|
|
"storj.io/private/cfgstruct"
|
|
|
|
"storj.io/private/process"
|
|
|
|
"storj.io/storj/multinode"
|
|
|
|
"storj.io/storj/multinode/multinodedb"
|
2021-11-17 12:58:57 +00:00
|
|
|
"storj.io/storj/multinode/nodes"
|
|
|
|
"storj.io/storj/private/multinodeauth"
|
2020-11-10 15:35:43 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// Config defines multinode configuration.
|
|
|
|
type Config struct {
|
2021-02-04 14:38:45 +00:00
|
|
|
Database string `help:"multinode database connection string" default:"sqlite3://file:$CONFDIR/master.db"`
|
2020-11-10 15:35:43 +00:00
|
|
|
|
|
|
|
multinode.Config
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
rootCmd = &cobra.Command{
|
|
|
|
Use: "multinode",
|
|
|
|
Short: "Multinode Dashboard",
|
|
|
|
}
|
|
|
|
runCmd = &cobra.Command{
|
|
|
|
Use: "run",
|
|
|
|
Short: "Run the multinode dashboard",
|
|
|
|
RunE: cmdRun,
|
|
|
|
}
|
|
|
|
setupCmd = &cobra.Command{
|
|
|
|
Use: "setup",
|
|
|
|
Short: "Create config files",
|
|
|
|
RunE: cmdSetup,
|
|
|
|
Annotations: map[string]string{"type": "setup"},
|
|
|
|
}
|
|
|
|
|
2021-11-17 12:58:57 +00:00
|
|
|
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
|
|
|
|
}
|
2020-11-10 15:35:43 +00:00
|
|
|
confDir string
|
|
|
|
identityDir string
|
|
|
|
)
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
process.ExecCustomDebug(rootCmd)
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
defaultConfDir := fpath.ApplicationDir("storj", "multinode")
|
|
|
|
cfgstruct.SetupFlag(zap.L(), rootCmd, &confDir, "config-dir", defaultConfDir, "main directory for multinode configuration")
|
2022-08-26 02:18:17 +01:00
|
|
|
cfgstruct.SetupFlag(zap.L(), rootCmd, &identityDir, "identity-dir", "", "main directory for multinode identity credentials")
|
2020-11-10 15:35:43 +00:00
|
|
|
defaults := cfgstruct.DefaultsFlag(rootCmd)
|
|
|
|
|
2022-08-26 02:18:17 +01:00
|
|
|
// Ignoring errors since MarkDeprecated only errors if the flag
|
|
|
|
// doesn't exist or no deprecated message is provided.
|
|
|
|
// and MarkHidden only errors if the flag doesn't exist.
|
|
|
|
_ = rootCmd.PersistentFlags().MarkDeprecated("identity-dir", "multinode no longer requires an identity key")
|
|
|
|
_ = rootCmd.PersistentFlags().MarkHidden("identity-dir")
|
|
|
|
|
2020-11-10 15:35:43 +00:00
|
|
|
rootCmd.AddCommand(setupCmd)
|
2020-11-12 17:21:56 +00:00
|
|
|
rootCmd.AddCommand(runCmd)
|
2021-11-17 12:58:57 +00:00
|
|
|
rootCmd.AddCommand(addCmd)
|
2020-11-10 15:35:43 +00:00
|
|
|
|
|
|
|
process.Bind(runCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
|
|
|
process.Bind(setupCmd, &setupCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir), cfgstruct.SetupMode())
|
2021-11-17 12:58:57 +00:00
|
|
|
process.Bind(addCmd, &addCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
|
2020-11-10 15:35:43 +00:00
|
|
|
}
|
|
|
|
|
2020-11-12 17:21:56 +00:00
|
|
|
func cmdSetup(cmd *cobra.Command, args []string) (err error) {
|
|
|
|
setupDir, err := filepath.Abs(confDir)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
valid, _ := fpath.IsValidSetupDir(setupDir)
|
|
|
|
if !valid {
|
|
|
|
return fmt.Errorf("multinode configuration already exists (%v)", setupDir)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = os.MkdirAll(setupDir, 0700)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return process.SaveConfig(cmd, filepath.Join(setupDir, "config.yaml"))
|
|
|
|
}
|
|
|
|
|
2020-11-10 15:35:43 +00:00
|
|
|
func cmdRun(cmd *cobra.Command, args []string) (err error) {
|
|
|
|
ctx, _ := process.Ctx(cmd)
|
|
|
|
log := zap.L()
|
|
|
|
|
|
|
|
runCfg.Debug.Address = *process.DebugAddrFlag
|
|
|
|
|
2022-08-26 02:18:17 +01:00
|
|
|
identity, err := getIdentity(ctx, &runCfg)
|
2020-11-10 15:35:43 +00:00
|
|
|
if err != nil {
|
2020-11-12 17:21:56 +00:00
|
|
|
log.Error("failed to load identity", zap.Error(err))
|
|
|
|
return errs.New("failed to load identity: %+v", err)
|
2020-11-10 15:35:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
db, err := multinodedb.Open(ctx, log.Named("db"), runCfg.Database)
|
|
|
|
if err != nil {
|
2020-11-12 17:21:56 +00:00
|
|
|
return errs.New("error connecting to master database on multinode: %+v", err)
|
2020-11-10 15:35:43 +00:00
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
err = errs.Combine(err, db.Close())
|
|
|
|
}()
|
2021-04-28 02:30:01 +01:00
|
|
|
if err := db.MigrateToLatest(ctx); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-11-10 15:35:43 +00:00
|
|
|
|
|
|
|
peer, err := multinode.New(log, identity, runCfg.Config, db)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
runError := peer.Run(ctx)
|
|
|
|
closeError := peer.Close()
|
|
|
|
return errs.Combine(runError, closeError)
|
|
|
|
}
|
2021-11-17 12:58:57 +00:00
|
|
|
|
|
|
|
func cmdAdd(cmd *cobra.Command, args []string) (err error) {
|
|
|
|
ctx, _ := process.Ctx(cmd)
|
|
|
|
log := zap.L()
|
|
|
|
|
2022-08-26 02:18:17 +01:00
|
|
|
identity, err := getIdentity(ctx, &addCfg.Config)
|
2021-11-17 12:58:57 +00:00
|
|
|
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)
|
|
|
|
|
2022-08-12 20:50:30 +01:00
|
|
|
var nodeList []nodes.Node
|
2021-11-17 12:58:57 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2022-08-12 20:50:30 +01:00
|
|
|
apiSecret, err := multinodeauth.SecretFromBase64(addCfg.APISecret)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
nodeList = []nodes.Node{
|
2021-11-17 12:58:57 +00:00
|
|
|
{
|
2022-08-12 20:50:30 +01:00
|
|
|
ID: nodeID,
|
2021-11-17 12:58:57 +00:00
|
|
|
PublicAddress: addCfg.PublicAddress,
|
2022-08-12 20:50:30 +01:00
|
|
|
APISecret: apiSecret,
|
2021-11-17 12:58:57 +00:00
|
|
|
Name: addCfg.Name,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
path := args[0]
|
2022-08-12 20:50:30 +01:00
|
|
|
var nodesJSONData []byte
|
2021-11-17 12:58:57 +00:00
|
|
|
if path == "-" {
|
|
|
|
stdin := cmd.InOrStdin()
|
2022-08-22 23:54:16 +01:00
|
|
|
data, err := io.ReadAll(stdin)
|
2021-11-17 12:58:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-08-12 20:50:30 +01:00
|
|
|
nodesJSONData = data
|
2021-11-17 12:58:57 +00:00
|
|
|
} else {
|
2022-08-12 20:50:30 +01:00
|
|
|
nodesJSONData, err = os.ReadFile(path)
|
2021-11-17 12:58:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-12 20:50:30 +01:00
|
|
|
nodeList, err = unmarshalJSONNodes(nodesJSONData)
|
2021-11-17 12:58:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, node := range nodeList {
|
2022-08-12 20:50:30 +01:00
|
|
|
if _, err := db.Nodes().Get(ctx, node.ID); err == nil {
|
|
|
|
return errs.New("Node with ID %s is already added to the multinode dashboard", node.ID)
|
2021-11-17 12:58:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
service := nodes.NewService(log, dialer, db.Nodes())
|
2022-08-12 20:50:30 +01:00
|
|
|
err = service.Add(ctx, node)
|
2021-11-17 12:58:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-08-22 23:54:16 +01:00
|
|
|
// decodeUTF16or8 decodes the b as UTF-16 if the special byte order mark is present.
|
|
|
|
func decodeUTF16or8(b []byte) ([]byte, error) {
|
|
|
|
r := bytes.NewReader(b)
|
|
|
|
// fallback to r if no BOM sequence is located in the source text.
|
|
|
|
t := unicode.BOMOverride(transform.Nop)
|
|
|
|
return io.ReadAll(transform.NewReader(r, t))
|
|
|
|
}
|
|
|
|
|
2022-08-12 20:50:30 +01:00
|
|
|
func unmarshalJSONNodes(nodesJSONData []byte) ([]nodes.Node, error) {
|
|
|
|
var nodesInfo []nodes.Node
|
2022-08-22 23:54:16 +01:00
|
|
|
var err error
|
|
|
|
|
|
|
|
nodesJSONData, err = decodeUTF16or8(nodesJSONData)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-08-12 20:50:30 +01:00
|
|
|
nodesJSONData = bytes.TrimLeft(nodesJSONData, " \t\r\n")
|
2021-11-17 12:58:57 +00:00
|
|
|
|
|
|
|
switch {
|
2022-08-12 20:50:30 +01:00
|
|
|
case len(nodesJSONData) > 0 && nodesJSONData[0] == '[': // data is json array
|
|
|
|
err := json.Unmarshal(nodesJSONData, &nodesInfo)
|
2021-11-17 12:58:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-08-12 20:50:30 +01:00
|
|
|
case len(nodesJSONData) > 0 && nodesJSONData[0] == '{': // data is json object
|
|
|
|
var singleNode nodes.Node
|
|
|
|
err := json.Unmarshal(nodesJSONData, &singleNode)
|
2021-11-17 12:58:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-08-12 20:50:30 +01:00
|
|
|
nodesInfo = []nodes.Node{singleNode}
|
2021-11-17 12:58:57 +00:00
|
|
|
default:
|
|
|
|
return nil, errs.New("invalid JSON format")
|
|
|
|
}
|
|
|
|
|
2022-08-12 20:50:30 +01:00
|
|
|
return nodesInfo, nil
|
2021-11-17 12:58:57 +00:00
|
|
|
}
|
2022-08-26 02:18:17 +01:00
|
|
|
|
|
|
|
func getIdentity(ctx context.Context, cfg *Config) (*identity.FullIdentity, error) {
|
|
|
|
// for backwards compatibility reasons, check if an identity was provided.
|
|
|
|
if cfgstruct.FindIdentityDirParam() != "" {
|
|
|
|
ident, err := cfg.Identity.Load()
|
|
|
|
if err == nil {
|
|
|
|
return ident, nil
|
|
|
|
}
|
|
|
|
zap.L().Error("failed to load identity.", zap.Error(err))
|
|
|
|
zap.L().Info("generating new identity.")
|
|
|
|
}
|
|
|
|
// generate new identity
|
|
|
|
return identity.NewFullIdentity(ctx, identity.NewCAOptions{
|
|
|
|
Difficulty: 0,
|
|
|
|
Concurrency: 1,
|
|
|
|
})
|
|
|
|
}
|