github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/ruby/agent.go (about)

     1  // Copyright (c) 2020-2021, R.I. Pienaar and the Choria Project contributors
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package ruby
     6  
     7  import (
     8  	"context"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"os/exec"
    14  	"regexp"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/choria-io/go-choria/config"
    19  	"github.com/choria-io/go-choria/inter"
    20  	"github.com/choria-io/go-choria/internal/util"
    21  	"github.com/choria-io/go-choria/providers/agent/mcorpc"
    22  	"github.com/choria-io/go-choria/providers/agent/mcorpc/ddl/agent"
    23  	"github.com/choria-io/go-choria/server"
    24  )
    25  
    26  const (
    27  	// if ruby agents should be enabled by default
    28  	activationDefault = true
    29  )
    30  
    31  // ShimRequest is the request being published to the shim runner
    32  type ShimRequest struct {
    33  	Agent      string           `json:"agent"`
    34  	Action     string           `json:"action"`
    35  	RequestID  string           `json:"requestid"`
    36  	SenderID   string           `json:"senderid"`
    37  	CallerID   string           `json:"callerid"`
    38  	Collective string           `json:"collective"`
    39  	TTL        int              `json:"ttl"`
    40  	Time       int64            `json:"msgtime"`
    41  	Body       *ShimRequestBody `json:"body"`
    42  }
    43  
    44  // ShimRequestBody is the body passed to the
    45  type ShimRequestBody struct {
    46  	Agent  string          `json:"agent"`
    47  	Action string          `json:"action"`
    48  	Data   json.RawMessage `json:"data"`
    49  	Caller string          `json:"caller"`
    50  }
    51  
    52  // NewRubyAgent creates a shim agent that calls to a old mcollective agent implemented in ruby
    53  func NewRubyAgent(ddl *agent.DDL, mgr server.AgentManager) (*mcorpc.Agent, error) {
    54  	agent := mcorpc.New(ddl.Metadata.Name, ddl.Metadata, mgr.Choria(), mgr.Logger())
    55  	agent.SetActivationChecker(activationCheck(ddl, mgr))
    56  
    57  	agent.Log.Debugf("Registering proxy actions for Ruby agent %s: %s", ddl.Metadata.Name, strings.Join(ddl.ActionNames(), ", "))
    58  
    59  	for _, action := range ddl.ActionNames() {
    60  		actint, err := ddl.ActionInterface(action)
    61  		if err != nil {
    62  			return nil, err
    63  		}
    64  
    65  		agent.MustRegisterAction(actint.Name, rubyAction)
    66  	}
    67  
    68  	return agent, nil
    69  }
    70  
    71  // checks if the plugin.agent.activate_agent is trueish
    72  func configActivationCheck(agent string, cfg *config.Config, dflt bool) bool {
    73  	opts := "plugin." + agent + ".activate_agent"
    74  	should := dflt
    75  
    76  	if cfg.HasOption(opts) {
    77  		val := cfg.Option(opts, "unknown")
    78  		if val != "unknown" {
    79  			should, _ = strToBool(val)
    80  		}
    81  	}
    82  
    83  	return should
    84  }
    85  
    86  func activationCheck(ddl *agent.DDL, mgr server.AgentManager) mcorpc.ActivationChecker {
    87  	cfg := mgr.Choria().Configuration()
    88  	should := configActivationCheck(ddl.Metadata.Name, cfg, activationDefault)
    89  
    90  	return func() bool { return should }
    91  }
    92  
    93  func rubyAction(ctx context.Context, req *mcorpc.Request, reply *mcorpc.Reply, agent *mcorpc.Agent, conn inter.ConnectorInfo) {
    94  	action := fmt.Sprintf("%s#%s", req.Agent, req.Action)
    95  	shim := agent.Config.Choria.RubyAgentShim
    96  	shimcfg := agent.Config.Choria.RubyAgentConfig
    97  
    98  	agent.Log.Debugf("Attempting to call Ruby agent %s with a timeout %d", action, agent.Metadata().Timeout)
    99  
   100  	if shim == "" {
   101  		abortAction(fmt.Sprintf("Cannot call Ruby action %s: Ruby compatibility shim was not configured", action), agent, reply)
   102  		return
   103  	}
   104  
   105  	if shimcfg == "" {
   106  		abortAction(fmt.Sprintf("Cannot call Ruby action %s: Ruby compatibility shim configuration file not configured", action), agent, reply)
   107  		return
   108  	}
   109  
   110  	if !util.FileExist(shim) {
   111  		abortAction(fmt.Sprintf("Cannot call Ruby action %s: Ruby compatibility shim was not found in %s", action, shim), agent, reply)
   112  		return
   113  	}
   114  
   115  	if !util.FileExist(shimcfg) {
   116  		abortAction(fmt.Sprintf("Cannot call Ruby action %s: Ruby compatibility shim configuration file was not found in %s", action, shimcfg), agent, reply)
   117  		return
   118  	}
   119  
   120  	// 1.5 extra second to give the shim time to start etc
   121  	tctx, cancel := context.WithTimeout(ctx, time.Duration(agent.Metadata().Timeout)*time.Second+(1500*time.Millisecond))
   122  	defer cancel()
   123  
   124  	execution := exec.CommandContext(tctx, agent.Config.Choria.RubyAgentShim, "--config", shimcfg)
   125  
   126  	stdin, err := execution.StdinPipe()
   127  	if err != nil {
   128  		abortAction(fmt.Sprintf("Cannot create stdin while calling Ruby action %s: %s", action, err), agent, reply)
   129  		return
   130  	}
   131  
   132  	shimr, err := newShimRequest(req)
   133  	if err != nil {
   134  		abortAction(fmt.Sprintf("Cannot prepare request data for Ruby action %s: %s", action, err), agent, reply)
   135  		return
   136  	}
   137  
   138  	stdout, err := execution.StdoutPipe()
   139  	if err != nil {
   140  		abortAction(fmt.Sprintf("Cannot open STDOUT from the Shim for Ruby action %s: %s", action, err), agent, reply)
   141  		return
   142  	}
   143  
   144  	err = execution.Start()
   145  	if err != nil {
   146  		abortAction(fmt.Sprintf("Cannot start the Shim for Ruby action %s: %s", action, err), agent, reply)
   147  		return
   148  	}
   149  
   150  	defer func() {
   151  		err := execution.Wait()
   152  		if err != nil {
   153  			agent.Log.Warnf("Wait call for action %s failed: %v", action, err)
   154  		}
   155  	}()
   156  
   157  	_, err = io.WriteString(stdin, string(shimr))
   158  	if err != nil {
   159  		abortAction(fmt.Sprintf("Could not send request to the Shim for Ruby action %s: %s", action, err), agent, reply)
   160  		return
   161  	}
   162  
   163  	stdin.Close()
   164  
   165  	if err := json.NewDecoder(stdout).Decode(reply); err != nil {
   166  		abortAction(fmt.Sprintf("Cannot decode output from Shim for Ruby action %s: %s", action, err), agent, reply)
   167  		return
   168  	}
   169  }
   170  
   171  func newShimRequest(req *mcorpc.Request) ([]byte, error) {
   172  	sr := ShimRequest{
   173  		Action: req.Action,
   174  		Agent:  req.Agent,
   175  		Body: &ShimRequestBody{
   176  			Action: req.Action,
   177  			Agent:  req.Agent,
   178  			Caller: req.CallerID,
   179  			Data:   req.Data,
   180  		},
   181  		CallerID:   req.CallerID,
   182  		Collective: req.Collective,
   183  		RequestID:  req.RequestID,
   184  		SenderID:   req.SenderID,
   185  		Time:       req.Time.Unix(),
   186  		TTL:        req.TTL,
   187  	}
   188  
   189  	return json.Marshal(sr)
   190  }
   191  
   192  func abortAction(reason string, agent *mcorpc.Agent, reply *mcorpc.Reply) {
   193  	agent.Log.Error(reason)
   194  	reply.Statuscode = mcorpc.Aborted
   195  	reply.Statusmsg = reason
   196  }
   197  
   198  func strToBool(s string) (bool, error) {
   199  	clean := strings.TrimSpace(s)
   200  
   201  	if regexp.MustCompile(`(?i)^(1|yes|true|y|t)$`).MatchString(clean) {
   202  		return true, nil
   203  	}
   204  
   205  	if regexp.MustCompile(`(?i)^(0|no|false|n|f)$`).MatchString(clean) {
   206  		return false, nil
   207  	}
   208  
   209  	return false, errors.New("cannot convert string value '" + clean + "' into a boolean.")
   210  }