forked from TrueCloudLab/frostfs-node
325 lines
8.9 KiB
Go
325 lines
8.9 KiB
Go
|
package state
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"crypto/ecdsa"
|
||
|
"encoding/hex"
|
||
|
"strconv"
|
||
|
|
||
|
"github.com/nspcc-dev/neofs-api-go/bootstrap"
|
||
|
"github.com/nspcc-dev/neofs-api-go/refs"
|
||
|
"github.com/nspcc-dev/neofs-api-go/service"
|
||
|
"github.com/nspcc-dev/neofs-api-go/state"
|
||
|
crypto "github.com/nspcc-dev/neofs-crypto"
|
||
|
"github.com/nspcc-dev/neofs-node/internal"
|
||
|
"github.com/nspcc-dev/neofs-node/lib/core"
|
||
|
"github.com/nspcc-dev/neofs-node/lib/implementations"
|
||
|
"github.com/nspcc-dev/neofs-node/modules/grpc"
|
||
|
"github.com/pkg/errors"
|
||
|
"github.com/prometheus/client_golang/prometheus"
|
||
|
"github.com/spf13/viper"
|
||
|
"go.uber.org/zap"
|
||
|
"google.golang.org/grpc/codes"
|
||
|
"google.golang.org/grpc/status"
|
||
|
)
|
||
|
|
||
|
type (
|
||
|
// Service is an interface of the server of State service.
|
||
|
Service interface {
|
||
|
state.StatusServer
|
||
|
grpc.Service
|
||
|
Healthy() error
|
||
|
}
|
||
|
|
||
|
// HealthChecker is an interface of node healthiness checking tool.
|
||
|
HealthChecker interface {
|
||
|
Name() string
|
||
|
Healthy() bool
|
||
|
}
|
||
|
|
||
|
// Stater is an interface of the node's network state storage with read access.
|
||
|
Stater interface {
|
||
|
NetworkState() *bootstrap.SpreadMap
|
||
|
}
|
||
|
|
||
|
// Params groups the parameters of State service server's constructor.
|
||
|
Params struct {
|
||
|
Stater Stater
|
||
|
|
||
|
Logger *zap.Logger
|
||
|
|
||
|
Viper *viper.Viper
|
||
|
|
||
|
Checkers []HealthChecker
|
||
|
|
||
|
PrivateKey *ecdsa.PrivateKey
|
||
|
|
||
|
MorphNetmapContract *implementations.MorphNetmapContract
|
||
|
}
|
||
|
|
||
|
stateService struct {
|
||
|
state Stater
|
||
|
config *viper.Viper
|
||
|
checkers []HealthChecker
|
||
|
private *ecdsa.PrivateKey
|
||
|
owners map[refs.OwnerID]struct{}
|
||
|
|
||
|
stateUpdater *implementations.MorphNetmapContract
|
||
|
}
|
||
|
|
||
|
// HealthRequest is a type alias of
|
||
|
// HealthRequest from state package of neofs-api-go.
|
||
|
HealthRequest = state.HealthRequest
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
errEmptyViper = internal.Error("empty config")
|
||
|
errEmptyLogger = internal.Error("empty logger")
|
||
|
errEmptyStater = internal.Error("empty stater")
|
||
|
errUnknownChangeState = internal.Error("received unknown state")
|
||
|
)
|
||
|
|
||
|
const msgMissingRequestInitiator = "missing request initiator"
|
||
|
|
||
|
var requestVerifyFunc = core.VerifyRequestWithSignatures
|
||
|
|
||
|
// New is an State service server's constructor.
|
||
|
func New(p Params) (Service, error) {
|
||
|
switch {
|
||
|
case p.Logger == nil:
|
||
|
return nil, errEmptyLogger
|
||
|
case p.Viper == nil:
|
||
|
return nil, errEmptyViper
|
||
|
case p.Stater == nil:
|
||
|
return nil, errEmptyStater
|
||
|
case p.PrivateKey == nil:
|
||
|
return nil, crypto.ErrEmptyPrivateKey
|
||
|
}
|
||
|
|
||
|
svc := &stateService{
|
||
|
config: p.Viper,
|
||
|
state: p.Stater,
|
||
|
private: p.PrivateKey,
|
||
|
owners: fetchOwners(p.Logger, p.Viper),
|
||
|
checkers: make([]HealthChecker, 0, len(p.Checkers)),
|
||
|
|
||
|
stateUpdater: p.MorphNetmapContract,
|
||
|
}
|
||
|
|
||
|
for i, checker := range p.Checkers {
|
||
|
if checker == nil {
|
||
|
p.Logger.Debug("ignore empty checker",
|
||
|
zap.Int("index", i))
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
p.Logger.Info("register health-checker",
|
||
|
zap.String("name", checker.Name()))
|
||
|
|
||
|
svc.checkers = append(svc.checkers, checker)
|
||
|
}
|
||
|
|
||
|
return svc, nil
|
||
|
}
|
||
|
|
||
|
func fetchOwners(l *zap.Logger, v *viper.Viper) map[refs.OwnerID]struct{} {
|
||
|
// if config.yml used:
|
||
|
items := v.GetStringSlice("node.rpc.owners")
|
||
|
|
||
|
for i := 0; ; i++ {
|
||
|
item := v.GetString("node.rpc.owners." + strconv.Itoa(i))
|
||
|
|
||
|
if item == "" {
|
||
|
l.Info("stat: skip empty owner", zap.Int("idx", i))
|
||
|
break
|
||
|
}
|
||
|
|
||
|
items = append(items, item)
|
||
|
}
|
||
|
|
||
|
result := make(map[refs.OwnerID]struct{}, len(items))
|
||
|
|
||
|
for i := range items {
|
||
|
var owner refs.OwnerID
|
||
|
|
||
|
if data, err := hex.DecodeString(items[i]); err != nil {
|
||
|
l.Warn("stat: skip wrong hex data",
|
||
|
zap.Int("idx", i),
|
||
|
zap.String("key", items[i]),
|
||
|
zap.Error(err))
|
||
|
|
||
|
continue
|
||
|
} else if key := crypto.UnmarshalPublicKey(data); key == nil {
|
||
|
l.Warn("stat: skip wrong key",
|
||
|
zap.Int("idx", i),
|
||
|
zap.String("key", items[i]))
|
||
|
continue
|
||
|
} else if owner, err = refs.NewOwnerID(key); err != nil {
|
||
|
l.Warn("stat: skip wrong key",
|
||
|
zap.Int("idx", i),
|
||
|
zap.String("key", items[i]),
|
||
|
zap.Error(err))
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
result[owner] = struct{}{}
|
||
|
|
||
|
l.Info("rpc owner added", zap.Stringer("owner", owner))
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
func nonForwarding(ttl uint32) error {
|
||
|
if ttl != service.NonForwardingTTL {
|
||
|
return status.Error(codes.InvalidArgument, service.ErrInvalidTTL.Error())
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func requestInitiator(req service.SignKeyPairSource) *ecdsa.PublicKey {
|
||
|
if signKeys := req.GetSignKeyPairs(); len(signKeys) > 0 {
|
||
|
return signKeys[0].GetPublicKey()
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// ChangeState allows to change current node state of node.
|
||
|
// To permit access, used server config options.
|
||
|
// The request should be signed.
|
||
|
func (s *stateService) ChangeState(ctx context.Context, in *state.ChangeStateRequest) (*state.ChangeStateResponse, error) {
|
||
|
// verify request structure
|
||
|
if err := requestVerifyFunc(in); err != nil {
|
||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||
|
}
|
||
|
|
||
|
// verify change state permission
|
||
|
if key := requestInitiator(in); key == nil {
|
||
|
return nil, status.Error(codes.InvalidArgument, msgMissingRequestInitiator)
|
||
|
} else if owner, err := refs.NewOwnerID(key); err != nil {
|
||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||
|
} else if _, ok := s.owners[owner]; !ok {
|
||
|
return nil, status.Error(codes.PermissionDenied, service.ErrWrongOwner.Error())
|
||
|
}
|
||
|
|
||
|
// convert State field to NodeState
|
||
|
if in.GetState() != state.ChangeStateRequest_Offline {
|
||
|
return nil, status.Error(codes.InvalidArgument, errUnknownChangeState.Error())
|
||
|
}
|
||
|
|
||
|
// set update state parameters
|
||
|
p := implementations.UpdateStateParams{}
|
||
|
p.SetState(implementations.StateOffline)
|
||
|
p.SetKey(
|
||
|
crypto.MarshalPublicKey(&s.private.PublicKey),
|
||
|
)
|
||
|
|
||
|
if err := s.stateUpdater.UpdateState(p); err != nil {
|
||
|
return nil, status.Error(codes.Aborted, err.Error())
|
||
|
}
|
||
|
|
||
|
return new(state.ChangeStateResponse), nil
|
||
|
}
|
||
|
|
||
|
// DumpConfig request allows dumping settings for the current node.
|
||
|
// To permit access, used server config options.
|
||
|
// The request should be signed.
|
||
|
func (s *stateService) DumpConfig(_ context.Context, req *state.DumpRequest) (*state.DumpResponse, error) {
|
||
|
if err := service.ProcessRequestTTL(req, nonForwarding); err != nil {
|
||
|
return nil, err
|
||
|
} else if err = requestVerifyFunc(req); err != nil {
|
||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||
|
} else if key := requestInitiator(req); key == nil {
|
||
|
return nil, status.Error(codes.InvalidArgument, msgMissingRequestInitiator)
|
||
|
} else if owner, err := refs.NewOwnerID(key); err != nil {
|
||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||
|
} else if _, ok := s.owners[owner]; !ok {
|
||
|
return nil, status.Error(codes.PermissionDenied, service.ErrWrongOwner.Error())
|
||
|
}
|
||
|
|
||
|
return state.EncodeConfig(s.config)
|
||
|
}
|
||
|
|
||
|
// Netmap returns SpreadMap from Stater (IRState / Place-component).
|
||
|
func (s *stateService) Netmap(_ context.Context, req *state.NetmapRequest) (*bootstrap.SpreadMap, error) {
|
||
|
if err := service.ProcessRequestTTL(req); err != nil {
|
||
|
return nil, err
|
||
|
} else if err = requestVerifyFunc(req); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if s.state != nil {
|
||
|
return s.state.NetworkState(), nil
|
||
|
}
|
||
|
|
||
|
return nil, status.New(codes.Unavailable, "service unavailable").Err()
|
||
|
}
|
||
|
|
||
|
func (s *stateService) healthy() error {
|
||
|
for _, svc := range s.checkers {
|
||
|
if !svc.Healthy() {
|
||
|
return errors.Errorf("service(%s) unhealthy", svc.Name())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Healthy returns error as status of service, if nil service healthy.
|
||
|
func (s *stateService) Healthy() error { return s.healthy() }
|
||
|
|
||
|
// Check that all checkers is healthy.
|
||
|
func (s *stateService) HealthCheck(_ context.Context, req *HealthRequest) (*state.HealthResponse, error) {
|
||
|
if err := service.ProcessRequestTTL(req); err != nil {
|
||
|
return nil, err
|
||
|
} else if err = requestVerifyFunc(req); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
err = s.healthy()
|
||
|
resp = &state.HealthResponse{Healthy: true, Status: "OK"}
|
||
|
)
|
||
|
|
||
|
if err != nil {
|
||
|
resp.Healthy = false
|
||
|
resp.Status = err.Error()
|
||
|
}
|
||
|
|
||
|
return resp, nil
|
||
|
}
|
||
|
|
||
|
func (*stateService) Metrics(_ context.Context, req *state.MetricsRequest) (*state.MetricsResponse, error) {
|
||
|
if err := service.ProcessRequestTTL(req); err != nil {
|
||
|
return nil, err
|
||
|
} else if err = requestVerifyFunc(req); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return state.EncodeMetrics(prometheus.DefaultGatherer)
|
||
|
}
|
||
|
|
||
|
func (s *stateService) DumpVars(_ context.Context, req *state.DumpVarsRequest) (*state.DumpVarsResponse, error) {
|
||
|
if err := service.ProcessRequestTTL(req, nonForwarding); err != nil {
|
||
|
return nil, err
|
||
|
} else if err = requestVerifyFunc(req); err != nil {
|
||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||
|
} else if key := requestInitiator(req); key == nil {
|
||
|
return nil, status.Error(codes.InvalidArgument, msgMissingRequestInitiator)
|
||
|
} else if owner, err := refs.NewOwnerID(key); err != nil {
|
||
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||
|
} else if _, ok := s.owners[owner]; !ok {
|
||
|
return nil, status.Error(codes.PermissionDenied, service.ErrWrongOwner.Error())
|
||
|
}
|
||
|
|
||
|
return state.EncodeVariables(), nil
|
||
|
}
|
||
|
|
||
|
// Name of the service.
|
||
|
func (*stateService) Name() string { return "StatusService" }
|
||
|
|
||
|
// Register service on gRPC server.
|
||
|
func (s *stateService) Register(g *grpc.Server) { state.RegisterStatusServer(g, s) }
|