Browse Source

Merge pull request #1947 from barakmich/dns_bootstrap

add capability to bootstrap from DNS SRV
Barak Michener 11 years ago
parent
commit
4f2d35679e
2 changed files with 163 additions and 2 deletions
  1. 81 2
      etcdmain/etcd.go
  2. 82 0
      etcdmain/etcd_test.go

+ 81 - 2
etcdmain/etcd.go

@@ -59,6 +59,7 @@ var (
 	name            = fs.String("name", "default", "Unique human-readable name for this node")
 	dir             = fs.String("data-dir", "", "Path to the data directory")
 	durl            = fs.String("discovery", "", "Discovery service used to bootstrap the cluster")
+	dnsCluster      = fs.String("discovery-srv", "", "Bootstrap initial cluster via DNS domain")
 	dproxy          = fs.String("discovery-proxy", "", "HTTP proxy to use for traffic to discovery service")
 	snapCount       = fs.Uint64("snapshot-count", etcdserver.DefaultSnapCount, "Number of committed transactions to trigger a snapshot")
 	printVersion    = fs.Bool("version", false, "Print the version and exit")
@@ -102,6 +103,8 @@ var (
 		"v",
 		"vv",
 	}
+
+	lookupSRV = net.LookupSRV
 )
 
 func init() {
@@ -403,8 +406,14 @@ func setupCluster(apurls []url.URL) (*etcdserver.Cluster, error) {
 	fs.Visit(func(f *flag.Flag) {
 		set[f.Name] = true
 	})
-	if set["discovery"] && set["initial-cluster"] {
-		return nil, fmt.Errorf("both discovery and bootstrap-config are set")
+	nSet := 0
+	for _, v := range []bool{set["discovery"], set["inital-cluster"], set["discovery-srv"]} {
+		if v {
+			nSet += 1
+		}
+	}
+	if nSet > 1 {
+		return nil, fmt.Errorf("multiple discovery or bootstrap flags are set. Choose one of \"discovery\", \"initial-cluster\", or \"discovery-srv\"")
 	}
 	var cls *etcdserver.Cluster
 	var err error
@@ -414,6 +423,12 @@ func setupCluster(apurls []url.URL) (*etcdserver.Cluster, error) {
 		// self's advertised peer URLs
 		clusterStr := genClusterString(*name, apurls)
 		cls, err = etcdserver.NewClusterFromString(*durl, clusterStr)
+	case set["discovery-srv"]:
+		clusterStr, clusterToken, err := genDNSClusterString(*initialClusterToken, apurls)
+		if err != nil {
+			return nil, err
+		}
+		cls, err = etcdserver.NewClusterFromString(clusterToken, clusterStr)
 	case set["initial-cluster"]:
 		fallthrough
 	default:
@@ -430,3 +445,67 @@ func genClusterString(name string, urls types.URLs) string {
 	}
 	return strings.Join(addrs, ",")
 }
+
+// TODO(barakmich): Currently ignores priority and weight (as they don't make as much sense for a bootstrap)
+// Also doesn't do any lookups for the token (though it could)
+// Also sees each entry as a separate instance.
+func genDNSClusterString(defaultToken string, apurls types.URLs) (string, string, error) {
+	stringParts := make([]string, 0)
+	tempName := int(0)
+	tcpAPUrls := make([]string, 0)
+
+	// First, resolve the apurls
+	for _, url := range apurls {
+		tcpAddr, err := net.ResolveTCPAddr("tcp", url.Host)
+		if err != nil {
+			log.Printf("etcd: Couldn't resolve host %s during SRV discovery", url.Host)
+			return "", "", err
+		}
+		tcpAPUrls = append(tcpAPUrls, tcpAddr.String())
+	}
+
+	updateNodeMap := func(service, prefix string) error {
+		_, addrs, err := lookupSRV(service, "tcp", *dnsCluster)
+		if err != nil {
+			return err
+		}
+		for _, srv := range addrs {
+			host := net.JoinHostPort(srv.Target, fmt.Sprintf("%d", srv.Port))
+			tcpAddr, err := net.ResolveTCPAddr("tcp", host)
+			if err != nil {
+				log.Printf("etcd: Couldn't resolve host %s during SRV discovery", host)
+				continue
+			}
+			n := ""
+			for _, url := range tcpAPUrls {
+				if url == tcpAddr.String() {
+					n = *name
+				}
+			}
+			if n == "" {
+				n = fmt.Sprintf("%d", tempName)
+				tempName += 1
+			}
+			stringParts = append(stringParts, fmt.Sprintf("%s=%s%s", n, prefix, tcpAddr.String()))
+			log.Printf("etcd: Got bootstrap from DNS for %s at host %s to %s%s", service, host, prefix, tcpAddr.String())
+		}
+		return nil
+	}
+
+	failCount := 0
+	err := updateNodeMap("etcd-server-ssl", "https://")
+	if err != nil {
+		log.Printf("etcd: Error querying DNS SRV records for _etcd-server-ssl. Error: %s.", err)
+		failCount += 1
+	}
+	err = updateNodeMap("etcd-server", "http://")
+	if err != nil {
+		log.Printf("etcd: Error querying DNS SRV records for _etcd-server. Error: %s.", err)
+		failCount += 1
+	}
+	if failCount == 2 {
+		log.Printf("etcd: Too many errors querying DNS SRV records. Failing discovery.")
+		return "", "", err
+	}
+	return strings.Join(stringParts, ","), defaultToken, nil
+}

+ 82 - 0
etcdmain/etcd_test.go

@@ -17,6 +17,8 @@
 package etcdmain
 
 import (
+	"errors"
+	"net"
 	"net/url"
 	"testing"
 
@@ -24,6 +26,9 @@ import (
 )
 
 func mustNewURLs(t *testing.T, urls []string) []url.URL {
+	if urls == nil {
+		return nil
+	}
 	u, err := types.NewURLs(urls)
 	if err != nil {
 		t.Fatalf("unexpected new urls error: %v", err)
@@ -54,3 +59,80 @@ func TestGenClusterString(t *testing.T) {
 		}
 	}
 }
+
+func TestGenDNSClusterString(t *testing.T) {
+	oldname := *name
+	*name = "dnsClusterTest"
+	defer func() { *name = oldname }()
+	tests := []struct {
+		withSSL    []*net.SRV
+		withoutSSL []*net.SRV
+		urls       []string
+		expected   string
+	}{
+		{
+			[]*net.SRV{},
+			[]*net.SRV{},
+			nil,
+			"",
+		},
+		{
+			[]*net.SRV{
+				&net.SRV{Target: "10.0.0.1", Port: 2480},
+				&net.SRV{Target: "10.0.0.2", Port: 2480},
+				&net.SRV{Target: "10.0.0.3", Port: 2480},
+			},
+			[]*net.SRV{},
+			nil,
+			"0=https://10.0.0.1:2480,1=https://10.0.0.2:2480,2=https://10.0.0.3:2480",
+		},
+		{
+			[]*net.SRV{
+				&net.SRV{Target: "10.0.0.1", Port: 2480},
+				&net.SRV{Target: "10.0.0.2", Port: 2480},
+				&net.SRV{Target: "10.0.0.3", Port: 2480},
+			},
+			[]*net.SRV{
+				&net.SRV{Target: "10.0.0.1", Port: 7001},
+			},
+			nil,
+			"0=https://10.0.0.1:2480,1=https://10.0.0.2:2480,2=https://10.0.0.3:2480,3=http://10.0.0.1:7001",
+		},
+		{
+			[]*net.SRV{
+				&net.SRV{Target: "10.0.0.1", Port: 2480},
+				&net.SRV{Target: "10.0.0.2", Port: 2480},
+				&net.SRV{Target: "10.0.0.3", Port: 2480},
+			},
+			[]*net.SRV{
+				&net.SRV{Target: "10.0.0.1", Port: 7001},
+			},
+			[]string{"https://10.0.0.1:2480"},
+			"dnsClusterTest=https://10.0.0.1:2480,0=https://10.0.0.2:2480,1=https://10.0.0.3:2480,2=http://10.0.0.1:7001",
+		},
+	}
+
+	for i, tt := range tests {
+		lookupSRV = func(service string, proto string, domain string) (string, []*net.SRV, error) {
+			if service == "etcd-server-ssl" {
+				return "", tt.withSSL, nil
+			}
+			if service == "etcd-server" {
+				return "", tt.withoutSSL, nil
+			}
+			return "", nil, errors.New("Unkown service in mock")
+		}
+		defer func() { lookupSRV = net.LookupSRV }()
+		urls := mustNewURLs(t, tt.urls)
+		str, token, err := genDNSClusterString("token", urls)
+		if err != nil {
+			t.Fatalf("%d: err: %#v", i, err)
+		}
+		if token != "token" {
+			t.Errorf("%d: token: %s", i, token)
+		}
+		if str != tt.expected {
+			t.Errorf("#%d: cluster = %s, want %s", i, str, tt.expected)
+		}
+	}
+}