lib/config.go (273 lines of code) (raw):

/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ package dhcplb import ( "encoding/json" "fmt" "io/ioutil" "net" "path/filepath" "strings" "time" "github.com/fsnotify/fsnotify" "github.com/golang/glog" ) // ConfigProvider is an interface which provides methods to fetch the // HostSourcer, parse extra configuration, provide additional load balancing // implementations and how to handle dhcp requests in server mode type ConfigProvider interface { NewHostSourcer( sourcerType, args string, version int) (DHCPServerSourcer, error) ParseExtras(extras json.RawMessage) (interface{}, error) NewDHCPBalancingAlgorithm(version int) (DHCPBalancingAlgorithm, error) NewHandler(extras interface{}, version int) (Handler, error) } // Config represents the server configuration. type Config struct { Version int Addr *net.UDPAddr Algorithm DHCPBalancingAlgorithm ServerUpdateInterval time.Duration PacketBufSize int Handler Handler HostSourcer DHCPServerSourcer RCRatio uint32 Overrides map[string]Override Extras interface{} CacheSize int CacheRate int Rate int ReplyAddr *net.UDPAddr } // Override represents the dhcp server or the group of dhcp servers (tier) we // want to send packets to. type Override struct { // note that Host override takes precedence over Tier Host string `json:"host"` Tier string `json:"tier"` Expiration string `json:"expiration"` } // Overrides is a struct that holds v4 and v6 list of overrides. // The keys of the map are mac addresses. type Overrides struct { V4 map[string]Override `json:"v4"` V6 map[string]Override `json:"v6"` } // LoadConfig will take the path of the json file, the path of the override json // file, an integer version and a ConfigProvider and will return a pointer to // a Config object. func LoadConfig(path, overridesPath string, version int, provider ConfigProvider) (*Config, error) { file, err := ioutil.ReadFile(path) if err != nil { return nil, err } overridesFile := []byte{} // path length of 0 means we aren't using overrides if len(overridesPath) != 0 { err = nil if overridesFile, err = ioutil.ReadFile(overridesPath); err != nil { return nil, err } } return ParseConfig(file, overridesFile, version, provider) } // ParseConfig will take JSON config files, a version and a ConfigProvider, // and return a pointer to a Config struct func ParseConfig(jsonConfig, jsonOverrides []byte, version int, provider ConfigProvider) (*Config, error) { var combined combinedconfigSpec if err := json.Unmarshal(jsonConfig, &combined); err != nil { glog.Errorf("Failed to parse JSON: %s", err) return nil, err } var spec configSpec if version == 4 { spec = combined.V4 } else if version == 6 { spec = combined.V6 } var overrides map[string]Override if len(jsonOverrides) == 0 { overrides = make(map[string]Override) } else { var err error overrides, err = parseOverrides(jsonOverrides, version) if err != nil { glog.Errorf("Failed to load overrides: %s", err) return nil, err } } glog.Infof("Loaded %d override(s)", len(overrides)) return newConfig(&spec, overrides, provider) } // WatchConfig will keep watching for changes to both config and override json // files. It uses fsnotify library (it uses inotify in Linux), and call // LoadConfig when it an inotify event signals the modification of the json // files. func WatchConfig( configPath, overridesPath string, version int, provider ConfigProvider, ) (chan *Config, error) { configChan := make(chan *Config) watcher, err := fsnotify.NewWatcher() if err != nil { return nil, err } // strings containing the real path of a config files, if they are symlinks var realConfigPath string var realOverridesPath string err = watcher.Add(filepath.Dir(configPath)) if err != nil { return nil, err } realConfigPath, err = filepath.EvalSymlinks(configPath) if err == nil { // configPath is a symlink, also watch the pointee err = watcher.Add(realConfigPath) } // setup watcher on overrides file if present if len(overridesPath) > 0 { err = watcher.Add(filepath.Dir(overridesPath)) if err != nil { glog.Errorf("Failed to start fsnotify on overrides config file: %s", err) return nil, err } realOverridesPath, err = filepath.EvalSymlinks(overridesPath) if err == nil { // overridesPath is a symlink, also watch the pointee err = watcher.Add(realOverridesPath) } } // watch for fsnotify events go func() { for { select { case ev := <-watcher.Events: // ignore Remove events if ev.Op&fsnotify.Remove == fsnotify.Remove { continue } // only care about symlinks and target of symlinks if ev.Name == overridesPath || ev.Name == configPath || ev.Name == realOverridesPath || ev.Name == realConfigPath { glog.Infof("Configuration file changed (%s), reloading", ev) config, err := LoadConfig( configPath, overridesPath, version, provider) if err != nil { glog.Fatalf("Failed to reload config: %s", err) panic(err) // fail hard } configChan <- config } case err := <-watcher.Errors: glog.Errorf("fsnotify error: %s", err) } } }() return configChan, nil } // configSpec holds the raw json configuration. type configSpec struct { Path string Version int `json:"version"` ListenAddr string `json:"listen_addr"` Port int `json:"port"` AlgorithmName string `json:"algorithm"` UpdateServerInterval int `json:"update_server_interval"` PacketBufSize int `json:"packet_buf_size"` HostSourcer string `json:"host_sourcer"` RCRatio uint32 `json:"rc_ratio"` Extras json.RawMessage `json:"extras"` CacheSize int `json:"throttle_cache_size"` CacheRate int `json:"throttle_cache_rate"` Rate int `json:"throttle_rate"` ReplyAddr string `json:"reply_addr"` } type combinedconfigSpec struct { V4 configSpec `json:"v4"` V6 configSpec `json:"v6"` } func (c *configSpec) sourcer(provider ConfigProvider) (DHCPServerSourcer, error) { // Load the DHCPServerSourcer implementation sourcerInfo := strings.Split(c.HostSourcer, ":") sourcerType := sourcerInfo[0] stable := sourcerInfo[1] rc := "" if strings.Index(sourcerInfo[1], ",") > -1 { sourcerArgs := strings.Split(sourcerInfo[1], ",") stable = sourcerArgs[0] rc = sourcerArgs[1] } switch sourcerType { default: return provider.NewHostSourcer(sourcerType, sourcerInfo[1], c.Version) case "file": sourcer, err := NewFileSourcer(stable, rc, c.Version) if err != nil { glog.Fatalf("Can't load FileSourcer") } return sourcer, err } } func (c *configSpec) algorithm(provider ConfigProvider) (DHCPBalancingAlgorithm, error) { // Balancing algorithms coming with the dhcplb source code modulo := new(modulo) rr := new(roundRobin) algorithms := map[string]DHCPBalancingAlgorithm{ modulo.Name(): modulo, rr.Name(): rr, } // load other non default algorithms from the ConfigProvider providedAlgo, err := provider.NewDHCPBalancingAlgorithm(c.Version) if err != nil { glog.Fatalf("Provided load balancing implementation error: %s", err) } if providedAlgo != nil { if _, exists := algorithms[providedAlgo.Name()]; exists { glog.Fatalf("Algorithm name %s exists already, pick another name.", providedAlgo.Name()) } algorithms[providedAlgo.Name()] = providedAlgo } lb, ok := algorithms[c.AlgorithmName] if !ok { supported := []string{} for k := range algorithms { supported = append(supported, k) } glog.Fatalf( "'%s' is not a supported balancing algorithm. "+ "Supported balancing algorithms are: %v", c.AlgorithmName, supported) return nil, fmt.Errorf( "'%s' is not a supported balancing algorithm", c.AlgorithmName) } lb.SetRCRatio(c.RCRatio) return lb, nil } func newConfig(spec *configSpec, overrides map[string]Override, provider ConfigProvider) (*Config, error) { if spec.Version != 4 && spec.Version != 6 { return nil, fmt.Errorf("Supported version: 4, 6 - not %d", spec.Version) } targetIP := net.ParseIP(spec.ListenAddr) if targetIP == nil { return nil, fmt.Errorf("Unable to parse IP %s", targetIP) } addr := &net.UDPAddr{ IP: targetIP, Port: spec.Port, Zone: "", } algo, err := spec.algorithm(provider) if err != nil { return nil, err } sourcer, err := spec.sourcer(provider) if err != nil { return nil, err } // extras extras, err := provider.ParseExtras(spec.Extras) if err != nil { return nil, err } handler, err := provider.NewHandler(extras, spec.Version) if err != nil { return nil, err } return &Config{ Version: spec.Version, Addr: addr, Algorithm: algo, ServerUpdateInterval: time.Duration( spec.UpdateServerInterval) * time.Second, PacketBufSize: spec.PacketBufSize, Handler: handler, HostSourcer: sourcer, RCRatio: spec.RCRatio, Overrides: overrides, Extras: extras, CacheSize: spec.CacheSize, CacheRate: spec.CacheRate, Rate: spec.Rate, ReplyAddr: &net.UDPAddr{IP: net.ParseIP(spec.ReplyAddr)}, }, nil } func parseOverrides(file []byte, version int) (map[string]Override, error) { overrides := Overrides{} err := json.Unmarshal(file, &overrides) if err != nil { glog.Errorf("Failed to parse JSON: %s", err) return nil, err } if version == 4 { return overrides.V4, nil } else if version == 6 { return overrides.V6, nil } return nil, fmt.Errorf("Unsupported version %d, must be 4|6", version) }