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 }