From cc6aece1fdefbc10638fe9e462a15608c6093115 Mon Sep 17 00:00:00 2001 From: Madhu Venugopal Date: Fri, 9 Oct 2015 11:21:48 -0700 Subject: [PATCH] IPAM API & UX introduced --subnet, --ip-range and --gateway options in docker network command. Also, user can allocate driver specific ip-address if any using the --aux-address option. Supports multiple subnets per network and also sharing ip range across networks if the network-driver and ipam-driver supports it. Example, Bridge driver doesnt support sharing same ip range across networks. Signed-off-by: Madhu Venugopal --- api/client/network.go | 165 ++++++++++- api/server/router/network/network_routes.go | 31 ++- api/types/types.go | 11 +- daemon/network.go | 44 ++- daemon/network/settings.go | 14 + docker/flags_experimental.go | 16 -- integration-cli/docker_api_network_test.go | 106 +++++++- integration-cli/docker_cli_network_test.go | 98 ------- .../docker_cli_network_unix_test.go | 257 ++++++++++++++++++ 9 files changed, 596 insertions(+), 146 deletions(-) delete mode 100644 docker/flags_experimental.go delete mode 100644 integration-cli/docker_cli_network_test.go create mode 100644 integration-cli/docker_cli_network_unix_test.go diff --git a/api/client/network.go b/api/client/network.go index 66ec8a70eb..d9ff387fce 100644 --- a/api/client/network.go +++ b/api/client/network.go @@ -5,10 +5,14 @@ import ( "encoding/json" "fmt" "io" + "net" + "strings" "text/tabwriter" "github.com/docker/docker/api/types" Cli "github.com/docker/docker/cli" + "github.com/docker/docker/daemon/network" + "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/stringid" ) @@ -29,15 +33,37 @@ func (cli *DockerCli) CmdNetwork(args ...string) error { // Usage: docker network create [OPTIONS] func (cli *DockerCli) CmdNetworkCreate(args ...string) error { cmd := Cli.Subcmd("network create", []string{"NETWORK-NAME"}, "Creates a new network with a name specified by the user", false) - flDriver := cmd.String([]string{"d", "-driver"}, "", "Driver to manage the Network") + flDriver := cmd.String([]string{"d", "-driver"}, "bridge", "Driver to manage the Network") + + flIpamDriver := cmd.String([]string{"-ipam-driver"}, "default", "IP Address Management Driver") + flIpamSubnet := opts.NewListOpts(nil) + flIpamIPRange := opts.NewListOpts(nil) + flIpamGateway := opts.NewListOpts(nil) + flIpamAux := opts.NewMapOpts(nil, nil) + + cmd.Var(&flIpamSubnet, []string{"-subnet"}, "Subnet in CIDR format that represents a network segment") + cmd.Var(&flIpamIPRange, []string{"-ip-range"}, "allocate container ip from a sub-range") + cmd.Var(&flIpamGateway, []string{"-gateway"}, "ipv4 or ipv6 Gateway for the master subnet") + cmd.Var(flIpamAux, []string{"-aux-address"}, "Auxiliary ipv4 or ipv6 addresses used by network driver") + cmd.Require(flag.Exact, 1) err := cmd.ParseFlags(args, true) if err != nil { return err } + ipamCfg, err := consolidateIpam(flIpamSubnet.GetAll(), flIpamIPRange.GetAll(), flIpamGateway.GetAll(), flIpamAux.GetAll()) + if err != nil { + return err + } + // Construct network create request body - nc := types.NetworkCreate{Name: cmd.Arg(0), Driver: *flDriver, CheckDuplicate: true} + nc := types.NetworkCreate{ + Name: cmd.Arg(0), + Driver: *flDriver, + IPAM: network.IPAM{Driver: *flIpamDriver, Config: ipamCfg}, + CheckDuplicate: true, + } obj, _, err := readBody(cli.call("POST", "/networks/create", nc, nil)) if err != nil { return err @@ -104,12 +130,13 @@ func (cli *DockerCli) CmdNetworkDisconnect(args ...string) error { // // Usage: docker network ls [OPTIONS] func (cli *DockerCli) CmdNetworkLs(args ...string) error { - cmd := Cli.Subcmd("network ls", []string{""}, "Lists all the networks created by the user", false) + cmd := Cli.Subcmd("network ls", nil, "Lists networks", true) quiet := cmd.Bool([]string{"q", "-quiet"}, false, "Only display numeric IDs") - noTrunc := cmd.Bool([]string{"", "-no-trunc"}, false, "Do not truncate the output") - nLatest := cmd.Bool([]string{"l", "-latest"}, false, "Show the latest network created") - last := cmd.Int([]string{"n"}, -1, "Show n last created networks") + noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Do not truncate the output") + + cmd.Require(flag.Exact, 0) err := cmd.ParseFlags(args, true) + if err != nil { return err } @@ -117,9 +144,6 @@ func (cli *DockerCli) CmdNetworkLs(args ...string) error { if err != nil { return err } - if *last == -1 && *nLatest { - *last = 1 - } var networkResources []types.NetworkResource err = json.Unmarshal(obj, &networkResources) @@ -186,6 +210,129 @@ func (cli *DockerCli) CmdNetworkInspect(args ...string) error { return nil } +// Consolidates the ipam configuration as a group from differnt related configurations +// user can configure network with multiple non-overlapping subnets and hence it is +// possible to corelate the various related parameters and consolidate them. +// consoidateIpam consolidates subnets, ip-ranges, gateways and auxilary addresses into +// structured ipam data. +func consolidateIpam(subnets, ranges, gateways []string, auxaddrs map[string]string) ([]network.IPAMConfig, error) { + if len(subnets) < len(ranges) || len(subnets) < len(gateways) { + return nil, fmt.Errorf("every ip-range or gateway must have a corresponding subnet") + } + iData := map[string]*network.IPAMConfig{} + + // Populate non-overlapping subnets into consolidation map + for _, s := range subnets { + for k := range iData { + ok1, err := subnetMatches(s, k) + if err != nil { + return nil, err + } + ok2, err := subnetMatches(k, s) + if err != nil { + return nil, err + } + if ok1 || ok2 { + return nil, fmt.Errorf("multiple overlapping subnet configuration is not supported") + } + } + iData[s] = &network.IPAMConfig{Subnet: s, AuxAddress: map[string]string{}} + } + + // Validate and add valid ip ranges + for _, r := range ranges { + match := false + for _, s := range subnets { + ok, err := subnetMatches(s, r) + if err != nil { + return nil, err + } + if !ok { + continue + } + if iData[s].IPRange != "" { + return nil, fmt.Errorf("cannot configure multiple ranges (%s, %s) on the same subnet (%s)", r, iData[s].IPRange, s) + } + d := iData[s] + d.IPRange = r + match = true + } + if !match { + return nil, fmt.Errorf("no matching subnet for range %s", r) + } + } + + // Validate and add valid gateways + for _, g := range gateways { + match := false + for _, s := range subnets { + ok, err := subnetMatches(s, g) + if err != nil { + return nil, err + } + if !ok { + continue + } + if iData[s].Gateway != "" { + return nil, fmt.Errorf("cannot configure multiple gateways (%s, %s) for the same subnet (%s)", g, iData[s].Gateway, s) + } + d := iData[s] + d.Gateway = g + match = true + } + if !match { + return nil, fmt.Errorf("no matching subnet for gateway %s", g) + } + } + + // Validate and add aux-addresses + for key, aa := range auxaddrs { + match := false + for _, s := range subnets { + ok, err := subnetMatches(s, aa) + if err != nil { + return nil, err + } + if !ok { + continue + } + iData[s].AuxAddress[key] = aa + match = true + } + if !match { + return nil, fmt.Errorf("no matching subnet for aux-address %s", aa) + } + } + + idl := []network.IPAMConfig{} + for _, v := range iData { + idl = append(idl, *v) + } + return idl, nil +} + +func subnetMatches(subnet, data string) (bool, error) { + var ( + ip net.IP + ) + + _, s, err := net.ParseCIDR(subnet) + if err != nil { + return false, fmt.Errorf("Invalid subnet %s : %v", s, err) + } + + if strings.Contains(data, "/") { + ip, _, err = net.ParseCIDR(data) + if err != nil { + return false, fmt.Errorf("Invalid cidr %s : %v", data, err) + } + } else { + ip = net.ParseIP(data) + } + + return s.Contains(ip), nil +} + func networkUsage() string { networkCommands := map[string]string{ "create": "Create a network", diff --git a/api/server/router/network/network_routes.go b/api/server/router/network/network_routes.go index 594c48a0e1..9d77054162 100644 --- a/api/server/router/network/network_routes.go +++ b/api/server/router/network/network_routes.go @@ -11,6 +11,7 @@ import ( "github.com/docker/docker/api/server/httputils" "github.com/docker/docker/api/types" "github.com/docker/docker/daemon" + "github.com/docker/docker/daemon/network" "github.com/docker/docker/pkg/parsers/filters" "github.com/docker/libnetwork" ) @@ -95,7 +96,7 @@ func (n *networkRouter) postNetworkCreate(ctx context.Context, w http.ResponseWr warning = fmt.Sprintf("Network with name %s (id : %s) already exists", nw.Name(), nw.ID()) } - nw, err = n.daemon.CreateNetwork(create.Name, create.Driver, create.Options) + nw, err = n.daemon.CreateNetwork(create.Name, create.Driver, create.IPAM) if err != nil { return err } @@ -179,8 +180,11 @@ func buildNetworkResource(nw libnetwork.Network) *types.NetworkResource { r.Name = nw.Name() r.ID = nw.ID() + r.Scope = nw.Info().Scope() r.Driver = nw.Type() r.Containers = make(map[string]types.EndpointResource) + buildIpamResources(r, nw) + epl := nw.Endpoints() for _, e := range epl { sb := e.Info().Sandbox() @@ -193,6 +197,31 @@ func buildNetworkResource(nw libnetwork.Network) *types.NetworkResource { return r } +func buildIpamResources(r *types.NetworkResource, nw libnetwork.Network) { + id, ipv4conf, ipv6conf := nw.Info().IpamConfig() + + r.IPAM.Driver = id + + r.IPAM.Config = []network.IPAMConfig{} + for _, ip4 := range ipv4conf { + iData := network.IPAMConfig{} + iData.Subnet = ip4.PreferredPool + iData.IPRange = ip4.SubPool + iData.Gateway = ip4.Gateway + iData.AuxAddress = ip4.AuxAddresses + r.IPAM.Config = append(r.IPAM.Config, iData) + } + + for _, ip6 := range ipv6conf { + iData := network.IPAMConfig{} + iData.Subnet = ip6.PreferredPool + iData.IPRange = ip6.SubPool + iData.Gateway = ip6.Gateway + iData.AuxAddress = ip6.AuxAddresses + r.IPAM.Config = append(r.IPAM.Config, iData) + } +} + func buildEndpointResource(e libnetwork.Endpoint) types.EndpointResource { er := types.EndpointResource{} if e == nil { diff --git a/api/types/types.go b/api/types/types.go index 764878665c..8063975fc2 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -313,9 +313,10 @@ type VolumeCreateRequest struct { type NetworkResource struct { Name string `json:"name"` ID string `json:"id"` + Scope string `json:"scope"` Driver string `json:"driver"` + IPAM network.IPAM `json:"ipam"` Containers map[string]EndpointResource `json:"containers"` - Options map[string]interface{} `json:"options,omitempty"` } //EndpointResource contains network resources allocated and usd for a container in a network @@ -328,10 +329,10 @@ type EndpointResource struct { // NetworkCreate is the expected body of the "create network" http request message type NetworkCreate struct { - Name string `json:"name"` - CheckDuplicate bool `json:"check_duplicate"` - Driver string `json:"driver"` - Options map[string]interface{} `json:"options"` + Name string `json:"name"` + CheckDuplicate bool `json:"check_duplicate"` + Driver string `json:"driver"` + IPAM network.IPAM `json:"ipam"` } // NetworkCreateResponse is the response message sent by the server for network create call diff --git a/daemon/network.go b/daemon/network.go index 6bebb6812a..2cc1647108 100644 --- a/daemon/network.go +++ b/daemon/network.go @@ -2,11 +2,12 @@ package daemon import ( "errors" + "fmt" + "net" "strings" + "github.com/docker/docker/daemon/network" "github.com/docker/libnetwork" - "github.com/docker/libnetwork/netlabel" - "github.com/docker/libnetwork/options" ) const ( @@ -79,14 +80,43 @@ func (daemon *Daemon) GetNetworksByID(partialID string) []libnetwork.Network { } // CreateNetwork creates a network with the given name, driver and other optional parameters -func (daemon *Daemon) CreateNetwork(name, driver string, labels map[string]interface{}) (libnetwork.Network, error) { +func (daemon *Daemon) CreateNetwork(name, driver string, ipam network.IPAM) (libnetwork.Network, error) { c := daemon.netController if driver == "" { driver = c.Config().Daemon.DefaultDriver } - option := libnetwork.NetworkOptionGeneric(options.Generic{ - netlabel.GenericData: map[string]string{}, - }) - return c.NewNetwork(driver, name, option) + nwOptions := []libnetwork.NetworkOption{} + + v4Conf, v6Conf, err := getIpamConfig(ipam.Config) + if err != nil { + return nil, err + } + + if len(ipam.Config) > 0 { + nwOptions = append(nwOptions, libnetwork.NetworkOptionIpam(ipam.Driver, "", v4Conf, v6Conf)) + } + return c.NewNetwork(driver, name, nwOptions...) +} + +func getIpamConfig(data []network.IPAMConfig) ([]*libnetwork.IpamConf, []*libnetwork.IpamConf, error) { + ipamV4Cfg := []*libnetwork.IpamConf{} + ipamV6Cfg := []*libnetwork.IpamConf{} + for _, d := range data { + iCfg := libnetwork.IpamConf{} + iCfg.PreferredPool = d.Subnet + iCfg.SubPool = d.IPRange + iCfg.Gateway = d.Gateway + iCfg.AuxAddresses = d.AuxAddress + ip, _, err := net.ParseCIDR(d.Subnet) + if err != nil { + return nil, nil, fmt.Errorf("Invalid subnet %s : %v", d.Subnet, err) + } + if ip.To4() != nil { + ipamV4Cfg = append(ipamV4Cfg, &iCfg) + } else { + ipamV6Cfg = append(ipamV6Cfg, &iCfg) + } + } + return ipamV4Cfg, ipamV6Cfg, nil } diff --git a/daemon/network/settings.go b/daemon/network/settings.go index c88a06c0a7..d59c369db9 100644 --- a/daemon/network/settings.go +++ b/daemon/network/settings.go @@ -8,6 +8,20 @@ type Address struct { PrefixLen int } +// IPAM represents IP Address Management +type IPAM struct { + Driver string `json:"driver"` + Config []IPAMConfig `json:"config"` +} + +// IPAMConfig represents IPAM configurations +type IPAMConfig struct { + Subnet string `json:"subnet,omitempty"` + IPRange string `json:"ip_range,omitempty"` + Gateway string `json:"gateway,omitempty"` + AuxAddress map[string]string `json:"auxiliary_address,omitempty"` +} + // Settings stores configuration details about the daemon network config // TODO Windows. Many of these fields can be factored out., type Settings struct { diff --git a/docker/flags_experimental.go b/docker/flags_experimental.go deleted file mode 100644 index 0994a82dd0..0000000000 --- a/docker/flags_experimental.go +++ /dev/null @@ -1,16 +0,0 @@ -// +build experimental - -package main - -import ( - "sort" - - "github.com/docker/docker/cli" -) - -func init() { - dockerCommands = append(dockerCommands, cli.Command{Name: "network", Description: "Network management"}) - - //Sorting logic required here to pass Command Sort Test. - sort.Sort(byName(dockerCommands)) -} diff --git a/integration-cli/docker_api_network_test.go b/integration-cli/docker_api_network_test.go index 587cb363ad..e668bb7594 100644 --- a/integration-cli/docker_api_network_test.go +++ b/integration-cli/docker_api_network_test.go @@ -2,12 +2,14 @@ package main import ( "encoding/json" + "fmt" "net" "net/http" "net/url" "strings" "github.com/docker/docker/api/types" + "github.com/docker/docker/daemon/network" "github.com/docker/docker/pkg/parsers/filters" "github.com/go-check/check" ) @@ -23,11 +25,15 @@ func (s *DockerSuite) TestApiNetworkGetDefaults(c *check.C) { func (s *DockerSuite) TestApiNetworkCreateDelete(c *check.C) { // Create a network name := "testnetwork" - id := createNetwork(c, name, true) + config := types.NetworkCreate{ + Name: name, + CheckDuplicate: true, + } + id := createNetwork(c, config, true) c.Assert(isNetworkAvailable(c, name), check.Equals, true) // POST another network with same name and CheckDuplicate must fail - createNetwork(c, name, false) + createNetwork(c, config, false) // delete the network and make sure it is deleted deleteNetwork(c, id, true) @@ -51,18 +57,46 @@ func (s *DockerSuite) TestApiNetworkInspect(c *check.C) { // inspect default bridge network again and make sure the container is connected nr = getNetworkResource(c, nr.ID) + c.Assert(nr.Driver, check.Equals, "bridge") + c.Assert(nr.Scope, check.Equals, "local") + c.Assert(nr.IPAM.Driver, check.Equals, "default") c.Assert(len(nr.Containers), check.Equals, 1) c.Assert(nr.Containers[containerID], check.NotNil) ip, _, err := net.ParseCIDR(nr.Containers[containerID].IPv4Address) c.Assert(err, check.IsNil) c.Assert(ip.String(), check.Equals, containerIP) + + // IPAM configuration inspect + ipam := network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{{Subnet: "172.28.0.0/16", IPRange: "172.28.5.0/24", Gateway: "172.28.5.254"}}, + } + config := types.NetworkCreate{ + Name: "br0", + Driver: "bridge", + IPAM: ipam, + } + id0 := createNetwork(c, config, true) + c.Assert(isNetworkAvailable(c, "br0"), check.Equals, true) + + nr = getNetworkResource(c, id0) + c.Assert(len(nr.IPAM.Config), check.Equals, 1) + c.Assert(nr.IPAM.Config[0].Subnet, check.Equals, "172.28.0.0/16") + c.Assert(nr.IPAM.Config[0].IPRange, check.Equals, "172.28.5.0/24") + c.Assert(nr.IPAM.Config[0].Gateway, check.Equals, "172.28.5.254") + // delete the network and make sure it is deleted + deleteNetwork(c, id0, true) + c.Assert(isNetworkAvailable(c, "br0"), check.Equals, false) } func (s *DockerSuite) TestApiNetworkConnectDisconnect(c *check.C) { // Create test network name := "testnetwork" - id := createNetwork(c, name, true) + config := types.NetworkCreate{ + Name: name, + } + id := createNetwork(c, config, true) nr := getNetworkResource(c, id) c.Assert(nr.Name, check.Equals, name) c.Assert(nr.ID, check.Equals, id) @@ -96,6 +130,64 @@ func (s *DockerSuite) TestApiNetworkConnectDisconnect(c *check.C) { deleteNetwork(c, nr.ID, true) } +func (s *DockerSuite) TestApiNetworkIpamMultipleBridgeNetworks(c *check.C) { + // test0 bridge network + ipam0 := network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{{Subnet: "192.178.0.0/16", IPRange: "192.178.128.0/17", Gateway: "192.178.138.100"}}, + } + config0 := types.NetworkCreate{ + Name: "test0", + Driver: "bridge", + IPAM: ipam0, + } + id0 := createNetwork(c, config0, true) + c.Assert(isNetworkAvailable(c, "test0"), check.Equals, true) + + ipam1 := network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{{Subnet: "192.178.128.0/17", Gateway: "192.178.128.1"}}, + } + // test1 bridge network overlaps with test0 + config1 := types.NetworkCreate{ + Name: "test1", + Driver: "bridge", + IPAM: ipam1, + } + createNetwork(c, config1, false) + c.Assert(isNetworkAvailable(c, "test1"), check.Equals, false) + + ipam2 := network.IPAM{ + Driver: "default", + Config: []network.IPAMConfig{{Subnet: "192.169.0.0/16", Gateway: "192.169.100.100"}}, + } + // test2 bridge network does not overlap + config2 := types.NetworkCreate{ + Name: "test2", + Driver: "bridge", + IPAM: ipam2, + } + createNetwork(c, config2, true) + c.Assert(isNetworkAvailable(c, "test2"), check.Equals, true) + + // remove test0 and retry to create test1 + deleteNetwork(c, id0, true) + createNetwork(c, config1, true) + c.Assert(isNetworkAvailable(c, "test1"), check.Equals, true) + + // for networks w/o ipam specified, docker will choose proper non-overlapping subnets + createNetwork(c, types.NetworkCreate{Name: "test3"}, true) + c.Assert(isNetworkAvailable(c, "test3"), check.Equals, true) + createNetwork(c, types.NetworkCreate{Name: "test4"}, true) + c.Assert(isNetworkAvailable(c, "test4"), check.Equals, true) + createNetwork(c, types.NetworkCreate{Name: "test5"}, true) + c.Assert(isNetworkAvailable(c, "test5"), check.Equals, true) + + for i := 1; i < 6; i++ { + deleteNetwork(c, fmt.Sprintf("test%d", i), true) + } +} + func isNetworkAvailable(c *check.C, name string) bool { status, body, err := sockRequest("GET", "/networks", nil) c.Assert(status, check.Equals, http.StatusOK) @@ -146,13 +238,7 @@ func getNetworkResource(c *check.C, id string) *types.NetworkResource { return &nr } -func createNetwork(c *check.C, name string, shouldSucceed bool) string { - config := types.NetworkCreate{ - Name: name, - Driver: "bridge", - CheckDuplicate: true, - } - +func createNetwork(c *check.C, config types.NetworkCreate, shouldSucceed bool) string { status, resp, err := sockRequest("POST", "/networks/create", config) if !shouldSucceed { c.Assert(status, check.Not(check.Equals), http.StatusCreated) diff --git a/integration-cli/docker_cli_network_test.go b/integration-cli/docker_cli_network_test.go deleted file mode 100644 index 134133918d..0000000000 --- a/integration-cli/docker_cli_network_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "encoding/json" - "net" - "strings" - - "github.com/docker/docker/api/types" - "github.com/go-check/check" -) - -func assertNwIsAvailable(c *check.C, name string) { - if !isNwPresent(c, name) { - c.Fatalf("Network %s not found in network ls o/p", name) - } -} - -func assertNwNotAvailable(c *check.C, name string) { - if isNwPresent(c, name) { - c.Fatalf("Found network %s in network ls o/p", name) - } -} - -func isNwPresent(c *check.C, name string) bool { - out, _ := dockerCmd(c, "network", "ls") - lines := strings.Split(out, "\n") - for i := 1; i < len(lines)-1; i++ { - if strings.Contains(lines[i], name) { - return true - } - } - return false -} - -func getNwResource(c *check.C, name string) *types.NetworkResource { - out, _ := dockerCmd(c, "network", "inspect", name) - nr := types.NetworkResource{} - err := json.Unmarshal([]byte(out), &nr) - c.Assert(err, check.IsNil) - return &nr -} - -func (s *DockerSuite) TestDockerNetworkLsDefault(c *check.C) { - defaults := []string{"bridge", "host", "none"} - for _, nn := range defaults { - assertNwIsAvailable(c, nn) - } -} - -func (s *DockerSuite) TestDockerNetworkCreateDelete(c *check.C) { - dockerCmd(c, "network", "create", "test") - assertNwIsAvailable(c, "test") - - dockerCmd(c, "network", "rm", "test") - assertNwNotAvailable(c, "test") -} - -func (s *DockerSuite) TestDockerNetworkConnectDisconnect(c *check.C) { - dockerCmd(c, "network", "create", "test") - assertNwIsAvailable(c, "test") - nr := getNwResource(c, "test") - - c.Assert(nr.Name, check.Equals, "test") - c.Assert(len(nr.Containers), check.Equals, 0) - - // run a container - out, _ := dockerCmd(c, "run", "-d", "--name", "test", "busybox", "top") - c.Assert(waitRun("test"), check.IsNil) - containerID := strings.TrimSpace(out) - - // connect the container to the test network - dockerCmd(c, "network", "connect", "test", containerID) - - // inspect the network to make sure container is connected - nr = getNetworkResource(c, nr.ID) - c.Assert(len(nr.Containers), check.Equals, 1) - c.Assert(nr.Containers[containerID], check.NotNil) - - // check if container IP matches network inspect - ip, _, err := net.ParseCIDR(nr.Containers[containerID].IPv4Address) - c.Assert(err, check.IsNil) - containerIP := findContainerIP(c, "test") - c.Assert(ip.String(), check.Equals, containerIP) - - // disconnect container from the network - dockerCmd(c, "network", "disconnect", "test", containerID) - nr = getNwResource(c, "test") - c.Assert(nr.Name, check.Equals, "test") - c.Assert(len(nr.Containers), check.Equals, 0) - - // check if network connect fails for inactive containers - dockerCmd(c, "stop", containerID) - _, _, err = dockerCmdWithError("network", "connect", "test", containerID) - c.Assert(err, check.NotNil) - - dockerCmd(c, "network", "rm", "test") - assertNwNotAvailable(c, "test") -} diff --git a/integration-cli/docker_cli_network_unix_test.go b/integration-cli/docker_cli_network_unix_test.go new file mode 100644 index 0000000000..c25bd8840f --- /dev/null +++ b/integration-cli/docker_cli_network_unix_test.go @@ -0,0 +1,257 @@ +// +build !windows + +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "os" + "strings" + + "github.com/docker/docker/api/types" + "github.com/docker/libnetwork/driverapi" + "github.com/go-check/check" +) + +const dummyNetworkDriver = "dummy-network-driver" + +func init() { + check.Suite(&DockerNetworkSuite{ + ds: &DockerSuite{}, + }) +} + +type DockerNetworkSuite struct { + server *httptest.Server + ds *DockerSuite + d *Daemon +} + +func (s *DockerNetworkSuite) SetUpTest(c *check.C) { + s.d = NewDaemon(c) +} + +func (s *DockerNetworkSuite) TearDownTest(c *check.C) { + s.d.Stop() + s.ds.TearDownTest(c) +} + +func (s *DockerNetworkSuite) SetUpSuite(c *check.C) { + mux := http.NewServeMux() + s.server = httptest.NewServer(mux) + if s.server == nil { + c.Fatal("Failed to start a HTTP Server") + } + + mux.HandleFunc("/Plugin.Activate", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, `{"Implements": ["%s"]}`, driverapi.NetworkPluginEndpointType) + }) + + mux.HandleFunc(fmt.Sprintf("/%s.GetCapabilities", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, `{"Scope":"local"}`) + }) + + mux.HandleFunc(fmt.Sprintf("/%s.CreateNetwork", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, "null") + }) + + mux.HandleFunc(fmt.Sprintf("/%s.DeleteNetwork", driverapi.NetworkPluginEndpointType), func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.docker.plugins.v1+json") + fmt.Fprintf(w, "null") + }) + + if err := os.MkdirAll("/etc/docker/plugins", 0755); err != nil { + c.Fatal(err) + } + + fileName := fmt.Sprintf("/etc/docker/plugins/%s.spec", dummyNetworkDriver) + if err := ioutil.WriteFile(fileName, []byte(s.server.URL), 0644); err != nil { + c.Fatal(err) + } +} + +func (s *DockerNetworkSuite) TearDownSuite(c *check.C) { + if s.server == nil { + return + } + + s.server.Close() + + if err := os.RemoveAll("/etc/docker/plugins"); err != nil { + c.Fatal(err) + } +} + +func assertNwIsAvailable(c *check.C, name string) { + if !isNwPresent(c, name) { + c.Fatalf("Network %s not found in network ls o/p", name) + } +} + +func assertNwNotAvailable(c *check.C, name string) { + if isNwPresent(c, name) { + c.Fatalf("Found network %s in network ls o/p", name) + } +} + +func isNwPresent(c *check.C, name string) bool { + out, _ := dockerCmd(c, "network", "ls") + lines := strings.Split(out, "\n") + for i := 1; i < len(lines)-1; i++ { + if strings.Contains(lines[i], name) { + return true + } + } + return false +} + +func getNwResource(c *check.C, name string) *types.NetworkResource { + out, _ := dockerCmd(c, "network", "inspect", name) + nr := types.NetworkResource{} + err := json.Unmarshal([]byte(out), &nr) + c.Assert(err, check.IsNil) + return &nr +} + +func (s *DockerNetworkSuite) TestDockerNetworkLsDefault(c *check.C) { + defaults := []string{"bridge", "host", "none"} + for _, nn := range defaults { + assertNwIsAvailable(c, nn) + } +} + +func (s *DockerNetworkSuite) TestDockerNetworkCreateDelete(c *check.C) { + dockerCmd(c, "network", "create", "test") + assertNwIsAvailable(c, "test") + + dockerCmd(c, "network", "rm", "test") + assertNwNotAvailable(c, "test") +} + +func (s *DockerNetworkSuite) TestDockerNetworkConnectDisconnect(c *check.C) { + dockerCmd(c, "network", "create", "test") + assertNwIsAvailable(c, "test") + nr := getNwResource(c, "test") + + c.Assert(nr.Name, check.Equals, "test") + c.Assert(len(nr.Containers), check.Equals, 0) + + // run a container + out, _ := dockerCmd(c, "run", "-d", "--name", "test", "busybox", "top") + c.Assert(waitRun("test"), check.IsNil) + containerID := strings.TrimSpace(out) + + // connect the container to the test network + dockerCmd(c, "network", "connect", "test", containerID) + + // inspect the network to make sure container is connected + nr = getNetworkResource(c, nr.ID) + c.Assert(len(nr.Containers), check.Equals, 1) + c.Assert(nr.Containers[containerID], check.NotNil) + + // check if container IP matches network inspect + ip, _, err := net.ParseCIDR(nr.Containers[containerID].IPv4Address) + c.Assert(err, check.IsNil) + containerIP := findContainerIP(c, "test") + c.Assert(ip.String(), check.Equals, containerIP) + + // disconnect container from the network + dockerCmd(c, "network", "disconnect", "test", containerID) + nr = getNwResource(c, "test") + c.Assert(nr.Name, check.Equals, "test") + c.Assert(len(nr.Containers), check.Equals, 0) + + // check if network connect fails for inactive containers + dockerCmd(c, "stop", containerID) + _, _, err = dockerCmdWithError("network", "connect", "test", containerID) + c.Assert(err, check.NotNil) + + dockerCmd(c, "network", "rm", "test") + assertNwNotAvailable(c, "test") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpamMultipleNetworks(c *check.C) { + // test0 bridge network + dockerCmd(c, "network", "create", "--subnet=192.168.0.0/16", "test1") + assertNwIsAvailable(c, "test1") + + // test2 bridge network does not overlap + dockerCmd(c, "network", "create", "--subnet=192.169.0.0/16", "test2") + assertNwIsAvailable(c, "test2") + + // for networks w/o ipam specified, docker will choose proper non-overlapping subnets + dockerCmd(c, "network", "create", "test3") + assertNwIsAvailable(c, "test3") + dockerCmd(c, "network", "create", "test4") + assertNwIsAvailable(c, "test4") + dockerCmd(c, "network", "create", "test5") + assertNwIsAvailable(c, "test5") + + // test network with multiple subnets + // bridge network doesnt support multiple subnets. hence, use a dummy driver that supports + + dockerCmd(c, "network", "create", "-d", dummyNetworkDriver, "--subnet=192.168.0.0/16", "--subnet=192.170.0.0/16", "test6") + assertNwIsAvailable(c, "test6") + + // test network with multiple subnets with valid ipam combinations + // also check same subnet across networks when the driver supports it. + dockerCmd(c, "network", "create", "-d", dummyNetworkDriver, + "--subnet=192.168.0.0/16", "--subnet=192.170.0.0/16", + "--gateway=192.168.0.100", "--gateway=192.170.0.100", + "--ip-range=192.168.1.0/24", + "--aux-address", "a=192.168.1.5", "--aux-address", "b=192.168.1.6", + "--aux-address", "a=192.170.1.5", "--aux-address", "b=192.170.1.6", + "test7") + assertNwIsAvailable(c, "test7") + + // cleanup + for i := 1; i < 8; i++ { + dockerCmd(c, "network", "rm", fmt.Sprintf("test%d", i)) + } +} + +func (s *DockerNetworkSuite) TestDockerNetworkInspect(c *check.C) { + // if unspecified, network gateway will be selected from inside preferred pool + dockerCmd(c, "network", "create", "--driver=bridge", "--subnet=172.28.0.0/16", "--ip-range=172.28.5.0/24", "--gateway=172.28.5.254", "br0") + assertNwIsAvailable(c, "br0") + + nr := getNetworkResource(c, "br0") + c.Assert(nr.Driver, check.Equals, "bridge") + c.Assert(nr.Scope, check.Equals, "local") + c.Assert(nr.IPAM.Driver, check.Equals, "default") + c.Assert(len(nr.IPAM.Config), check.Equals, 1) + c.Assert(nr.IPAM.Config[0].Subnet, check.Equals, "172.28.0.0/16") + c.Assert(nr.IPAM.Config[0].IPRange, check.Equals, "172.28.5.0/24") + c.Assert(nr.IPAM.Config[0].Gateway, check.Equals, "172.28.5.254") + dockerCmd(c, "network", "rm", "br0") +} + +func (s *DockerNetworkSuite) TestDockerNetworkIpamInvalidCombinations(c *check.C) { + // network with ip-range out of subnet range + _, _, err := dockerCmdWithError("network", "create", "--subnet=192.168.0.0/16", "--ip-range=192.170.0.0/16", "test") + c.Assert(err, check.NotNil) + + // network with multiple gateways for a single subnet + _, _, err = dockerCmdWithError("network", "create", "--subnet=192.168.0.0/16", "--gateway=192.168.0.1", "--gateway=192.168.0.2", "test") + c.Assert(err, check.NotNil) + + // Multiple overlaping subnets in the same network must fail + _, _, err = dockerCmdWithError("network", "create", "--subnet=192.168.0.0/16", "--subnet=192.168.1.0/16", "test") + c.Assert(err, check.NotNil) + + // overlapping subnets across networks must fail + // create a valid test0 network + dockerCmd(c, "network", "create", "--subnet=192.168.0.0/16", "test0") + assertNwIsAvailable(c, "test0") + // create an overlapping test1 network + _, _, err = dockerCmdWithError("network", "create", "--subnet=192.168.128.0/17", "test1") + c.Assert(err, check.NotNil) + dockerCmd(c, "network", "rm", "test0") +}