github.com/choria-io/go-choria@v0.28.1-0.20240416190746-b3bf9c7d5a45/providers/agent/mcorpc/golang/rpcutil/rpcutil.go (about) 1 // Copyright (c) 2020-2022, R.I. Pienaar and the Choria Project contributors 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 5 package rpcutil 6 7 import ( 8 "context" 9 "encoding/json" 10 "fmt" 11 "os" 12 "runtime" 13 "sort" 14 "strings" 15 "time" 16 17 "github.com/choria-io/go-choria/config" 18 "github.com/choria-io/go-choria/confkey" 19 "github.com/choria-io/go-choria/filter/facts" 20 "github.com/choria-io/go-choria/inter" 21 "github.com/choria-io/go-choria/internal/util" 22 "github.com/choria-io/go-choria/providers/agent/mcorpc" 23 "github.com/choria-io/go-choria/providers/data" 24 "github.com/choria-io/go-choria/server" 25 "github.com/choria-io/go-choria/server/agents" 26 "github.com/sirupsen/logrus" 27 ) 28 29 type PingReply struct { 30 Pong int64 `json:"pong"` 31 } 32 33 type GetFactReply struct { 34 Fact string `json:"fact"` 35 Value any `json:"value"` 36 } 37 38 type GetFactsReply struct { 39 Values map[string]any `json:"values"` 40 } 41 42 type CollectiveInfoReply struct { 43 MainCollective string `json:"main_collective"` 44 Collectives []string `json:"collectives"` 45 } 46 47 type AgentInventoryInfoReply struct { 48 Agent string `json:"agent"` 49 50 agents.Metadata 51 } 52 53 type AgentInventoryReply struct { 54 Agents []AgentInventoryInfoReply `json:"agents"` 55 } 56 57 type GetConfigItemReply struct { 58 Item string `json:"item"` 59 Value string `json:"value"` 60 } 61 62 type MachineState struct { 63 Name string `json:"name" yaml:"name"` 64 State string `json:"state" yaml:"state"` 65 Version string `json:"version" yaml:"version"` 66 } 67 68 type InventoryReply struct { 69 Agents []string `json:"agents"` 70 Classes []string `json:"classes"` 71 Collectives []string `json:"collectives"` 72 DataPlugins []string `json:"data_plugins"` 73 Facts json.RawMessage `json:"facts"` 74 Machines []MachineState `json:"machines"` 75 MainCollective string `json:"main_collective"` 76 Version string `json:"version"` 77 Upgradable bool `json:"upgradable"` 78 } 79 80 type CPUTimes struct { 81 ChildSystemTime int `json:"cstime"` 82 ChildUserTime int `json:"cutime"` 83 SystemTime int `json:"stime"` 84 UserTime int `json:"utime"` 85 } 86 87 type DaemonStatsReply struct { 88 Agents []string `json:"agents"` 89 ConfigFile string `json:"configfile"` 90 Filtered int64 `json:"filtered"` 91 PID int `json:"pid"` 92 Passed int64 `json:"passed"` 93 Procs []string `json:"threads"` 94 Replies int64 `json:"replies"` 95 StartTime int64 `json:"starttime"` 96 TTLExpired int64 `json:"ttlexpired"` 97 Events int64 `json:"events"` 98 Times CPUTimes `json:"times"` 99 Total int64 `json:"total"` 100 Unvalidated int64 `json:"unvalidated"` 101 Validated int64 `json:"validated"` 102 Version string `json:"version"` 103 } 104 105 type GetDataRequest struct { 106 Query string `json:"query"` 107 Source string `json:"source"` 108 } 109 110 type GetConfigItemRequest struct { 111 Item string `json:"item"` 112 } 113 114 type GetConfigItemResponse struct { 115 Item string `json:"item"` 116 Value any `json:"value"` 117 } 118 119 // New creates a new rpcutil agent 120 func New(mgr server.AgentManager) (*mcorpc.Agent, error) { 121 bi := util.BuildInfo() 122 metadata := &agents.Metadata{ 123 Name: "rpcutil", 124 Description: "Choria RPC Utilities", 125 Author: "R.I.Pienaar <rip@devco.net>", 126 Version: bi.Version(), 127 License: bi.License(), 128 Timeout: 2, 129 URL: "http://choria.io", 130 } 131 132 agent := mcorpc.New("rpcutil", metadata, mgr.Choria(), mgr.Logger()) 133 134 err := agent.RegisterAction("collective_info", collectiveInfoAction) 135 if err != nil { 136 return nil, fmt.Errorf("could not register collective_info action: %s", err) 137 } 138 139 agent.MustRegisterAction("ping", pingAction) 140 agent.MustRegisterAction("get_fact", getFactAction) 141 agent.MustRegisterAction("get_facts", getFactsAction) 142 agent.MustRegisterAction("agent_inventory", agentInventoryAction) 143 agent.MustRegisterAction("inventory", inventoryAction) 144 agent.MustRegisterAction("daemon_stats", daemonStatsAction) 145 agent.MustRegisterAction("get_data", getData) 146 agent.MustRegisterAction("get_config_item", getConfigItem) 147 148 return agent, nil 149 } 150 151 func getConfigItem(ctx context.Context, req *mcorpc.Request, reply *mcorpc.Reply, agent *mcorpc.Agent, conn inter.ConnectorInfo) { 152 i := GetConfigItemRequest{} 153 if !mcorpc.ParseRequestData(&i, req, reply) { 154 return 155 } 156 157 val, ok := confkey.InterfaceWithKey(agent.Config, i.Item) 158 if !ok { 159 val, ok = confkey.InterfaceWithKey(agent.Config.Choria, i.Item) 160 if !ok { 161 reply.Statuscode = mcorpc.Aborted 162 reply.Statusmsg = "Unknown key" 163 return 164 } 165 } 166 167 r := &GetConfigItemResponse{Item: i.Item, Value: val} 168 reply.Data = r 169 } 170 171 func getData(ctx context.Context, req *mcorpc.Request, reply *mcorpc.Reply, agent *mcorpc.Agent, conn inter.ConnectorInfo) { 172 dfm, err := agent.ServerInfoSource.DataFuncMap() 173 if err != nil { 174 reply.Statuscode = mcorpc.Aborted 175 reply.Statusmsg = "Could not load data sources" 176 agent.Log.Errorf("Failed to load data sources: %s", err) 177 return 178 } 179 180 i := GetDataRequest{} 181 if !mcorpc.ParseRequestData(&i, req, reply) { 182 return 183 } 184 185 df, ok := dfm[i.Source] 186 if !ok { 187 reply.Statuscode = mcorpc.Aborted 188 reply.Statusmsg = "Unknown data plugin" 189 return 190 } 191 192 var output map[string]data.OutputItem 193 194 if df.DDL.Query == nil { 195 f, ok := df.F.(func() map[string]data.OutputItem) 196 if !ok { 197 reply.Statuscode = mcorpc.Aborted 198 reply.Statusmsg = "Invalid data plugin" 199 return 200 } 201 202 output = f() 203 } else { 204 f, ok := df.F.(func(string) map[string]data.OutputItem) 205 if !ok { 206 reply.Statuscode = mcorpc.Aborted 207 reply.Statusmsg = "Invalid data plugin" 208 return 209 } 210 output = f(i.Query) 211 } 212 213 reply.Data = output 214 } 215 216 func daemonStatsAction(ctx context.Context, req *mcorpc.Request, reply *mcorpc.Reply, agent *mcorpc.Agent, conn inter.ConnectorInfo) { 217 stats := agent.ServerInfoSource.Stats() 218 219 bi := util.BuildInfo() 220 221 output := &DaemonStatsReply{ 222 Agents: agent.ServerInfoSource.KnownAgents(), 223 ConfigFile: agent.ServerInfoSource.ConfigFile(), 224 Filtered: stats.Filtered, 225 PID: os.Getpid(), 226 Passed: stats.Passed, 227 Procs: []string{fmt.Sprintf("Go %s with %d go procs on %d cores", runtime.Version(), runtime.NumGoroutine(), runtime.NumCPU())}, 228 Replies: stats.Replies, 229 StartTime: agent.ServerInfoSource.StartTime().Unix(), 230 TTLExpired: stats.TTLExpired, 231 Events: stats.Events, 232 Times: CPUTimes{}, 233 Total: stats.Total, 234 Unvalidated: stats.Invalid, 235 Validated: stats.Valid, 236 Version: bi.Version(), 237 } 238 239 reply.Data = output 240 } 241 242 func inventoryAction(ctx context.Context, req *mcorpc.Request, reply *mcorpc.Reply, agent *mcorpc.Agent, conn inter.ConnectorInfo) { 243 output := &InventoryReply{ 244 Agents: agent.ServerInfoSource.KnownAgents(), 245 Classes: agent.ServerInfoSource.Classes(), 246 Collectives: agent.Config.Collectives, 247 DataPlugins: []string{}, 248 Facts: agent.ServerInfoSource.Facts(), 249 Machines: []MachineState{}, 250 MainCollective: agent.Config.MainCollective, 251 Version: util.BuildInfo().Version(), 252 Upgradable: agent.Config.Choria.ProvisionAllowUpdate || util.BuildInfo().ProvisionAllowServerUpdate(), 253 } 254 255 dfm, err := agent.ServerInfoSource.DataFuncMap() 256 if err != nil { 257 agent.Log.Warnf("Could not retrieve data plugin list: %s", err) 258 } 259 for _, d := range dfm { 260 output.DataPlugins = append(output.DataPlugins, d.Name) 261 } 262 sort.Strings(output.DataPlugins) 263 264 states, err := agent.ServerInfoSource.MachinesStatus() 265 if err != nil { 266 agent.Log.Warnf("Could not retrieve machine status: %s", err) 267 } 268 269 for _, s := range states { 270 output.Machines = append(output.Machines, MachineState{ 271 Name: s.Name, 272 Version: s.Version, 273 State: s.State, 274 }) 275 } 276 277 reply.Data = output 278 } 279 280 func agentInventoryAction(ctx context.Context, req *mcorpc.Request, reply *mcorpc.Reply, agent *mcorpc.Agent, conn inter.ConnectorInfo) { 281 o := AgentInventoryReply{} 282 reply.Data = &o 283 284 for _, a := range agent.ServerInfoSource.KnownAgents() { 285 md, ok := agent.ServerInfoSource.AgentMetadata(a) 286 287 if ok { 288 o.Agents = append(o.Agents, AgentInventoryInfoReply{a, md}) 289 } 290 } 291 } 292 293 func getFactsAction(ctx context.Context, req *mcorpc.Request, reply *mcorpc.Reply, agent *mcorpc.Agent, conn inter.ConnectorInfo) { 294 type input struct { 295 Facts string `json:"facts"` 296 } 297 298 i := input{} 299 if !mcorpc.ParseRequestData(&i, req, reply) { 300 return 301 } 302 303 o := &GetFactsReply{ 304 Values: make(map[string]any), 305 } 306 reply.Data = o 307 308 for _, fact := range strings.Split(i.Facts, ",") { 309 fact = strings.TrimSpace(fact) 310 v, _ := getFactValue(fact, agent.Config, agent.Log) 311 o.Values[fact] = v 312 } 313 } 314 315 func getFactAction(ctx context.Context, req *mcorpc.Request, reply *mcorpc.Reply, agent *mcorpc.Agent, conn inter.ConnectorInfo) { 316 type input struct { 317 Fact string `json:"fact"` 318 } 319 320 i := input{} 321 if !mcorpc.ParseRequestData(&i, req, reply) { 322 return 323 } 324 325 o := GetFactReply{i.Fact, nil} 326 reply.Data = &o 327 328 v, err := getFactValue(i.Fact, agent.Config, agent.Log) 329 if err != nil { 330 // I imagine you might want to error here, but old code just return nil 331 return 332 } 333 334 o.Value = v 335 } 336 337 func pingAction(ctx context.Context, req *mcorpc.Request, reply *mcorpc.Reply, agent *mcorpc.Agent, conn inter.ConnectorInfo) { 338 reply.Data = PingReply{time.Now().Unix()} 339 } 340 341 func collectiveInfoAction(ctx context.Context, req *mcorpc.Request, reply *mcorpc.Reply, agent *mcorpc.Agent, conn inter.ConnectorInfo) { 342 reply.Data = CollectiveInfoReply{ 343 MainCollective: agent.Config.MainCollective, 344 Collectives: agent.Config.Collectives, 345 } 346 } 347 348 func getFactValue(fact string, c *config.Config, log *logrus.Entry) (any, error) { 349 _, value, err := facts.GetFact(fact, c.FactSourceFile, log) 350 if err != nil { 351 return nil, err 352 } 353 354 if !value.Exists() { 355 return nil, nil 356 } 357 358 return value.Value(), nil 359 }