From fa78a98fc611e5a046b39ba64866fd71a96b4b87 Mon Sep 17 00:00:00 2001 From: Madhu Venugopal Date: Mon, 4 May 2015 14:55:36 -0700 Subject: [PATCH] Initial dnet tool to test and manage libnetwork end-to-end Signed-off-by: Madhu Venugopal --- cmd/dnet/dnet.go | 206 ++++++++++++++++++++++++++++++++++++++++++ cmd/dnet/dnet_test.go | 132 +++++++++++++++++++++++++++ cmd/dnet/flags.go | 49 ++++++++++ 3 files changed, 387 insertions(+) create mode 100644 cmd/dnet/dnet.go create mode 100644 cmd/dnet/dnet_test.go create mode 100644 cmd/dnet/flags.go diff --git a/cmd/dnet/dnet.go b/cmd/dnet/dnet.go new file mode 100644 index 0000000..547ec31 --- /dev/null +++ b/cmd/dnet/dnet.go @@ -0,0 +1,206 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "strings" + + flag "github.com/docker/docker/pkg/mflag" + "github.com/docker/docker/pkg/parsers" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/term" + "github.com/docker/libnetwork" + "github.com/docker/libnetwork/api" + "github.com/docker/libnetwork/client" + "github.com/gorilla/mux" +) + +var ( + // DefaultHTTPHost is used if only port is provided to -H flag e.g. docker -d -H tcp://:8080 + DefaultHTTPHost = "127.0.0.1" + // DefaultHTTPPort is the default http port used by dnet + DefaultHTTPPort = 2385 + // DefaultUnixSocket exported + DefaultUnixSocket = "/var/run/dnet.sock" +) + +func main() { + _, stdout, stderr := term.StdStreams() + logrus.SetOutput(stderr) + + err := dnetCommand(stdout, stderr) + if err != nil { + os.Exit(1) + } +} + +func dnetCommand(stdout, stderr io.Writer) error { + flag.Parse() + + if *flHelp { + flag.Usage() + return nil + } + + if *flLogLevel != "" { + lvl, err := logrus.ParseLevel(*flLogLevel) + if err != nil { + fmt.Fprintf(stderr, "Unable to parse logging level: %s\n", *flLogLevel) + return err + } + logrus.SetLevel(lvl) + } else { + logrus.SetLevel(logrus.InfoLevel) + } + + if *flDebug { + logrus.SetLevel(logrus.DebugLevel) + } + + if *flHost == "" { + defaultHost := os.Getenv("DNET_HOST") + if defaultHost == "" { + // TODO : Add UDS support + defaultHost = fmt.Sprintf("tcp://%s:%d", DefaultHTTPHost, DefaultHTTPPort) + } + *flHost = defaultHost + } + + dc, err := newDnetConnection(*flHost) + if err != nil { + if *flDaemon { + logrus.Error(err) + } else { + fmt.Fprint(stderr, err) + } + return err + } + + if *flDaemon { + err := dc.dnetDaemon() + if err != nil { + logrus.Errorf("dnet Daemon exited with an error : %v", err) + } + return err + } + + cli := client.NewNetworkCli(stdout, stderr, dc.httpCall) + if err := cli.Cmd("dnet", flag.Args()...); err != nil { + fmt.Fprintln(stderr, err) + return err + } + return nil +} + +type dnetConnection struct { + // proto holds the client protocol i.e. unix. + proto string + // addr holds the client address. + addr string +} + +func (d *dnetConnection) dnetDaemon() error { + controller, err := libnetwork.New() + if err != nil { + fmt.Println("Error starting dnetDaemon :", err) + return err + } + httpHandler := api.NewHTTPHandler(controller) + r := mux.NewRouter().StrictSlash(false) + post := r.PathPrefix("/networks").Subrouter() + post.Methods("GET").HandlerFunc(httpHandler) + post.Methods("PUT", "POST").HandlerFunc(httpHandler) + post.Methods("DELETE").HandlerFunc(httpHandler) + return http.ListenAndServe(d.addr, r) +} + +func newDnetConnection(val string) (*dnetConnection, error) { + url, err := parsers.ParseHost(DefaultHTTPHost, DefaultUnixSocket, val) + if err != nil { + return nil, err + } + protoAddrParts := strings.SplitN(url, "://", 2) + if len(protoAddrParts) != 2 { + return nil, fmt.Errorf("bad format, expected tcp://ADDR") + } + if strings.ToLower(protoAddrParts[0]) != "tcp" { + return nil, fmt.Errorf("dnet currently only supports tcp transport") + } + + return &dnetConnection{protoAddrParts[0], protoAddrParts[1]}, nil +} + +func (d *dnetConnection) httpCall(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, int, error) { + var in io.Reader + in, err := encodeData(data) + if err != nil { + return nil, -1, err + } + + req, err := http.NewRequest(method, fmt.Sprintf("%s", path), in) + if err != nil { + return nil, -1, err + } + + setupRequestHeaders(method, data, req, headers) + + req.URL.Host = d.addr + req.URL.Scheme = "http" + + httpClient := &http.Client{} + resp, err := httpClient.Do(req) + statusCode := -1 + if resp != nil { + statusCode = resp.StatusCode + } + if err != nil { + return nil, statusCode, fmt.Errorf("An error occurred trying to connect: %v", err) + } + + if statusCode < 200 || statusCode >= 400 { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, statusCode, err + } + return nil, statusCode, fmt.Errorf("Error response from daemon: %s", bytes.TrimSpace(body)) + } + + return resp.Body, statusCode, nil +} + +func setupRequestHeaders(method string, data interface{}, req *http.Request, headers map[string][]string) { + if data != nil { + if headers == nil { + headers = make(map[string][]string) + } + headers["Content-Type"] = []string{"application/json"} + } + + expectedPayload := (method == "POST" || method == "PUT") + + if expectedPayload && req.Header.Get("Content-Type") == "" { + req.Header.Set("Content-Type", "text/plain") + } + + if headers != nil { + for k, v := range headers { + req.Header[k] = v + } + } +} + +func encodeData(data interface{}) (*bytes.Buffer, error) { + params := bytes.NewBuffer(nil) + if data != nil { + if err := json.NewEncoder(params).Encode(data); err != nil { + return nil, err + } + } + return params, nil +} diff --git a/cmd/dnet/dnet_test.go b/cmd/dnet/dnet_test.go new file mode 100644 index 0000000..b8466f1 --- /dev/null +++ b/cmd/dnet/dnet_test.go @@ -0,0 +1,132 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + "time" + + "github.com/docker/libnetwork/netutils" +) + +const dnetCommandName = "dnet" + +var origStdOut = os.Stdout + +func TestDnetDaemonCustom(t *testing.T) { + if !netutils.IsRunningInContainer() { + t.Skip("This test must run inside a container ") + } + customPort := 4567 + doneChan := make(chan bool) + go func() { + args := []string{dnetCommandName, "-d", fmt.Sprintf("-H=:%d", customPort)} + executeDnetCommand(t, args, true) + doneChan <- true + }() + + select { + case <-doneChan: + t.Fatal("dnet Daemon is not supposed to exit") + case <-time.After(3 * time.Second): + args := []string{dnetCommandName, "-d=false", fmt.Sprintf("-H=:%d", customPort), "-D", "network", "ls"} + executeDnetCommand(t, args, true) + } +} + +func TestDnetDaemonInvalidCustom(t *testing.T) { + if !netutils.IsRunningInContainer() { + t.Skip("This test must run inside a container ") + } + customPort := 4668 + doneChan := make(chan bool) + go func() { + args := []string{dnetCommandName, "-d=true", fmt.Sprintf("-H=:%d", customPort)} + executeDnetCommand(t, args, true) + doneChan <- true + }() + + select { + case <-doneChan: + t.Fatal("dnet Daemon is not supposed to exit") + case <-time.After(3 * time.Second): + args := []string{dnetCommandName, "-d=false", "-H=:6669", "-D", "network", "ls"} + executeDnetCommand(t, args, false) + } +} + +func TestDnetDaemonInvalidParams(t *testing.T) { + if !netutils.IsRunningInContainer() { + t.Skip("This test must run inside a container ") + } + args := []string{dnetCommandName, "-d=false", "-H=tcp:/127.0.0.1:8080"} + executeDnetCommand(t, args, false) + + args = []string{dnetCommandName, "-d=false", "-H=unix://var/run/dnet.sock"} + executeDnetCommand(t, args, false) + + args = []string{dnetCommandName, "-d=false", "-H=", "-l=invalid"} + executeDnetCommand(t, args, false) + + args = []string{dnetCommandName, "-d=false", "-H=", "-l=error", "invalid"} + executeDnetCommand(t, args, false) +} + +func TestDnetDefaultsWithFlags(t *testing.T) { + if !netutils.IsRunningInContainer() { + t.Skip("This test must run inside a container ") + } + doneChan := make(chan bool) + go func() { + args := []string{dnetCommandName, "-d=true", "-H=", "-l=error"} + executeDnetCommand(t, args, true) + doneChan <- true + }() + + select { + case <-doneChan: + t.Fatal("dnet Daemon is not supposed to exit") + case <-time.After(3 * time.Second): + args := []string{dnetCommandName, "-d=false", "network", "create", "-d=null", "test"} + executeDnetCommand(t, args, true) + + args = []string{dnetCommandName, "-d=false", "-D", "network", "ls"} + executeDnetCommand(t, args, true) + } +} + +func TestDnetMain(t *testing.T) { + if !netutils.IsRunningInContainer() { + t.Skip("This test must run inside a container ") + } + customPort := 4568 + doneChan := make(chan bool) + go func() { + args := []string{dnetCommandName, "-d=true", "-h=false", fmt.Sprintf("-H=:%d", customPort)} + os.Args = args + main() + doneChan <- true + }() + select { + case <-doneChan: + t.Fatal("dnet Daemon is not supposed to exit") + case <-time.After(2 * time.Second): + } +} + +func executeDnetCommand(t *testing.T, args []string, shouldSucced bool) { + _, w, _ := os.Pipe() + os.Stdout = w + + os.Args = args + err := dnetCommand(ioutil.Discard, ioutil.Discard) + if shouldSucced && err != nil { + os.Stdout = origStdOut + t.Fatalf("cli [%v] must succeed, but failed with an error : %v", args, err) + } else if !shouldSucced && err == nil { + os.Stdout = origStdOut + t.Fatalf("cli [%v] must fail, but succeeded with an error : %v", args, err) + } + os.Stdout = origStdOut +} diff --git a/cmd/dnet/flags.go b/cmd/dnet/flags.go new file mode 100644 index 0000000..468f18d --- /dev/null +++ b/cmd/dnet/flags.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "os" + + flag "github.com/docker/docker/pkg/mflag" +) + +type command struct { + name string + description string +} + +type byName []command + +var ( + flDaemon = flag.Bool([]string{"d", "-daemon"}, false, "Enable daemon mode") + flHost = flag.String([]string{"H", "-Host"}, "", "Daemon socket to connect to") + flLogLevel = flag.String([]string{"l", "-log-level"}, "info", "Set the logging level") + flDebug = flag.Bool([]string{"D", "-debug"}, false, "Enable debug mode") + flHelp = flag.Bool([]string{"h", "-help"}, false, "Print usage") + + dnetCommands = []command{ + {"network", "Network management commands"}, + } +) + +func init() { + flag.Usage = func() { + fmt.Fprint(os.Stdout, "Usage: dnet [OPTIONS] COMMAND [arg...]\n\nA self-sufficient runtime for container networking.\n\nOptions:\n") + + flag.CommandLine.SetOutput(os.Stdout) + flag.PrintDefaults() + + help := "\nCommands:\n" + + for _, cmd := range dnetCommands { + help += fmt.Sprintf(" %-10.10s%s\n", cmd.name, cmd.description) + } + + help += "\nRun 'dnet COMMAND --help' for more information on a command." + fmt.Fprintf(os.Stdout, "%s\n", help) + } +} + +func printUsage() { + fmt.Println("Usage: dnet network ") +}