浏览代码

Merge pull request #231 from eyakubovich/srv-sock-activation

Add socket activation support for server
Eugene Yakubovich 9 年之前
父节点
当前提交
00c99f545e

+ 5 - 0
Godeps/Godeps.json

@@ -30,6 +30,11 @@
 			"Comment": "v2.0.0-3-g0424b5f",
 			"Rev": "0424b5f86ef0ca57a5309c599f74bbb3e97ecd9d"
 		},
+		{
+			"ImportPath": "github.com/coreos/go-systemd/activation",
+			"Comment": "v2-26-ga606a1e",
+			"Rev": "a606a1e936df81b70d85448221c7b1c6d8a74ef1"
+		},
 		{
 			"ImportPath": "github.com/coreos/go-systemd/daemon",
 			"Comment": "v2-26-ga606a1e",

+ 56 - 0
Godeps/_workspace/src/github.com/coreos/go-systemd/activation/files.go

@@ -0,0 +1,56 @@
+/*
+Copyright 2013 CoreOS Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package activation implements primitives for systemd socket activation.
+package activation
+
+import (
+	"os"
+	"strconv"
+	"syscall"
+)
+
+// based on: https://gist.github.com/alberts/4640792
+const (
+	listenFdsStart = 3
+)
+
+func Files(unsetEnv bool) []*os.File {
+	if unsetEnv {
+		// there is no way to unset env in golang os package for now
+		// https://code.google.com/p/go/issues/detail?id=6423
+		defer os.Setenv("LISTEN_PID", "")
+		defer os.Setenv("LISTEN_FDS", "")
+	}
+
+	pid, err := strconv.Atoi(os.Getenv("LISTEN_PID"))
+	if err != nil || pid != os.Getpid() {
+		return nil
+	}
+
+	nfds, err := strconv.Atoi(os.Getenv("LISTEN_FDS"))
+	if err != nil || nfds == 0 {
+		return nil
+	}
+
+	var files []*os.File
+	for fd := listenFdsStart; fd < listenFdsStart+nfds; fd++ {
+		syscall.CloseOnExec(fd)
+		files = append(files, os.NewFile(uintptr(fd), "LISTEN_FD_"+strconv.Itoa(fd)))
+	}
+
+	return files
+}

+ 84 - 0
Godeps/_workspace/src/github.com/coreos/go-systemd/activation/files_test.go

@@ -0,0 +1,84 @@
+/*
+Copyright 2013 CoreOS Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package activation
+
+import (
+	"bytes"
+	"io"
+	"os"
+	"os/exec"
+	"testing"
+)
+
+// correctStringWritten fails the text if the correct string wasn't written
+// to the other side of the pipe.
+func correctStringWritten(t *testing.T, r *os.File, expected string) bool {
+	bytes := make([]byte, len(expected))
+	io.ReadAtLeast(r, bytes, len(expected))
+
+	if string(bytes) != expected {
+		t.Fatalf("Unexpected string %s", string(bytes))
+	}
+
+	return true
+}
+
+// TestActivation forks out a copy of activation.go example and reads back two
+// strings from the pipes that are passed in.
+func TestActivation(t *testing.T) {
+	cmd := exec.Command("go", "run", "../examples/activation/activation.go")
+
+	r1, w1, _ := os.Pipe()
+	r2, w2, _ := os.Pipe()
+	cmd.ExtraFiles = []*os.File{
+		w1,
+		w2,
+	}
+
+	cmd.Env = os.Environ()
+	cmd.Env = append(cmd.Env, "LISTEN_FDS=2", "FIX_LISTEN_PID=1")
+
+	err := cmd.Run()
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	correctStringWritten(t, r1, "Hello world")
+	correctStringWritten(t, r2, "Goodbye world")
+}
+
+func TestActivationNoFix(t *testing.T) {
+	cmd := exec.Command("go", "run", "../examples/activation/activation.go")
+	cmd.Env = os.Environ()
+	cmd.Env = append(cmd.Env, "LISTEN_FDS=2")
+
+	out, _ := cmd.CombinedOutput()
+	if bytes.Contains(out, []byte("No files")) == false {
+		t.Fatalf("Child didn't error out as expected")
+	}
+}
+
+func TestActivationNoFiles(t *testing.T) {
+	cmd := exec.Command("go", "run", "../examples/activation/activation.go")
+	cmd.Env = os.Environ()
+	cmd.Env = append(cmd.Env, "LISTEN_FDS=0", "FIX_LISTEN_PID=1")
+
+	out, _ := cmd.CombinedOutput()
+	if bytes.Contains(out, []byte("No files")) == false {
+		t.Fatalf("Child didn't error out as expected")
+	}
+}

+ 42 - 0
Godeps/_workspace/src/github.com/coreos/go-systemd/activation/listeners.go

@@ -0,0 +1,42 @@
+/*
+Copyright 2014 CoreOS Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package activation
+
+import (
+	"net"
+)
+
+// Listeners returns a slice containing a net.Listener for each matching socket type
+// passed to this process.
+//
+// The order of the file descriptors is preserved in the returned slice.
+// Nil values are used to fill any gaps. For example if systemd were to return file descriptors
+// corresponding with "udp, tcp, tcp", then the slice would contain {nil, net.Listener, net.Listener}
+func Listeners(unsetEnv bool) ([]net.Listener, error) {
+	files := Files(unsetEnv)
+	listeners := make([]net.Listener, 0)
+
+	for i := 0; i < len(files); i++ {
+		if pc, err := net.FileListener(files[i]); err == nil {
+			listeners = append(listeners, pc)
+			continue
+		} else {
+			listeners = append(listeners, nil)
+		}
+	}
+	return listeners, nil
+}

+ 88 - 0
Godeps/_workspace/src/github.com/coreos/go-systemd/activation/listeners_test.go

@@ -0,0 +1,88 @@
+/*
+Copyright 2014 CoreOS Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package activation
+
+import (
+	"io"
+	"net"
+	"os"
+	"os/exec"
+	"testing"
+)
+
+// correctStringWritten fails the text if the correct string wasn't written
+// to the other side of the pipe.
+func correctStringWrittenNet(t *testing.T, r net.Conn, expected string) bool {
+	bytes := make([]byte, len(expected))
+	io.ReadAtLeast(r, bytes, len(expected))
+
+	if string(bytes) != expected {
+		t.Fatalf("Unexpected string %s", string(bytes))
+	}
+
+	return true
+}
+
+// TestActivation forks out a copy of activation.go example and reads back two
+// strings from the pipes that are passed in.
+func TestListeners(t *testing.T) {
+	cmd := exec.Command("go", "run", "../examples/activation/listen.go")
+
+	l1, err := net.Listen("tcp", ":9999")
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	l2, err := net.Listen("tcp", ":1234")
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	t1 := l1.(*net.TCPListener)
+	t2 := l2.(*net.TCPListener)
+
+	f1, _ := t1.File()
+	f2, _ := t2.File()
+
+	cmd.ExtraFiles = []*os.File{
+		f1,
+		f2,
+	}
+
+	r1, err := net.Dial("tcp", "127.0.0.1:9999")
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	r1.Write([]byte("Hi"))
+
+	r2, err := net.Dial("tcp", "127.0.0.1:1234")
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	r2.Write([]byte("Hi"))
+
+	cmd.Env = os.Environ()
+	cmd.Env = append(cmd.Env, "LISTEN_FDS=2", "FIX_LISTEN_PID=1")
+
+	out, err := cmd.Output()
+	if err != nil {
+		println(string(out))
+		t.Fatalf(err.Error())
+	}
+
+	correctStringWrittenNet(t, r1, "Hello world")
+	correctStringWrittenNet(t, r2, "Goodbye world")
+}

+ 41 - 0
Godeps/_workspace/src/github.com/coreos/go-systemd/activation/packetconns.go

@@ -0,0 +1,41 @@
+/*
+Copyright 2014 CoreOS Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package activation
+
+import (
+	"net"
+)
+
+// PacketConns returns a slice containing a net.PacketConn for each matching socket type
+// passed to this process.
+//
+// The order of the file descriptors is preserved in the returned slice.
+// Nil values are used to fill any gaps. For example if systemd were to return file descriptors
+// corresponding with "udp, tcp, udp", then the slice would contain {net.PacketConn, nil, net.PacketConn}
+func PacketConns(unsetEnv bool) ([]net.PacketConn, error) {
+	files := Files(unsetEnv)
+	conns := make([]net.PacketConn, 0)
+	for i := 0; i < len(files); i++ {
+		if pc, err := net.FilePacketConn(files[i]); err == nil {
+			conns = append(conns, pc)
+			continue
+		} else {
+			conns = append(conns, nil)
+		}
+	}
+	return conns, nil
+}

+ 70 - 0
Godeps/_workspace/src/github.com/coreos/go-systemd/activation/packetconns_test.go

@@ -0,0 +1,70 @@
+/*
+Copyright 2014 CoreOS Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package activation
+
+import (
+	"net"
+	"os"
+	"os/exec"
+	"testing"
+)
+
+// TestActivation forks out a copy of activation.go example and reads back two
+// strings from the pipes that are passed in.
+func TestPacketConns(t *testing.T) {
+	cmd := exec.Command("go", "run", "../examples/activation/udpconn.go")
+
+	u1, err := net.ListenUDP("udp", &net.UDPAddr{Port: 9999})
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	u2, err := net.ListenUDP("udp", &net.UDPAddr{Port: 1234})
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+
+	f1, _ := u1.File()
+	f2, _ := u2.File()
+
+	cmd.ExtraFiles = []*os.File{
+		f1,
+		f2,
+	}
+
+	r1, err := net.Dial("udp", "127.0.0.1:9999")
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	r1.Write([]byte("Hi"))
+
+	r2, err := net.Dial("udp", "127.0.0.1:1234")
+	if err != nil {
+		t.Fatalf(err.Error())
+	}
+	r2.Write([]byte("Hi"))
+
+	cmd.Env = os.Environ()
+	cmd.Env = append(cmd.Env, "LISTEN_FDS=2", "FIX_LISTEN_PID=1")
+
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		t.Fatalf("Cmd output '%s', err: '%s'\n", out, err)
+	}
+
+	correctStringWrittenNet(t, r1, "Hello world")
+	correctStringWrittenNet(t, r2, "Goodbye world")
+}

+ 1 - 1
README.md

@@ -202,7 +202,7 @@ $ flanneld --remote=10.0.0.3:8888 --networks=blue,green
 --iface="": interface to use (IP or name) for inter-host communication. Defaults to the interface for the default route on the machine.
 --subnet-file=/run/flannel/subnet.env: filename where env variables (subnet and MTU values) will be written to.
 --ip-masq=false: setup IP masquerade for traffic destined for outside the flannel network.
---listen="": if specified, will run in server mode. Value is IP and port (e.g. `0.0.0.0:8888`) to listen on.
+--listen="": if specified, will run in server mode. Value is IP and port (e.g. `0.0.0.0:8888`) to listen on or `fd://` for [socket activation](http://www.freedesktop.org/software/systemd/man/systemd.socket.html).
 --remote="": if specified, will run in client mode. Value is IP and port of the server.
 --networks="": if specified, will run in multi-network mode. Value is comma separate list of networks to join.
 -v=0: log level for V logs. Set to 1 to see messages related to data path.

+ 49 - 1
remote/server.go

@@ -20,7 +20,10 @@ import (
 	"net"
 	"net/http"
 	"net/url"
+	"regexp"
+	"strconv"
 
+	"github.com/coreos/flannel/Godeps/_workspace/src/github.com/coreos/go-systemd/activation"
 	log "github.com/coreos/flannel/Godeps/_workspace/src/github.com/golang/glog"
 	"github.com/coreos/flannel/Godeps/_workspace/src/github.com/gorilla/mux"
 	"github.com/coreos/flannel/Godeps/_workspace/src/golang.org/x/net/context"
@@ -153,6 +156,51 @@ func bindHandler(h handler, ctx context.Context, sm subnet.Manager) http.Handler
 	}
 }
 
+func fdListener(addr string) (net.Listener, error) {
+	fdOffset := 0
+	if addr != "" {
+		fd, err := strconv.Atoi(addr)
+		if err != nil {
+			return nil, fmt.Errorf("fd index is not a number")
+		}
+		fdOffset = fd - 3
+	}
+
+	listeners, err := activation.Listeners(false)
+	if err != nil {
+		return nil, err
+	}
+
+	if fdOffset >= len(listeners) {
+		return nil, fmt.Errorf("fd %v is out of range (%v)", addr, len(listeners)+3)
+	}
+
+	if listeners[fdOffset] == nil {
+		return nil, fmt.Errorf("fd %v was not socket activated", addr)
+	}
+
+	return listeners[fdOffset], nil
+}
+
+func listener(addr string) (net.Listener, error) {
+	rex := regexp.MustCompile("(?:([a-z]+)://)?(.*)")
+	groups := rex.FindStringSubmatch(addr)
+
+	switch {
+	case groups == nil:
+		return nil, fmt.Errorf("bad listener address")
+
+	case groups[1] == "", groups[1] == "tcp":
+		return net.Listen("tcp", groups[2])
+
+	case groups[1] == "fd":
+		return fdListener(groups[2])
+
+	default:
+		return nil, fmt.Errorf("bad listener scheme")
+	}
+}
+
 func RunServer(ctx context.Context, sm subnet.Manager, listenAddr string) {
 	// {network} is always required a the API level but to
 	// keep backward compat, special "_" network is allowed
@@ -164,7 +212,7 @@ func RunServer(ctx context.Context, sm subnet.Manager, listenAddr string) {
 	r.HandleFunc("/v1/{network}/leases/{subnet}", bindHandler(handleRenewLease, ctx, sm)).Methods("PUT")
 	r.HandleFunc("/v1/{network}/leases", bindHandler(handleWatchLeases, ctx, sm)).Methods("GET")
 
-	l, err := net.Listen("tcp", listenAddr)
+	l, err := listener(listenAddr)
 	if err != nil {
 		log.Errorf("Error listening on %v: %v", listenAddr, err)
 		return