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

When -version or -help are given, don't warn about having to derive the data directory name. Print warnings about deprecated options on separate lines so the log isn't screwy.
505 lines
14 KiB
Go
505 lines
14 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"github.com/coreos/etcd/log"
|
|
)
|
|
|
|
// 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"`
|
|
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"`
|
|
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"`
|
|
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"`
|
|
}
|
|
}
|
|
|
|
// NewConfig returns a Config initialized with default values.
|
|
func NewConfig() *Config {
|
|
c := new(Config)
|
|
c.SystemPath = DefaultSystemConfigPath
|
|
c.Addr = "127.0.0.1:4001"
|
|
c.MaxClusterSize = 9
|
|
c.MaxResultBuffer = 1024
|
|
c.MaxRetryAttempts = 3
|
|
c.SnapshotCount = 10000
|
|
c.Peer.Addr = "127.0.0.1:7001"
|
|
c.Peer.HeartbeatTimeout = 0
|
|
c.Peer.ElectionTimeout = 0
|
|
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)
|
|
}
|
|
|
|
// 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(trimsplit(v, ",")))
|
|
}
|
|
}
|
|
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.Verbose, "")
|
|
|
|
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.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.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", "", "")
|
|
|
|
// 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 = trimsplit(peers, ",")
|
|
}
|
|
if cors != "" {
|
|
c.CorsOrigins = 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 = 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, "info")); err != nil {
|
|
return err
|
|
}
|
|
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
|
|
}
|
|
|
|
// Reads the info file from the file system or initializes it based on the config.
|
|
func (c *Config) Info() (*Info, error) {
|
|
info := &Info{}
|
|
path := filepath.Join(c.DataDir, "info")
|
|
|
|
// Open info file and read it out.
|
|
f, err := os.Open(path)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return nil, err
|
|
} else if f != nil {
|
|
defer f.Close()
|
|
if err := json.NewDecoder(f).Decode(&info); err != nil {
|
|
return nil, err
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
// If the file doesn't exist then initialize it.
|
|
info.Name = strings.TrimSpace(c.Name)
|
|
info.EtcdURL = c.Addr
|
|
info.EtcdListenHost = c.BindAddr
|
|
info.RaftURL = c.Peer.Addr
|
|
info.RaftListenHost = c.Peer.BindAddr
|
|
info.EtcdTLS = c.TLSInfo()
|
|
info.RaftTLS = c.PeerTLSInfo()
|
|
|
|
// Write to file.
|
|
f, err = os.Create(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
if err := json.NewEncoder(f).Encode(info); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// Sanitize cleans the input fields.
|
|
func (c *Config) Sanitize() error {
|
|
tlsConfig, err := c.TLSConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
peerTlsConfig, err := c.PeerTLSConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sanitize the URLs first.
|
|
if c.Addr, err = sanitizeURL(c.Addr, tlsConfig.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, peerTlsConfig.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
|
|
}
|
|
|
|
// TLSInfo retrieves a TLSInfo object for the client server.
|
|
func (c *Config) TLSInfo() TLSInfo {
|
|
return TLSInfo{
|
|
CAFile: c.CAFile,
|
|
CertFile: c.CertFile,
|
|
KeyFile: c.KeyFile,
|
|
}
|
|
}
|
|
|
|
// ClientTLSConfig generates the TLS configuration for the client server.
|
|
func (c *Config) TLSConfig() (TLSConfig, error) {
|
|
return c.TLSInfo().Config()
|
|
}
|
|
|
|
// PeerTLSInfo retrieves a TLSInfo object for the peer server.
|
|
func (c *Config) PeerTLSInfo() TLSInfo {
|
|
return TLSInfo{
|
|
CAFile: c.Peer.CAFile,
|
|
CertFile: c.Peer.CertFile,
|
|
KeyFile: c.Peer.KeyFile,
|
|
}
|
|
}
|
|
|
|
// PeerTLSConfig generates the TLS configuration for the peer server.
|
|
func (c *Config) PeerTLSConfig() (TLSConfig, error) {
|
|
return c.PeerTLSInfo().Config()
|
|
}
|
|
|
|
// 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
|
|
}
|