From 15e35c447058851850155f90292e51decb482956 Mon Sep 17 00:00:00 2001 From: John Howard Date: Fri, 18 Sep 2015 18:21:57 -0700 Subject: [PATCH] Windows: Adds support for Hyper-V Containers Signed-off-by: John Howard --- daemon/container.go | 6 ++-- daemon/container_unix.go | 17 ++++++++++ daemon/container_windows.go | 25 ++++++++++++++ daemon/execdriver/driver.go | 1 + daemon/execdriver/windows/run.go | 10 ++++++ runconfig/config.go | 4 +++ runconfig/config_test.go | 57 ++++++++++++++++++++++++++++++++ runconfig/hostconfig.go | 11 ++++++ runconfig/hostconfig_unix.go | 5 +++ runconfig/hostconfig_windows.go | 13 ++++++++ runconfig/parse.go | 33 +++++++++--------- runconfig/parse_unix.go | 15 +++++++++ runconfig/parse_windows.go | 15 +++++++++ 13 files changed, 192 insertions(+), 20 deletions(-) diff --git a/daemon/container.go b/daemon/container.go index 480bd8eb96..6d2c0ea58b 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -269,7 +269,7 @@ func (container *Container) Start() (err error) { } }() - if err := container.Mount(); err != nil { + if err := container.conditionalMountOnStart(); err != nil { return err } @@ -341,9 +341,7 @@ func (container *Container) cleanup() { logrus.Errorf("%s: Failed to umount ipc filesystems: %v", container.ID, err) } - if err := container.Unmount(); err != nil { - logrus.Errorf("%s: Failed to umount filesystem: %v", container.ID, err) - } + container.conditionalUnmountOnCleanup() for _, eConfig := range container.execCommands.s { container.daemon.unregisterExecCommand(eConfig) diff --git a/daemon/container_unix.go b/daemon/container_unix.go index c005347f14..e5f52bec9b 100644 --- a/daemon/container_unix.go +++ b/daemon/container_unix.go @@ -1433,3 +1433,20 @@ func (container *Container) ipcMounts() []execdriver.Mount { func detachMounted(path string) error { return syscall.Unmount(path, syscall.MNT_DETACH) } + +// conditionalMountOnStart is a platform specific helper function during the +// container start to call mount. +func (container *Container) conditionalMountOnStart() error { + if err := container.Mount(); err != nil { + return err + } + return nil +} + +// conditionalUnmountOnCleanup is a platform specific helper function called +// during the cleanup of a container to unmount. +func (container *Container) conditionalUnmountOnCleanup() { + if err := container.Unmount(); err != nil { + logrus.Errorf("%v: Failed to umount filesystem: %v", container.ID, err) + } +} diff --git a/daemon/container_windows.go b/daemon/container_windows.go index 5d62a96d68..6fd956556b 100644 --- a/daemon/container_windows.go +++ b/daemon/container_windows.go @@ -5,6 +5,7 @@ package daemon import ( "strings" + "github.com/Sirupsen/logrus" "github.com/docker/docker/daemon/execdriver" derr "github.com/docker/docker/errors" "github.com/docker/docker/volume" @@ -144,6 +145,7 @@ func populateCommand(c *Container, env []string) error { LayerFolder: layerFolder, LayerPaths: layerPaths, Hostname: c.Config.Hostname, + Isolated: c.hostConfig.Isolation.IsHyperV(), } return nil @@ -194,3 +196,26 @@ func (container *Container) ipcMounts() []execdriver.Mount { func getDefaultRouteMtu() (int, error) { return -1, errSystemNotSupported } + +// conditionalMountOnStart is a platform specific helper function during the +// container start to call mount. +func (container *Container) conditionalMountOnStart() error { + // We do not mount if a Hyper-V container + if !container.hostConfig.Isolation.IsHyperV() { + if err := container.Mount(); err != nil { + return err + } + } + return nil +} + +// conditionalUnmountOnCleanup is a platform specific helper function called +// during the cleanup of a container to unmount. +func (container *Container) conditionalUnmountOnCleanup() { + // We do not unmount if a Hyper-V container + if !container.hostConfig.Isolation.IsHyperV() { + if err := container.Unmount(); err != nil { + logrus.Errorf("%v: Failed to umount filesystem: %v", container.ID, err) + } + } +} diff --git a/daemon/execdriver/driver.go b/daemon/execdriver/driver.go index 75d266fcf1..c0b7882371 100644 --- a/daemon/execdriver/driver.go +++ b/daemon/execdriver/driver.go @@ -210,4 +210,5 @@ type Command struct { LayerPaths []string `json:"layer_paths"` // Windows needs to know the layer paths and folder for a command LayerFolder string `json:"layer_folder"` Hostname string `json:"hostname"` // Windows sets the hostname in the execdriver + Isolated bool `json:"isolated"` // Windows: Isolated is a Hyper-V container rather than Windows Server Container } diff --git a/daemon/execdriver/windows/run.go b/daemon/execdriver/windows/run.go index ed66221881..058109f2ef 100644 --- a/daemon/execdriver/windows/run.go +++ b/daemon/execdriver/windows/run.go @@ -77,6 +77,8 @@ type containerInit struct { ProcessorWeight int64 // CPU Shares 1..9 on Windows; or 0 is platform default. HostName string // Hostname MappedDirectories []mappedDir // List of mapped directories (volumes/mounts) + SandboxPath string // Location of unmounted sandbox (used for Hyper-V containers, not Windows Server containers) + HvPartition bool // True if it a Hyper-V Container } // defaultOwner is a tag passed to HCS to allow it to differentiate between @@ -108,6 +110,14 @@ func (d *Driver) Run(c *execdriver.Command, pipes *execdriver.Pipes, hooks execd LayerFolderPath: c.LayerFolder, ProcessorWeight: c.Resources.CPUShares, HostName: c.Hostname, + HvPartition: c.Isolated, + } + + if c.Isolated { + cu.SandboxPath = filepath.Dir(c.LayerFolder) + } else { + cu.VolumePath = c.Rootfs + cu.LayerFolderPath = c.LayerFolder } for _, layerPath := range c.LayerPaths { diff --git a/runconfig/config.go b/runconfig/config.go index ced845f20e..47e895871e 100644 --- a/runconfig/config.go +++ b/runconfig/config.go @@ -75,6 +75,10 @@ func DecodeContainerConfig(src io.Reader) (*Config, *HostConfig, error) { return nil, nil, err } + // Validate the isolation level + if err := ValidateIsolationLevel(hc); err != nil { + return nil, nil, err + } return w.Config, hc, nil } diff --git a/runconfig/config_test.go b/runconfig/config_test.go index b4890aeb0c..b721cf1086 100644 --- a/runconfig/config_test.go +++ b/runconfig/config_test.go @@ -2,9 +2,11 @@ package runconfig import ( "bytes" + "encoding/json" "fmt" "io/ioutil" "runtime" + "strings" "testing" "github.com/docker/docker/pkg/stringutils" @@ -60,3 +62,58 @@ func TestDecodeContainerConfig(t *testing.T) { } } } + +// TestDecodeContainerConfigIsolation validates the isolation level passed +// to the daemon in the hostConfig structure. Note this is platform specific +// as to what level of container isolation is supported. +func TestDecodeContainerConfigIsolation(t *testing.T) { + + // An invalid isolation level + if _, _, err := callDecodeContainerConfigIsolation("invalid"); err != nil { + if !strings.Contains(err.Error(), `invalid --isolation: "invalid"`) { + t.Fatal(err) + } + } + + // Blank isolation level (== default) + if _, _, err := callDecodeContainerConfigIsolation(""); err != nil { + t.Fatal("Blank isolation should have succeeded") + } + + // Default isolation level + if _, _, err := callDecodeContainerConfigIsolation("default"); err != nil { + t.Fatal("default isolation should have succeeded") + } + + // Hyper-V Containers isolation level (Valid on Windows only) + if runtime.GOOS == "windows" { + if _, _, err := callDecodeContainerConfigIsolation("hyperv"); err != nil { + t.Fatal("hyperv isolation should have succeeded") + } + } else { + if _, _, err := callDecodeContainerConfigIsolation("hyperv"); err != nil { + if !strings.Contains(err.Error(), `invalid --isolation: "hyperv"`) { + t.Fatal(err) + } + } + } +} + +// callDecodeContainerConfigIsolation is a utility function to call +// DecodeContainerConfig for validating isolation levels +func callDecodeContainerConfigIsolation(isolation string) (*Config, *HostConfig, error) { + var ( + b []byte + err error + ) + w := ContainerConfigWrapper{ + Config: &Config{}, + HostConfig: &HostConfig{ + NetworkMode: "none", + Isolation: IsolationLevel(isolation)}, + } + if b, err = json.Marshal(w); err != nil { + return nil, nil, fmt.Errorf("Error on marshal %s", err.Error()) + } + return DecodeContainerConfig(bytes.NewReader(b)) +} diff --git a/runconfig/hostconfig.go b/runconfig/hostconfig.go index 7a7b8f3b37..0449fef76e 100644 --- a/runconfig/hostconfig.go +++ b/runconfig/hostconfig.go @@ -19,6 +19,16 @@ type KeyValuePair struct { // NetworkMode represents the container network stack. type NetworkMode string +// IsolationLevel represents the isolation level of a container. The supported +// values are platform specific +type IsolationLevel string + +// IsDefault indicates the default isolation level of a container. On Linux this +// is LXC. On Windows, this is a Windows Server Container. +func (i IsolationLevel) IsDefault() bool { + return strings.ToLower(string(i)) == "default" || string(i) == "" +} + // IpcMode represents the container ipc stack. type IpcMode string @@ -254,6 +264,7 @@ type HostConfig struct { CgroupParent string // Parent cgroup. ConsoleSize [2]int // Initial console size on Windows VolumeDriver string // Name of the volume driver used to mount volumes + Isolation IsolationLevel // Isolation level of the container (eg default, hyperv) } // DecodeHostConfig creates a HostConfig based on the specified Reader. diff --git a/runconfig/hostconfig_unix.go b/runconfig/hostconfig_unix.go index fd0b988eb8..0777d9ae0c 100644 --- a/runconfig/hostconfig_unix.go +++ b/runconfig/hostconfig_unix.go @@ -6,6 +6,11 @@ import ( "strings" ) +// IsValid indicates is an isolation level is valid +func (i IsolationLevel) IsValid() bool { + return i.IsDefault() +} + // IsPrivate indicates whether container uses it's private network stack. func (n NetworkMode) IsPrivate() bool { return !(n.IsHost() || n.IsContainer()) diff --git a/runconfig/hostconfig_windows.go b/runconfig/hostconfig_windows.go index d21e082280..0943800b0b 100644 --- a/runconfig/hostconfig_windows.go +++ b/runconfig/hostconfig_windows.go @@ -1,10 +1,23 @@ package runconfig +import "strings" + // IsDefault indicates whether container uses the default network stack. func (n NetworkMode) IsDefault() bool { return n == "default" } +// IsHyperV indicates the use of Hyper-V Containers for isolation (as opposed +// to Windows Server Containers +func (i IsolationLevel) IsHyperV() bool { + return strings.ToLower(string(i)) == "hyperv" +} + +// IsValid indicates is an isolation level is valid +func (i IsolationLevel) IsValid() bool { + return i.IsDefault() || i.IsHyperV() +} + // DefaultDaemonNetworkMode returns the default network stack the daemon should // use. func DefaultDaemonNetworkMode() NetworkMode { diff --git a/runconfig/parse.go b/runconfig/parse.go index 4bc2fee6aa..0ea7d9fd95 100644 --- a/runconfig/parse.go +++ b/runconfig/parse.go @@ -55,22 +55,21 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flUlimits = opts.NewUlimitOpt(nil) - flPublish = opts.NewListOpts(nil) - flExpose = opts.NewListOpts(nil) - flDNS = opts.NewListOpts(opts.ValidateIPAddress) - flDNSSearch = opts.NewListOpts(opts.ValidateDNSSearch) - flDNSOptions = opts.NewListOpts(nil) - flExtraHosts = opts.NewListOpts(opts.ValidateExtraHost) - flVolumesFrom = opts.NewListOpts(nil) - flLxcOpts = opts.NewListOpts(nil) - flEnvFile = opts.NewListOpts(nil) - flCapAdd = opts.NewListOpts(nil) - flCapDrop = opts.NewListOpts(nil) - flGroupAdd = opts.NewListOpts(nil) - flSecurityOpt = opts.NewListOpts(nil) - flLabelsFile = opts.NewListOpts(nil) - flLoggingOpts = opts.NewListOpts(nil) - + flPublish = opts.NewListOpts(nil) + flExpose = opts.NewListOpts(nil) + flDNS = opts.NewListOpts(opts.ValidateIPAddress) + flDNSSearch = opts.NewListOpts(opts.ValidateDNSSearch) + flDNSOptions = opts.NewListOpts(nil) + flExtraHosts = opts.NewListOpts(opts.ValidateExtraHost) + flVolumesFrom = opts.NewListOpts(nil) + flLxcOpts = opts.NewListOpts(nil) + flEnvFile = opts.NewListOpts(nil) + flCapAdd = opts.NewListOpts(nil) + flCapDrop = opts.NewListOpts(nil) + flGroupAdd = opts.NewListOpts(nil) + flSecurityOpt = opts.NewListOpts(nil) + flLabelsFile = opts.NewListOpts(nil) + flLoggingOpts = opts.NewListOpts(nil) flNetwork = cmd.Bool([]string{"#n", "#-networking"}, true, "Enable networking for this container") flPrivileged = cmd.Bool([]string{"#privileged", "-privileged"}, false, "Give extended privileges to this container") flPidMode = cmd.String([]string{"-pid"}, "", "PID namespace to use") @@ -104,6 +103,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe flCgroupParent = cmd.String([]string{"-cgroup-parent"}, "", "Optional parent cgroup for the container") flVolumeDriver = cmd.String([]string{"-volume-driver"}, "", "Optional volume driver for the container") flStopSignal = cmd.String([]string{"-stop-signal"}, signal.DefaultStopSignal, fmt.Sprintf("Signal to stop a container, %v by default", signal.DefaultStopSignal)) + flIsolation = cmd.String([]string{"-isolation"}, "default", "Container isolation level") ) cmd.Var(&flAttach, []string{"a", "-attach"}, "Attach to STDIN, STDOUT or STDERR") @@ -377,6 +377,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe LogConfig: LogConfig{Type: *flLoggingDriver, Config: loggingOpts}, CgroupParent: *flCgroupParent, VolumeDriver: *flVolumeDriver, + Isolation: IsolationLevel(*flIsolation), } // When allocating stdin in attached mode, close stdin at client disconnect diff --git a/runconfig/parse_unix.go b/runconfig/parse_unix.go index fc1aa16d04..adc92c9519 100644 --- a/runconfig/parse_unix.go +++ b/runconfig/parse_unix.go @@ -4,6 +4,7 @@ package runconfig import ( "fmt" + "runtime" "strings" ) @@ -58,3 +59,17 @@ func ValidateNetMode(c *Config, hc *HostConfig) error { } return nil } + +// ValidateIsolationLevel performs platform specific validation of the +// isolation level in the hostconfig structure. Linux only supports "default" +// which is LXC container isolation +func ValidateIsolationLevel(hc *HostConfig) error { + // We may not be passed a host config, such as in the case of docker commit + if hc == nil { + return nil + } + if !hc.Isolation.IsValid() { + return fmt.Errorf("invalid --isolation: %q - %s only supports 'default'", hc.Isolation, runtime.GOOS) + } + return nil +} diff --git a/runconfig/parse_windows.go b/runconfig/parse_windows.go index 01e39c7f85..d9c010c7d3 100644 --- a/runconfig/parse_windows.go +++ b/runconfig/parse_windows.go @@ -20,3 +20,18 @@ func ValidateNetMode(c *Config, hc *HostConfig) error { } return nil } + +// ValidateIsolationLevel performs platform specific validation of the +// isolation level in the hostconfig structure. Windows supports 'default' (or +// blank), and 'hyperv'. These refer to Windows Server Containers and +// Hyper-V Containers respectively. +func ValidateIsolationLevel(hc *HostConfig) error { + // We may not be passed a host config, such as in the case of docker commit + if hc == nil { + return nil + } + if !hc.Isolation.IsValid() { + return fmt.Errorf("invalid --isolation: %q. Windows supports 'default' (Windows Server Container) or 'hyperv' (Hyper-V Container)", hc.Isolation) + } + return nil +}