mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00

For tests and other environments it would be nice to be able to tune how long to sleep between retries.
508 lines
15 KiB
Go
508 lines
15 KiB
Go
package config
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/coreos/etcd/third_party/github.com/BurntSushi/toml"
|
|
|
|
"github.com/coreos/etcd/discovery"
|
|
"github.com/coreos/etcd/log"
|
|
ustrings "github.com/coreos/etcd/pkg/strings"
|
|
"github.com/coreos/etcd/server"
|
|
)
|
|
|
|
// The default location for the etcd configuration file.
|
|
const DefaultSystemConfigPath = "/etc/etcd/etcd.conf"
|
|
|
|
// A lookup of deprecated flags to their new flag name.
|
|
var newFlagNameLookup = map[string]string{
|
|
"C": "peers",
|
|
"CF": "peers-file",
|
|
"n": "name",
|
|
"c": "addr",
|
|
"cl": "bind-addr",
|
|
"s": "peer-addr",
|
|
"sl": "peer-bind-addr",
|
|
"d": "data-dir",
|
|
"m": "max-result-buffer",
|
|
"r": "max-retry-attempts",
|
|
"maxsize": "max-cluster-size",
|
|
"clientCAFile": "ca-file",
|
|
"clientCert": "cert-file",
|
|
"clientKey": "key-file",
|
|
"serverCAFile": "peer-ca-file",
|
|
"serverCert": "peer-cert-file",
|
|
"serverKey": "peer-key-file",
|
|
"snapshotCount": "snapshot-count",
|
|
}
|
|
|
|
// Config represents the server configuration.
|
|
type Config struct {
|
|
SystemPath string
|
|
|
|
Addr string `toml:"addr" env:"ETCD_ADDR"`
|
|
BindAddr string `toml:"bind_addr" env:"ETCD_BIND_ADDR"`
|
|
CAFile string `toml:"ca_file" env:"ETCD_CA_FILE"`
|
|
CertFile string `toml:"cert_file" env:"ETCD_CERT_FILE"`
|
|
CPUProfileFile string
|
|
CorsOrigins []string `toml:"cors" env:"ETCD_CORS"`
|
|
DataDir string `toml:"data_dir" env:"ETCD_DATA_DIR"`
|
|
Discovery string `toml:"discovery" env:"ETCD_DISCOVERY"`
|
|
Force bool
|
|
KeyFile string `toml:"key_file" env:"ETCD_KEY_FILE"`
|
|
Peers []string `toml:"peers" env:"ETCD_PEERS"`
|
|
PeersFile string `toml:"peers_file" env:"ETCD_PEERS_FILE"`
|
|
MaxClusterSize int `toml:"max_cluster_size" env:"ETCD_MAX_CLUSTER_SIZE"`
|
|
MaxResultBuffer int `toml:"max_result_buffer" env:"ETCD_MAX_RESULT_BUFFER"`
|
|
MaxRetryAttempts int `toml:"max_retry_attempts" env:"ETCD_MAX_RETRY_ATTEMPTS"`
|
|
RetryInterval float64 `toml:"retry_interval" env:"ETCD_RETRY_INTERVAL"`
|
|
Name string `toml:"name" env:"ETCD_NAME"`
|
|
Snapshot bool `toml:"snapshot" env:"ETCD_SNAPSHOT"`
|
|
SnapshotCount int `toml:"snapshot_count" env:"ETCD_SNAPSHOTCOUNT"`
|
|
ShowHelp bool
|
|
ShowVersion bool
|
|
Verbose bool `toml:"verbose" env:"ETCD_VERBOSE"`
|
|
VeryVerbose bool `toml:"very_verbose" env:"ETCD_VERY_VERBOSE"`
|
|
VeryVeryVerbose bool `toml:"very_very_verbose" env:"ETCD_VERY_VERY_VERBOSE"`
|
|
Peer struct {
|
|
Addr string `toml:"addr" env:"ETCD_PEER_ADDR"`
|
|
BindAddr string `toml:"bind_addr" env:"ETCD_PEER_BIND_ADDR"`
|
|
CAFile string `toml:"ca_file" env:"ETCD_PEER_CA_FILE"`
|
|
CertFile string `toml:"cert_file" env:"ETCD_PEER_CERT_FILE"`
|
|
KeyFile string `toml:"key_file" env:"ETCD_PEER_KEY_FILE"`
|
|
HeartbeatTimeout int `toml:"heartbeat_timeout" env:"ETCD_PEER_HEARTBEAT_TIMEOUT"`
|
|
ElectionTimeout int `toml:"election_timeout" env:"ETCD_PEER_ELECTION_TIMEOUT"`
|
|
}
|
|
strTrace string `toml:"trace" env:"ETCD_TRACE"`
|
|
GraphiteHost string `toml:"graphite_host" env:"ETCD_GRAPHITE_HOST"`
|
|
}
|
|
|
|
// New returns a Config initialized with default values.
|
|
func New() *Config {
|
|
c := new(Config)
|
|
c.SystemPath = DefaultSystemConfigPath
|
|
c.Addr = "127.0.0.1:4001"
|
|
c.MaxClusterSize = 9
|
|
c.MaxResultBuffer = 1024
|
|
c.MaxRetryAttempts = 3
|
|
c.RetryInterval = 10.0
|
|
c.Snapshot = true
|
|
c.SnapshotCount = 10000
|
|
c.Peer.Addr = "127.0.0.1:7001"
|
|
c.Peer.HeartbeatTimeout = defaultHeartbeatTimeout
|
|
c.Peer.ElectionTimeout = defaultElectionTimeout
|
|
return c
|
|
}
|
|
|
|
// Loads the configuration from the system config, command line config,
|
|
// environment variables, and finally command line arguments.
|
|
func (c *Config) Load(arguments []string) error {
|
|
var path string
|
|
f := flag.NewFlagSet("etcd", -1)
|
|
f.SetOutput(ioutil.Discard)
|
|
f.StringVar(&path, "config", "", "path to config file")
|
|
f.Parse(arguments)
|
|
|
|
// Load from system file.
|
|
if err := c.LoadSystemFile(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load from config file specified in arguments.
|
|
if path != "" {
|
|
if err := c.LoadFile(path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Load from the environment variables next.
|
|
if err := c.LoadEnv(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Load from command line flags.
|
|
if err := c.LoadFlags(arguments); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Loads peers if a peer file was specified.
|
|
if err := c.LoadPeersFile(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sanitize all the input fields.
|
|
if err := c.Sanitize(); err != nil {
|
|
return fmt.Errorf("sanitize: %v", err)
|
|
}
|
|
|
|
// Attempt cluster discovery
|
|
if c.Discovery != "" {
|
|
if err := c.handleDiscovery(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Force remove server configuration if specified.
|
|
if c.Force {
|
|
c.Reset()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Loads from the system etcd configuration file if it exists.
|
|
func (c *Config) LoadSystemFile() error {
|
|
if _, err := os.Stat(c.SystemPath); os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return c.LoadFile(c.SystemPath)
|
|
}
|
|
|
|
// Loads configuration from a file.
|
|
func (c *Config) LoadFile(path string) error {
|
|
_, err := toml.DecodeFile(path, &c)
|
|
return err
|
|
}
|
|
|
|
// LoadEnv loads the configuration via environment variables.
|
|
func (c *Config) LoadEnv() error {
|
|
if err := c.loadEnv(c); err != nil {
|
|
return err
|
|
}
|
|
if err := c.loadEnv(&c.Peer); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Config) loadEnv(target interface{}) error {
|
|
value := reflect.Indirect(reflect.ValueOf(target))
|
|
typ := value.Type()
|
|
for i := 0; i < typ.NumField(); i++ {
|
|
field := typ.Field(i)
|
|
|
|
// Retrieve environment variable.
|
|
v := strings.TrimSpace(os.Getenv(field.Tag.Get("env")))
|
|
if v == "" {
|
|
continue
|
|
}
|
|
|
|
// Set the appropriate type.
|
|
switch field.Type.Kind() {
|
|
case reflect.Bool:
|
|
value.Field(i).SetBool(v != "0" && v != "false")
|
|
case reflect.Int:
|
|
newValue, err := strconv.ParseInt(v, 10, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("Parse error: %s: %s", field.Tag.Get("env"), err)
|
|
}
|
|
value.Field(i).SetInt(newValue)
|
|
case reflect.String:
|
|
value.Field(i).SetString(v)
|
|
case reflect.Slice:
|
|
value.Field(i).Set(reflect.ValueOf(ustrings.TrimSplit(v, ",")))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Config) handleDiscovery() error {
|
|
p, err := discovery.Do(c.Discovery, c.Name, c.Peer.Addr)
|
|
|
|
// This is fatal, discovery encountered an unexpected error
|
|
// and we have no peer list.
|
|
if err != nil && len(c.Peers) == 0 {
|
|
log.Fatalf("Discovery failed and a backup peer list wasn't provided: %v", err)
|
|
return err
|
|
}
|
|
|
|
// Warn about errors coming from discovery, this isn't fatal
|
|
// since the user might have provided a peer list elsewhere.
|
|
if err != nil {
|
|
log.Warnf("Discovery encountered an error but a backup peer list (%v) was provided: %v", c.Peers, err)
|
|
}
|
|
|
|
for i := range p {
|
|
// Strip the scheme off of the peer if it has one
|
|
// TODO(bp): clean this up!
|
|
purl, err := url.Parse(p[i])
|
|
if err == nil {
|
|
p[i] = purl.Host
|
|
}
|
|
}
|
|
|
|
c.Peers = p
|
|
|
|
return nil
|
|
}
|
|
|
|
// Loads configuration from command line flags.
|
|
func (c *Config) LoadFlags(arguments []string) error {
|
|
var peers, cors, path string
|
|
|
|
f := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
|
f.SetOutput(ioutil.Discard)
|
|
|
|
f.BoolVar(&c.ShowHelp, "h", false, "")
|
|
f.BoolVar(&c.ShowHelp, "help", false, "")
|
|
f.BoolVar(&c.ShowVersion, "version", false, "")
|
|
|
|
f.BoolVar(&c.Force, "f", false, "")
|
|
f.BoolVar(&c.Force, "force", false, "")
|
|
|
|
f.BoolVar(&c.Verbose, "v", c.Verbose, "")
|
|
f.BoolVar(&c.VeryVerbose, "vv", c.VeryVerbose, "")
|
|
f.BoolVar(&c.VeryVeryVerbose, "vvv", c.VeryVeryVerbose, "")
|
|
|
|
f.StringVar(&peers, "peers", "", "")
|
|
f.StringVar(&c.PeersFile, "peers-file", c.PeersFile, "")
|
|
|
|
f.StringVar(&c.Name, "name", c.Name, "")
|
|
f.StringVar(&c.Addr, "addr", c.Addr, "")
|
|
f.StringVar(&c.Discovery, "discovery", c.Discovery, "")
|
|
f.StringVar(&c.BindAddr, "bind-addr", c.BindAddr, "")
|
|
f.StringVar(&c.Peer.Addr, "peer-addr", c.Peer.Addr, "")
|
|
f.StringVar(&c.Peer.BindAddr, "peer-bind-addr", c.Peer.BindAddr, "")
|
|
|
|
f.StringVar(&c.CAFile, "ca-file", c.CAFile, "")
|
|
f.StringVar(&c.CertFile, "cert-file", c.CertFile, "")
|
|
f.StringVar(&c.KeyFile, "key-file", c.KeyFile, "")
|
|
|
|
f.StringVar(&c.Peer.CAFile, "peer-ca-file", c.Peer.CAFile, "")
|
|
f.StringVar(&c.Peer.CertFile, "peer-cert-file", c.Peer.CertFile, "")
|
|
f.StringVar(&c.Peer.KeyFile, "peer-key-file", c.Peer.KeyFile, "")
|
|
|
|
f.StringVar(&c.DataDir, "data-dir", c.DataDir, "")
|
|
f.IntVar(&c.MaxResultBuffer, "max-result-buffer", c.MaxResultBuffer, "")
|
|
f.IntVar(&c.MaxRetryAttempts, "max-retry-attempts", c.MaxRetryAttempts, "")
|
|
f.Float64Var(&c.RetryInterval, "retry-interval", c.RetryInterval, "")
|
|
f.IntVar(&c.MaxClusterSize, "max-cluster-size", c.MaxClusterSize, "")
|
|
f.IntVar(&c.Peer.HeartbeatTimeout, "peer-heartbeat-timeout", c.Peer.HeartbeatTimeout, "")
|
|
f.IntVar(&c.Peer.ElectionTimeout, "peer-election-timeout", c.Peer.ElectionTimeout, "")
|
|
|
|
f.StringVar(&cors, "cors", "", "")
|
|
|
|
f.BoolVar(&c.Snapshot, "snapshot", c.Snapshot, "")
|
|
f.IntVar(&c.SnapshotCount, "snapshot-count", c.SnapshotCount, "")
|
|
f.StringVar(&c.CPUProfileFile, "cpuprofile", "", "")
|
|
|
|
f.StringVar(&c.strTrace, "trace", "", "")
|
|
f.StringVar(&c.GraphiteHost, "graphite-host", "", "")
|
|
|
|
// BEGIN IGNORED FLAGS
|
|
f.StringVar(&path, "config", "", "")
|
|
// BEGIN IGNORED FLAGS
|
|
|
|
// BEGIN DEPRECATED FLAGS
|
|
f.StringVar(&peers, "C", "", "(deprecated)")
|
|
f.StringVar(&c.PeersFile, "CF", c.PeersFile, "(deprecated)")
|
|
f.StringVar(&c.Name, "n", c.Name, "(deprecated)")
|
|
f.StringVar(&c.Addr, "c", c.Addr, "(deprecated)")
|
|
f.StringVar(&c.BindAddr, "cl", c.BindAddr, "(deprecated)")
|
|
f.StringVar(&c.Peer.Addr, "s", c.Peer.Addr, "(deprecated)")
|
|
f.StringVar(&c.Peer.BindAddr, "sl", c.Peer.BindAddr, "(deprecated)")
|
|
f.StringVar(&c.Peer.CAFile, "serverCAFile", c.Peer.CAFile, "(deprecated)")
|
|
f.StringVar(&c.Peer.CertFile, "serverCert", c.Peer.CertFile, "(deprecated)")
|
|
f.StringVar(&c.Peer.KeyFile, "serverKey", c.Peer.KeyFile, "(deprecated)")
|
|
f.StringVar(&c.CAFile, "clientCAFile", c.CAFile, "(deprecated)")
|
|
f.StringVar(&c.CertFile, "clientCert", c.CertFile, "(deprecated)")
|
|
f.StringVar(&c.KeyFile, "clientKey", c.KeyFile, "(deprecated)")
|
|
f.StringVar(&c.DataDir, "d", c.DataDir, "(deprecated)")
|
|
f.IntVar(&c.MaxResultBuffer, "m", c.MaxResultBuffer, "(deprecated)")
|
|
f.IntVar(&c.MaxRetryAttempts, "r", c.MaxRetryAttempts, "(deprecated)")
|
|
f.IntVar(&c.MaxClusterSize, "maxsize", c.MaxClusterSize, "(deprecated)")
|
|
f.IntVar(&c.SnapshotCount, "snapshotCount", c.SnapshotCount, "(deprecated)")
|
|
// END DEPRECATED FLAGS
|
|
|
|
if err := f.Parse(arguments); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Print deprecation warnings on STDERR.
|
|
f.Visit(func(f *flag.Flag) {
|
|
if len(newFlagNameLookup[f.Name]) > 0 {
|
|
fmt.Fprintf(os.Stderr, "[deprecated] use -%s, not -%s\n", newFlagNameLookup[f.Name], f.Name)
|
|
}
|
|
})
|
|
|
|
// Convert some parameters to lists.
|
|
if peers != "" {
|
|
c.Peers = ustrings.TrimSplit(peers, ",")
|
|
}
|
|
if cors != "" {
|
|
c.CorsOrigins = ustrings.TrimSplit(cors, ",")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadPeersFile loads the peers listed in the peers file.
|
|
func (c *Config) LoadPeersFile() error {
|
|
if c.PeersFile == "" {
|
|
return nil
|
|
}
|
|
|
|
b, err := ioutil.ReadFile(c.PeersFile)
|
|
if err != nil {
|
|
return fmt.Errorf("Peers file error: %s", err)
|
|
}
|
|
c.Peers = ustrings.TrimSplit(string(b), ",")
|
|
|
|
return nil
|
|
}
|
|
|
|
// DataDirFromName sets the data dir from a machine name and issue a warning
|
|
// that etcd is guessing.
|
|
func (c *Config) DataDirFromName() {
|
|
c.DataDir = c.Name + ".etcd"
|
|
log.Warnf("Using the directory %s as the etcd curation directory because a directory was not specified. ", c.DataDir)
|
|
|
|
return
|
|
}
|
|
|
|
// NameFromHostname sets the machine name from the hostname. This is to help
|
|
// people get started without thinking up a name.
|
|
func (c *Config) NameFromHostname() {
|
|
host, err := os.Hostname()
|
|
if err != nil && host == "" {
|
|
log.Fatal("Node name required and hostname not set. e.g. '-name=name'")
|
|
}
|
|
c.Name = host
|
|
}
|
|
|
|
// Reset removes all server configuration files.
|
|
func (c *Config) Reset() error {
|
|
if err := os.RemoveAll(filepath.Join(c.DataDir, "log")); err != nil {
|
|
return err
|
|
}
|
|
if err := os.RemoveAll(filepath.Join(c.DataDir, "conf")); err != nil {
|
|
return err
|
|
}
|
|
if err := os.RemoveAll(filepath.Join(c.DataDir, "snapshot")); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Sanitize cleans the input fields.
|
|
func (c *Config) Sanitize() error {
|
|
var err error
|
|
|
|
// Sanitize the URLs first.
|
|
if c.Addr, err = sanitizeURL(c.Addr, c.EtcdTLSInfo().Scheme()); err != nil {
|
|
return fmt.Errorf("Advertised URL: %s", err)
|
|
}
|
|
if c.BindAddr, err = sanitizeBindAddr(c.BindAddr, c.Addr); err != nil {
|
|
return fmt.Errorf("Listen Host: %s", err)
|
|
}
|
|
if c.Peer.Addr, err = sanitizeURL(c.Peer.Addr, c.PeerTLSInfo().Scheme()); err != nil {
|
|
return fmt.Errorf("Peer Advertised URL: %s", err)
|
|
}
|
|
if c.Peer.BindAddr, err = sanitizeBindAddr(c.Peer.BindAddr, c.Peer.Addr); err != nil {
|
|
return fmt.Errorf("Peer Listen Host: %s", err)
|
|
}
|
|
|
|
// Only guess the machine name if there is no data dir specified
|
|
// because the info file should have our name
|
|
if c.Name == "" && c.DataDir == "" {
|
|
c.NameFromHostname()
|
|
}
|
|
|
|
if c.DataDir == "" && c.Name != "" && !c.ShowVersion && !c.ShowHelp {
|
|
c.DataDirFromName()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EtcdTLSInfo retrieves a TLSInfo object for the etcd server
|
|
func (c *Config) EtcdTLSInfo() server.TLSInfo {
|
|
return server.TLSInfo{
|
|
CAFile: c.CAFile,
|
|
CertFile: c.CertFile,
|
|
KeyFile: c.KeyFile,
|
|
}
|
|
}
|
|
|
|
// PeerRaftInfo retrieves a TLSInfo object for the peer server.
|
|
func (c *Config) PeerTLSInfo() server.TLSInfo {
|
|
return server.TLSInfo{
|
|
CAFile: c.Peer.CAFile,
|
|
CertFile: c.Peer.CertFile,
|
|
KeyFile: c.Peer.KeyFile,
|
|
}
|
|
}
|
|
|
|
// MetricsBucketName generates the name that should be used for a
|
|
// corresponding MetricsBucket object
|
|
func (c *Config) MetricsBucketName() string {
|
|
return fmt.Sprintf("etcd.%s", c.Name)
|
|
}
|
|
|
|
// Trace determines if any trace-level information should be emitted
|
|
func (c *Config) Trace() bool {
|
|
return c.strTrace == "*"
|
|
}
|
|
|
|
// sanitizeURL will cleanup a host string in the format hostname[:port] and
|
|
// attach a schema.
|
|
func sanitizeURL(host string, defaultScheme string) (string, error) {
|
|
// Blank URLs are fine input, just return it
|
|
if len(host) == 0 {
|
|
return host, nil
|
|
}
|
|
|
|
p, err := url.Parse(host)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Make sure the host is in Host:Port format
|
|
_, _, err = net.SplitHostPort(host)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
p = &url.URL{Host: host, Scheme: defaultScheme}
|
|
return p.String(), nil
|
|
}
|
|
|
|
// sanitizeBindAddr cleans up the BindAddr parameter and appends a port
|
|
// if necessary based on the advertised port.
|
|
func sanitizeBindAddr(bindAddr string, addr string) (string, error) {
|
|
aurl, err := url.Parse(addr)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// If it is a valid host:port simply return with no further checks.
|
|
bhost, bport, err := net.SplitHostPort(bindAddr)
|
|
if err == nil && bhost != "" {
|
|
return bindAddr, nil
|
|
}
|
|
|
|
// SplitHostPort makes the host optional, but we don't want that.
|
|
if bhost == "" && bport != "" {
|
|
return "", fmt.Errorf("IP required can't use a port only")
|
|
}
|
|
|
|
// bindAddr doesn't have a port if we reach here so take the port from the
|
|
// advertised URL.
|
|
_, aport, err := net.SplitHostPort(aurl.Host)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return net.JoinHostPort(bindAddr, aport), nil
|
|
}
|