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  }