frostfs-node/services/public/state/service.go

325 lines
8.9 KiB
Go
Raw Normal View History

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) }