[#114] Add remove-node IR control command

Signed-off-by: Alejandro Lopez <a.lopez@yadro.com>
This commit is contained in:
Alejandro Lopez 2023-05-03 16:19:46 +03:00 committed by Evgenii Stratonikov
parent ea10abb42a
commit 90799497d3
10 changed files with 157 additions and 0 deletions

View file

@ -10,6 +10,8 @@ var irCmd = &cobra.Command{
func initControlIRCmd() { func initControlIRCmd() {
irCmd.AddCommand(tickEpochCmd) irCmd.AddCommand(tickEpochCmd)
irCmd.AddCommand(removeNodeCmd)
initControlIRTickEpochCmd() initControlIRTickEpochCmd()
initControlIRRemoveNodeCmd()
} }

View file

@ -0,0 +1,58 @@
package control
import (
"encoding/hex"
"errors"
rawclient "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/rpc/client"
"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
ircontrol "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir"
ircontrolsrv "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir/server"
"github.com/spf13/cobra"
)
var removeNodeCmd = &cobra.Command{
Use: "remove-node",
Short: "Forces a node removal from netmap",
Long: "Forces a node removal from netmap via a notary request. It should be executed on other IR nodes as well.",
Run: removeNode,
}
func initControlIRRemoveNodeCmd() {
initControlFlags(removeNodeCmd)
flags := removeNodeCmd.Flags()
flags.String("node", "", "Node public key as a hex string")
_ = removeNodeCmd.MarkFlagRequired("node")
}
func removeNode(cmd *cobra.Command, _ []string) {
pk := key.Get(cmd)
c := getClient(cmd, pk)
nodeKeyStr, _ := cmd.Flags().GetString("node")
if len(nodeKeyStr) == 0 {
commonCmd.ExitOnErr(cmd, "parsing node public key: ", errors.New("key cannot be empty"))
}
nodeKey, err := hex.DecodeString(nodeKeyStr)
commonCmd.ExitOnErr(cmd, "can't decode node public key: %w", err)
req := new(ircontrol.RemoveNodeRequest)
req.SetBody(&ircontrol.RemoveNodeRequest_Body{
Key: nodeKey,
})
commonCmd.ExitOnErr(cmd, "could not sign request: %w", ircontrolsrv.SignMessage(pk, req))
var resp *ircontrol.RemoveNodeResponse
err = c.ExecRaw(func(client *rawclient.Client) error {
resp, err = ircontrol.RemoveNode(client, req)
return err
})
commonCmd.ExitOnErr(cmd, "rpc error: %w", err)
verifyResponse(cmd, resp.GetSignature(), resp.GetBody())
cmd.Println("Node removed")
}

View file

@ -41,3 +41,19 @@ func (c *Client) AddPeer(p AddPeerPrm) error {
} }
return nil return nil
} }
// ForceRemovePeer marks the given peer as offline via a notary control transaction.
func (c *Client) ForceRemovePeer(nodeInfo netmap.NodeInfo) error {
if !c.client.WithNotary() {
return fmt.Errorf("peer can be forcefully removed only in notary environment")
}
prm := UpdatePeerPrm{}
prm.SetKey(nodeInfo.PublicKey())
prm.SetControlTX(true)
if err := c.UpdatePeerState(prm); err != nil {
return fmt.Errorf("updating peer state: %v", err)
}
return nil
}

View file

