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  }