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 }