@ -11,6 +11,7 @@ const serviceName = "ircontrol.ControlService"
const ( const (
rpcHealthCheck = "HealthCheck" rpcHealthCheck = "HealthCheck"
rpcTickEpoch = "TickEpoch" rpcTickEpoch = "TickEpoch"
rpcRemoveNode = "RemoveNode"
) )
// HealthCheck executes ControlService.HealthCheck RPC. // HealthCheck executes ControlService.HealthCheck RPC.
@ -31,6 +32,14 @@ func TickEpoch(
return sendUnary[TickEpochRequest, TickEpochResponse](cli, rpcTickEpoch, req, opts...) return sendUnary[TickEpochRequest, TickEpochResponse](cli, rpcTickEpoch, req, opts...)
} }
func RemoveNode(
cli *client.Client,
req *RemoveNodeRequest,
opts ...client.CallOption,
) (*RemoveNodeResponse, error) {
return sendUnary[RemoveNodeRequest, RemoveNodeResponse](cli, rpcRemoveNode, req, opts...)
}
func sendUnary[I, O grpc.Message](cli *client.Client, rpcName string, req *I, opts ...client.CallOption) (*O, error) { func sendUnary[I, O grpc.Message](cli *client.Client, rpcName string, req *I, opts ...client.CallOption) (*O, error) {
var resp O var resp O
wResp := &responseWrapper[*O]{ wResp := &responseWrapper[*O]{

View file

@ -1,9 +1,11 @@
package control package control
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/morph/client/netmap"
control "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir" control "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/control/ir"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
@ -57,3 +59,43 @@ func (s *Server) TickEpoch(_ context.Context, req *control.TickEpochRequest) (*c
return resp, nil return resp, nil
} }
// RemoveNode forces a node removal.
//
// If request is not signed with a key from white list, permission error returns.
func (s *Server) RemoveNode(_ context.Context, req *control.RemoveNodeRequest) (*control.RemoveNodeResponse, error) {
if err := s.isValidRequest(req); err != nil {
return nil, status.Error(codes.PermissionDenied, err.Error())
}
resp := new(control.RemoveNodeResponse)
resp.SetBody(new(control.RemoveNodeResponse_Body))
nm, err := s.netmapClient.NetMap()
if err != nil {
return nil, fmt.Errorf("getting netmap: %w", err)
}
var nodeInfo netmap.NodeInfo
for _, info := range nm.Nodes() {
if bytes.Equal(info.PublicKey(), req.GetBody().GetKey()) {
nodeInfo = info
break
}
}
if len(nodeInfo.PublicKey()) == 0 {
return nil, status.Error(codes.NotFound, "no such node")
}
if nodeInfo.IsOffline() {
return nil, status.Error(codes.FailedPrecondition, "node is already offline")
}
if err := s.netmapClient.ForceRemovePeer(nodeInfo); err != nil {
return nil, fmt.Errorf("forcing node removal: %w", err)
}
if err := SignMessage(&s.prm.key.PrivateKey, resp); err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return resp, nil
}

View file

@ -32,3 +32,15 @@ func (x *TickEpochResponse) SetBody(v *TickEpochResponse_Body) {
x.Body = v x.Body = v
} }
} }
func (x *RemoveNodeRequest) SetBody(v *RemoveNodeRequest_Body) {
if x != nil {
x.Body = v
}
}
func (x *RemoveNodeResponse) SetBody(v *RemoveNodeResponse_Body) {
if x != nil {
x.Body = v
}
}

Binary file not shown.

View file

@ -12,6 +12,8 @@ service ControlService {
rpc HealthCheck (HealthCheckRequest) returns (HealthCheckResponse); rpc HealthCheck (HealthCheckRequest) returns (HealthCheckResponse);
// Forces a new epoch to be signaled by the IR node with high probability. // Forces a new epoch to be signaled by the IR node with high probability.
rpc TickEpoch (TickEpochRequest) returns (TickEpochResponse); rpc TickEpoch (TickEpochRequest) returns (TickEpochResponse);
// Forces a node removal to be signaled by the IR node with high probability.
rpc RemoveNode (RemoveNodeRequest) returns (RemoveNodeResponse);
} }
// Health check request. // Health check request.
@ -57,3 +59,19 @@ message TickEpochResponse {
Body body = 1; Body body = 1;
Signature signature = 2; Signature signature = 2;
} }
message RemoveNodeRequest {
message Body{
bytes key = 1;
}
Body body = 1;
Signature signature = 2;
}
message RemoveNodeResponse {
message Body{}
Body body = 1;
Signature signature = 2;
}

Binary file not shown.

Binary file not shown.