mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00
407 lines
12 KiB
Go
407 lines
12 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
)
|
|
|
|
// The default location for the etcd configuration file.
|
|
const DefaultSystemConfigPath = "/etc/etcd/etcd.conf"
|
|
|
|
// Config represents the server configuration.
|
|
type Config struct {
|
|
SystemPath string
|
|
|
|
AdvertisedUrl string `toml:"advertised_url" env:"ETCD_ADVERTISED_URL"`
|
|
CAFile string `toml:"ca_file" env:"ETCD_CA_FILE"`
|
|
CertFile string `toml:"cert_file" env:"ETCD_CERT_FILE"`
|
|
Cors []string `toml:"cors" env:"ETCD_CORS"`
|
|
DataDir string `toml:"datadir" env:"ETCD_DATADIR"`
|
|
KeyFile string `toml:"key_file" env:"ETCD_KEY_FILE"`
|
|
ListenHost string `toml:"listen_host" env:"ETCD_LISTEN_HOST"`
|
|
Machines []string `toml:"machines" env:"ETCD_MACHINES"`
|
|
MachinesFile string `toml:"machines_file" env:"ETCD_MACHINES_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"`
|
|
SnapCount int `toml:"snapshot_count" env:"ETCD_SNAPSHOTCOUNT"`
|
|
Verbose bool `toml:"verbose" env:"ETCD_VERBOSE"`
|
|
VeryVerbose bool `toml:"very_verbose" env:"ETCD_VERY_VERBOSE"`
|
|
WebURL string `toml:"web_url" env:"ETCD_WEB_URL"`
|
|
|
|
Peer struct {
|
|
AdvertisedUrl string `toml:"advertised_url" env:"ETCD_PEER_ADVERTISED_URL"`
|
|
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"`
|
|
ListenHost string `toml:"listen_host" env:"ETCD_PEER_LISTEN_HOST"`
|
|
}
|
|
}
|
|
|
|
// NewConfig returns a Config initialized with default values.
|
|
func NewConfig() *Config {
|
|
c := new(Config)
|
|
c.SystemPath = DefaultSystemConfigPath
|
|
c.AdvertisedUrl = "127.0.0.1:4001"
|
|
c.AdvertisedUrl = "127.0.0.1:4001"
|
|
c.DataDir = "."
|
|
c.MaxClusterSize = 9
|
|
c.MaxResultBuffer = 1024
|
|
c.MaxRetryAttempts = 3
|
|
c.Peer.AdvertisedUrl = "127.0.0.1:7001"
|
|
c.SnapCount = 10000
|
|
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 machines if a machine file was specified.
|
|
if err := c.LoadMachineFile(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Sanitize all the input fields.
|
|
if err := c.Sanitize(); err != nil {
|
|
return fmt.Errorf("sanitize:", err)
|
|
}
|
|
|
|
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 machines, cors string
|
|
var force bool
|
|
|
|
f := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
|
|
|
|
f.BoolVar(&force, "f", false, "force new node configuration if existing is found (WARNING: data loss!)")
|
|
|
|
f.BoolVar(&c.Verbose, "v", c.Verbose, "verbose logging")
|
|
f.BoolVar(&c.VeryVerbose, "vv", c.Verbose, "very verbose logging")
|
|
|
|
f.StringVar(&machines, "C", "", "the ip address and port of a existing machines in the cluster, sepearate by comma")
|
|
f.StringVar(&c.MachinesFile, "CF", c.MachinesFile, "the file contains a list of existing machines in the cluster, seperate by comma")
|
|
|
|
f.StringVar(&c.Name, "n", c.Name, "the node name (required)")
|
|
f.StringVar(&c.AdvertisedUrl, "c", c.AdvertisedUrl, "the advertised public hostname:port for etcd client communication")
|
|
f.StringVar(&c.Peer.AdvertisedUrl, "s", c.Peer.AdvertisedUrl, "the advertised public hostname:port for raft server communication")
|
|
f.StringVar(&c.ListenHost, "cl", c.ListenHost, "the listening hostname for etcd client communication (defaults to advertised ip)")
|
|
f.StringVar(&c.Peer.ListenHost, "sl", c.Peer.ListenHost, "the listening hostname for raft server communication (defaults to advertised ip)")
|
|
f.StringVar(&c.WebURL, "w", c.WebURL, "the hostname:port of web interface")
|
|
|
|
f.StringVar(&c.Peer.CAFile, "serverCAFile", c.Peer.CAFile, "the path of the CAFile")
|
|
f.StringVar(&c.Peer.CertFile, "serverCert", c.Peer.CertFile, "the cert file of the server")
|
|
f.StringVar(&c.Peer.KeyFile, "serverKey", c.Peer.KeyFile, "the key file of the server")
|
|
|
|
f.StringVar(&c.CAFile, "clientCAFile", c.CAFile, "the path of the client CAFile")
|
|
f.StringVar(&c.CertFile, "clientCert", c.CertFile, "the cert file of the client")
|
|
f.StringVar(&c.KeyFile, "clientKey", c.KeyFile, "the key file of the client")
|
|
|
|
f.StringVar(&c.DataDir, "d", c.DataDir, "the directory to store log and snapshot")
|
|
f.IntVar(&c.MaxResultBuffer, "m", c.MaxResultBuffer, "the max size of result buffer")
|
|
f.IntVar(&c.MaxRetryAttempts, "r", c.MaxRetryAttempts, "the max retry attempts when trying to join a cluster")
|
|
f.IntVar(&c.MaxClusterSize, "maxsize", c.MaxClusterSize, "the max size of the cluster")
|
|
f.StringVar(&cors, "cors", "", "whitelist origins for cross-origin resource sharing (e.g. '*' or 'http://localhost:8001,etc')")
|
|
|
|
f.BoolVar(&c.Snapshot, "snapshot", c.Snapshot, "open or close snapshot")
|
|
f.IntVar(&c.SnapCount, "snapshotCount", c.SnapCount, "save the in memory logs and states to a snapshot file after snapCount transactions")
|
|
|
|
// These flags are ignored since they were already parsed.
|
|
var path string
|
|
f.StringVar(&path, "config", "", "path to config file")
|
|
|
|
f.Parse(arguments)
|
|
|
|
// Convert some parameters to lists.
|
|
if machines != "" {
|
|
c.Machines = trimsplit(machines, ",")
|
|
}
|
|
if cors != "" {
|
|
c.Cors = trimsplit(cors, ",")
|
|
}
|
|
|
|
// Force remove server configuration if specified.
|
|
if force {
|
|
c.Reset()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadMachineFile loads the machines listed in the machine file.
|
|
func (c *Config) LoadMachineFile() error {
|
|
if c.MachinesFile == "" {
|
|
return nil
|
|
}
|
|
|
|
b, err := ioutil.ReadFile(c.MachinesFile)
|
|
if err != nil {
|
|
return fmt.Errorf("Machines file error: %s", err)
|
|
}
|
|
c.Machines = trimsplit(string(b), ",")
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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.AdvertisedUrl
|
|
info.EtcdListenHost = c.ListenHost
|
|
info.RaftURL = c.Peer.AdvertisedUrl
|
|
info.RaftListenHost = c.Peer.ListenHost
|
|
info.WebURL = c.WebURL
|
|
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.AdvertisedUrl, err = sanitizeURL(c.AdvertisedUrl, tlsConfig.Scheme); err != nil {
|
|
return fmt.Errorf("Advertised URL: %s", err)
|
|
}
|
|
if c.ListenHost, err = sanitizeListenHost(c.ListenHost, c.AdvertisedUrl); err != nil {
|
|
return fmt.Errorf("Listen Host: %s", err)
|
|
}
|
|
if c.WebURL, err = sanitizeURL(c.WebURL, "http"); err != nil {
|
|
return fmt.Errorf("Web URL: %s", err)
|
|
}
|
|
if c.Peer.AdvertisedUrl, err = sanitizeURL(c.Peer.AdvertisedUrl, peerTlsConfig.Scheme); err != nil {
|
|
return fmt.Errorf("Peer Advertised URL: %s", err)
|
|
}
|
|
if c.Peer.ListenHost, err = sanitizeListenHost(c.Peer.ListenHost, c.Peer.AdvertisedUrl); err != nil {
|
|
return fmt.Errorf("Peer Listen Host: %s", err)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// sanitizeListenHost cleans up the ListenHost parameter and appends a port
|
|
// if necessary based on the advertised port.
|
|
func sanitizeListenHost(listen string, advertised string) (string, error) {
|
|
aurl, err := url.Parse(advertised)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
ahost, aport, err := net.SplitHostPort(aurl.Host)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// If the listen host isn't set use the advertised host
|
|
if listen == "" {
|
|
listen = ahost
|
|
}
|
|
|
|
return net.JoinHostPort(listen, aport), nil
|
|
}
|