github.com/aristanetworks/goarista@v0.0.0-20240514173732-cca2755bbd44/cmd/ocredis/main.go (about)

     1  // Copyright (c) 2016 Arista Networks, Inc.
     2  // Use of this source code is governed by the Apache License 2.0
     3  // that can be found in the COPYING file.
     4  
     5  // The ocredis tool is a client for the OpenConfig gRPC interface that
     6  // subscribes to state and pushes it to Redis, using Redis' support for hash
     7  // maps and for publishing events that can be subscribed to.
     8  package main
     9  
    10  import (
    11  	"context"
    12  	"encoding/json"
    13  	"flag"
    14  	"fmt"
    15  	"strings"
    16  
    17  	"github.com/aristanetworks/goarista/gnmi"
    18  
    19  	"github.com/aristanetworks/glog"
    20  	pb "github.com/openconfig/gnmi/proto/gnmi"
    21  	"golang.org/x/sync/errgroup"
    22  	redis "gopkg.in/redis.v4"
    23  )
    24  
    25  var clusterMode = flag.Bool("cluster", false, "Whether the redis server is a cluster")
    26  
    27  var redisFlag = flag.String("redis", "",
    28  	"Comma separated list of Redis servers to push updates to")
    29  
    30  var redisPassword = flag.String("redispass", "", "Password of redis server/cluster")
    31  
    32  // baseClient allows us to represent both a redis.Client and redis.ClusterClient.
    33  type baseClient interface {
    34  	Close() error
    35  	ClusterInfo() *redis.StringCmd
    36  	HDel(string, ...string) *redis.IntCmd
    37  	HMSet(string, map[string]string) *redis.StatusCmd
    38  	Ping() *redis.StatusCmd
    39  	Pipelined(func(*redis.Pipeline) error) ([]redis.Cmder, error)
    40  	Publish(string, string) *redis.IntCmd
    41  }
    42  
    43  var client baseClient
    44  
    45  func main() {
    46  
    47  	// gNMI options
    48  	cfg := &gnmi.Config{}
    49  	flag.StringVar(&cfg.Addr, "addr", "localhost", "gNMI gRPC server `address`")
    50  	flag.StringVar(&cfg.CAFile, "cafile", "", "Path to server TLS certificate file")
    51  	flag.StringVar(&cfg.CertFile, "certfile", "", "Path to client TLS certificate file")
    52  	flag.StringVar(&cfg.KeyFile, "keyfile", "", "Path to client TLS private key file")
    53  	flag.StringVar(&cfg.Username, "username", "", "Username to authenticate with")
    54  	flag.StringVar(&cfg.Password, "password", "", "Password to authenticate with")
    55  	flag.BoolVar(&cfg.TLS, "tls", false, "Enable TLS")
    56  	flag.StringVar(&cfg.TLSMinVersion, "tls-min-version", "",
    57  		fmt.Sprintf("Set minimum TLS version for connection (%s)", gnmi.TLSVersions))
    58  	flag.StringVar(&cfg.TLSMaxVersion, "tls-max-version", "",
    59  		fmt.Sprintf("Set maximum TLS version for connection (%s)", gnmi.TLSVersions))
    60  	subscribePaths := flag.String("subscribe", "/", "Comma-separated list of paths to subscribe to")
    61  	flag.Parse()
    62  	if *redisFlag == "" {
    63  		glog.Fatal("Specify the address of the Redis server to write to with -redis")
    64  	}
    65  
    66  	subscriptions := strings.Split(*subscribePaths, ",")
    67  	redisAddrs := strings.Split(*redisFlag, ",")
    68  	if !*clusterMode && len(redisAddrs) > 1 {
    69  		glog.Fatal("Please pass only 1 redis address in noncluster mode or enable cluster mode")
    70  	}
    71  
    72  	if *clusterMode {
    73  		client = redis.NewClusterClient(&redis.ClusterOptions{
    74  			Addrs:    redisAddrs,
    75  			Password: *redisPassword,
    76  		})
    77  	} else {
    78  		client = redis.NewClient(&redis.Options{
    79  			Addr:     *redisFlag,
    80  			Password: *redisPassword,
    81  		})
    82  	}
    83  	defer client.Close()
    84  
    85  	// TODO: Figure out ways to handle being in the wrong mode:
    86  	// Connecting to cluster in non cluster mode - we get a MOVED error on the first HMSET
    87  	// Connecting to a noncluster in cluster mode - we get stuck forever
    88  	_, err := client.Ping().Result()
    89  	if err != nil {
    90  		glog.Fatal("Failed to connect to client: ", err)
    91  	}
    92  	ctx := gnmi.NewContext(context.Background(), cfg)
    93  	client, err := gnmi.Dial(cfg)
    94  	if err != nil {
    95  		glog.Fatal(err)
    96  	}
    97  	respChan := make(chan *pb.SubscribeResponse)
    98  	subscribeOptions := &gnmi.SubscribeOptions{
    99  		Mode:       "stream",
   100  		StreamMode: "target_defined",
   101  		Paths:      gnmi.SplitPaths(subscriptions),
   102  	}
   103  	var g errgroup.Group
   104  	g.Go(func() error { return gnmi.SubscribeErr(ctx, client, subscribeOptions, respChan) })
   105  	for resp := range respChan {
   106  		bufferToRedis(cfg.Addr, resp.GetUpdate())
   107  	}
   108  	if err := g.Wait(); err != nil {
   109  		glog.Fatal(err)
   110  	}
   111  }
   112  
   113  type redisData struct {
   114  	key   string
   115  	hmset map[string]string
   116  	hdel  []string
   117  	pub   map[string]interface{}
   118  }
   119  
   120  func bufferToRedis(addr string, notif *pb.Notification) {
   121  	if notif == nil {
   122  		// possible that this should be ignored silently
   123  		glog.Error("Nil notification ignored")
   124  		return
   125  	}
   126  	path := addr + "/" + joinPath(notif.Prefix)
   127  	data := &redisData{key: path}
   128  
   129  	if len(notif.Update) != 0 {
   130  		hmset := make(map[string]string, len(notif.Update))
   131  
   132  		// Updates to publish on the pub/sub.
   133  		pub := make(map[string]interface{}, len(notif.Update))
   134  		for _, update := range notif.Update {
   135  			key := joinPath(update.Path)
   136  			value, err := gnmi.ExtractValue(update)
   137  			if err != nil {
   138  				glog.Fatalf("Failed to extract valid type from %#v", update)
   139  			}
   140  			pub[key] = value
   141  			marshaledValue, err := json.Marshal(value)
   142  			if err != nil {
   143  				glog.Fatalf("Failed to JSON marshal update %#v", update)
   144  			}
   145  			hmset[key] = string(marshaledValue)
   146  		}
   147  		data.hmset = hmset
   148  		data.pub = pub
   149  	}
   150  
   151  	if len(notif.Delete) != 0 {
   152  		hdel := make([]string, len(notif.Delete))
   153  		for i, del := range notif.Delete {
   154  			hdel[i] = joinPath(del)
   155  		}
   156  		data.hdel = hdel
   157  	}
   158  	pushToRedis(data)
   159  }
   160  
   161  func pushToRedis(data *redisData) {
   162  	_, err := client.Pipelined(func(pipe *redis.Pipeline) error {
   163  		if data.hmset != nil {
   164  			if reply := client.HMSet(data.key, data.hmset); reply.Err() != nil {
   165  				glog.Fatal("Redis HMSET error: ", reply.Err())
   166  			}
   167  			redisPublish(data.key, "updates", data.pub)
   168  		}
   169  		if data.hdel != nil {
   170  			if reply := client.HDel(data.key, data.hdel...); reply.Err() != nil {
   171  				glog.Fatal("Redis HDEL error: ", reply.Err())
   172  			}
   173  			redisPublish(data.key, "deletes", data.hdel)
   174  		}
   175  		return nil
   176  	})
   177  	if err != nil {
   178  		glog.Fatal("Failed to send Pipelined commands: ", err)
   179  	}
   180  }
   181  
   182  func redisPublish(path, kind string, payload interface{}) {
   183  	js, err := json.Marshal(map[string]interface{}{
   184  		"kind":    kind,
   185  		"payload": payload,
   186  	})
   187  	if err != nil {
   188  		glog.Fatalf("JSON error: %s", err)
   189  	}
   190  	if reply := client.Publish(path, string(js)); reply.Err() != nil {
   191  		glog.Fatal("Redis PUBLISH error: ", reply.Err())
   192  	}
   193  }
   194  
   195  func joinPath(path *pb.Path) string {
   196  	return gnmi.StrPath(path)
   197  }