github.com/psiphon-inc/goarista@v0.0.0-20160825065156-d002785f4c67/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 "encoding/json" 12 "flag" 13 "strconv" 14 "strings" 15 "sync" 16 17 "github.com/aristanetworks/glog" 18 "github.com/aristanetworks/goarista/openconfig" 19 occlient "github.com/aristanetworks/goarista/openconfig/client" 20 redis "gopkg.in/redis.v4" 21 ) 22 23 var clusterMode = flag.Bool("cluster", false, "Whether the redis server is a cluster") 24 25 var redisFlag = flag.String("redis", "", 26 "Comma separated list of Redis servers to push updates to") 27 28 var redisPassword = flag.String("redispass", "", "Password of redis server/cluster") 29 30 // baseClient allows us to represent both a redis.Client and redis.ClusterClient. 31 type baseClient interface { 32 Close() error 33 ClusterInfo() *redis.StringCmd 34 HDel(string, ...string) *redis.IntCmd 35 HMSet(string, map[string]string) *redis.StatusCmd 36 Ping() *redis.StatusCmd 37 Pipelined(func(*redis.Pipeline) error) ([]redis.Cmder, error) 38 Publish(string, string) *redis.IntCmd 39 } 40 41 var client baseClient 42 43 func main() { 44 username, password, subscriptions, hostAddrs, opts := occlient.ParseFlags() 45 if *redisFlag == "" { 46 glog.Fatal("Specify the address of the Redis server to write to with -redis") 47 } 48 49 redisAddrs := strings.Split(*redisFlag, ",") 50 if !*clusterMode && len(redisAddrs) > 1 { 51 glog.Fatal("Please pass only 1 redis address in noncluster mode or enable cluster mode") 52 } 53 54 if *clusterMode { 55 client = redis.NewClusterClient(&redis.ClusterOptions{ 56 Addrs: redisAddrs, 57 Password: *redisPassword, 58 }) 59 } else { 60 client = redis.NewClient(&redis.Options{ 61 Addr: *redisFlag, 62 Password: *redisPassword, 63 }) 64 } 65 defer client.Close() 66 67 // TODO: Figure out ways to handle being in the wrong mode: 68 // Connecting to cluster in non cluster mode - we get a MOVED error on the first HMSET 69 // Connecting to a noncluster in cluster mode - we get stuck forever 70 _, err := client.Ping().Result() 71 if err != nil { 72 glog.Fatal("Failed to connect to client: ", err) 73 } 74 75 ocPublish := func(addr string) func(*openconfig.SubscribeResponse) { 76 return func(resp *openconfig.SubscribeResponse) { 77 if notif := resp.GetUpdate(); notif != nil { 78 bufferToRedis(addr, notif) 79 } 80 } 81 } 82 83 wg := new(sync.WaitGroup) 84 for _, hostAddr := range hostAddrs { 85 wg.Add(1) 86 go occlient.Run(ocPublish(hostAddr), wg, username, password, hostAddr, subscriptions, opts) 87 } 88 wg.Wait() 89 } 90 91 type redisData struct { 92 key string 93 hmset map[string]string 94 hdel []string 95 pub map[string]interface{} 96 } 97 98 func bufferToRedis(addr string, notif *openconfig.Notification) { 99 path := addr + "/" + joinPath(notif.Prefix) 100 data := &redisData{key: path} 101 102 if len(notif.Update) != 0 { 103 hmset := make(map[string]string, len(notif.Update)) 104 105 // Updates to publish on the pub/sub. 106 pub := make(map[string]interface{}, len(notif.Update)) 107 for _, update := range notif.Update { 108 key := joinPath(update.Path) 109 value := convertUpdate(update) 110 pub[key] = value 111 marshaledValue, err := json.Marshal(value) 112 if err != nil { 113 glog.Fatalf("Failed to JSON marshal update %#v", update) 114 } 115 hmset[key] = string(marshaledValue) 116 } 117 data.hmset = hmset 118 data.pub = pub 119 } 120 121 if len(notif.Delete) != 0 { 122 hdel := make([]string, len(notif.Delete)) 123 for i, del := range notif.Delete { 124 hdel[i] = joinPath(del) 125 } 126 data.hdel = hdel 127 } 128 pushToRedis(data) 129 } 130 131 func pushToRedis(data *redisData) { 132 _, err := client.Pipelined(func(pipe *redis.Pipeline) error { 133 if data.hmset != nil { 134 if reply := client.HMSet(data.key, data.hmset); reply.Err() != nil { 135 glog.Fatal("Redis HMSET error: ", reply.Err()) 136 } 137 redisPublish(data.key, "updates", data.pub) 138 } 139 if data.hdel != nil { 140 if reply := client.HDel(data.key, data.hdel...); reply.Err() != nil { 141 glog.Fatal("Redis HDEL error: ", reply.Err()) 142 } 143 redisPublish(data.key, "deletes", data.hdel) 144 } 145 return nil 146 }) 147 if err != nil { 148 glog.Fatal("Failed to send Pipelined commands: ", err) 149 } 150 } 151 152 func redisPublish(path, kind string, payload interface{}) { 153 js, err := json.Marshal(map[string]interface{}{ 154 "kind": kind, 155 "payload": payload, 156 }) 157 if err != nil { 158 glog.Fatalf("JSON error: %s", err) 159 } 160 if reply := client.Publish(path, string(js)); reply.Err() != nil { 161 glog.Fatal("Redis PUBLISH error: ", reply.Err()) 162 } 163 } 164 165 func joinPath(path *openconfig.Path) string { 166 return strings.Join(path.Element, "/") 167 } 168 169 func convertUpdate(update *openconfig.Update) interface{} { 170 switch update.Value.Type { 171 case openconfig.Type_JSON: 172 var value interface{} 173 err := json.Unmarshal(update.Value.Value, &value) 174 if err != nil { 175 glog.Fatalf("Malformed JSON update %q in %s", update.Value.Value, update) 176 } 177 return value 178 case openconfig.Type_BYTES: 179 return strconv.Quote(string(update.Value.Value)) 180 default: 181 glog.Fatalf("Unhandled type of value %v in %s", update.Value.Type, update) 182 return nil 183 } 184 }