diff --git a/pkg/util/mount/mount_linux.go b/pkg/util/mount/mount_linux.go index e823d353a23..c4f2650e4d8 100644 --- a/pkg/util/mount/mount_linux.go +++ b/pkg/util/mount/mount_linux.go @@ -35,22 +35,43 @@ import ( const ( // How many times to retry for a consistent read of /proc/mounts. maxListTries = 3 - // Number of fields per line in "/proc/mounts", as per the fstab man page. + // Number of fields per line in /proc/mounts as per the fstab man page. expectedNumFieldsPerLine = 6 + // Location of the mount file to use + procMountsPath = "/proc/mounts" ) -// Mounter implements mount.Interface for linux platform. +// Mounter provides the default implementation of mount.Interface +// for the linux platform. This implementation assumes that the +// kubelet is running in the host's root mount namespace. type Mounter struct{} +var _ = Interface(&Mounter{}) + // Mount mounts source to target as fstype with given options. 'source' and 'fstype' must // be an emtpy string in case it's not required, e.g. for remount, or for auto filesystem // type, where kernel handles fs type for you. The mount 'options' is a list of options, // currently come from mount(8), e.g. "ro", "remount", "bind", etc. If no more option is // required, call Mount with an empty string list or nil. func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { - // The remount options to use in case of bind mount, due to the fact that bind mount doesn't - // respect mount options. The list equals: - // options - 'bind' + 'remount' (no duplicate) + bind, bindRemountOpts := isBind(options) + + if bind { + err := doMount(source, target, fstype, []string{"bind"}) + if err != nil { + return err + } + return doMount(source, target, fstype, bindRemountOpts) + } else { + return doMount(source, target, fstype, options) + } +} + +// isBind detects whether a bind mount is being requested and makes the remount options to +// use in case of bind mount, due to the fact that bind mount doesn't respect mount options. +// The list equals: +// options - 'bind' + 'remount' (no duplicate) +func isBind(options []string) (bool, []string) { bindRemountOpts := []string{"remount"} bind := false @@ -68,19 +89,24 @@ func (mounter *Mounter) Mount(source string, target string, fstype string, optio } } - if bind { - err := doMount(source, target, fstype, []string{"bind"}) - if err != nil { - return err - } - return doMount(source, target, fstype, bindRemountOpts) - } else { - return doMount(source, target, fstype, options) - } + return bind, bindRemountOpts } +// doMount runs the mount command. func doMount(source string, target string, fstype string, options []string) error { glog.V(5).Infof("Mounting %s %s %s %v", source, target, fstype, options) + mountArgs := makeMountArgs(source, target, fstype, options) + command := exec.Command("mount", mountArgs...) + output, err := command.CombinedOutput() + if err != nil { + glog.Errorf("Mount failed: %v\nMounting arguments: %s %s %s %v\nOutput: %s\n", + err, source, target, fstype, options, string(output)) + } + return err +} + +// makeMountArgs makes the arguments to the mount(8) command. +func makeMountArgs(source, target, fstype string, options []string) []string { // Build mount command as follows: // mount [-t $fstype] [-o $options] [$source] $target mountArgs := []string{} @@ -94,16 +120,11 @@ func doMount(source string, target string, fstype string, options []string) erro mountArgs = append(mountArgs, source) } mountArgs = append(mountArgs, target) - command := exec.Command("mount", mountArgs...) - output, err := command.CombinedOutput() - if err != nil { - glog.Errorf("Mount failed: %v\nMounting arguments: %s %s %s %v\nOutput: %s\n", - err, source, target, fstype, options, string(output)) - } - return err + + return mountArgs } -// Unmount unmounts target with given options. +// Unmount unmounts the target. func (mounter *Mounter) Unmount(target string) error { glog.V(5).Infof("Unmounting %s %v") command := exec.Command("umount", target) @@ -116,25 +137,8 @@ func (mounter *Mounter) Unmount(target string) error { } // List returns a list of all mounted filesystems. -func (mounter *Mounter) List() ([]MountPoint, error) { - hash1, err := readProcMounts(nil) - if err != nil { - return nil, err - } - - for i := 0; i < maxListTries; i++ { - mps := []MountPoint{} - hash2, err := readProcMounts(&mps) - if err != nil { - return nil, err - } - if hash1 == hash2 { - // Success - return mps, nil - } - hash1 = hash2 - } - return nil, fmt.Errorf("failed to get a consistent snapshot of /proc/mounts after %d tries", maxListTries) +func (*Mounter) List() ([]MountPoint, error) { + return listProcMounts(procMountsPath) } // IsMountPoint determines if a directory is a mountpoint, by comparing the device for the @@ -153,10 +157,31 @@ func (mounter *Mounter) IsMountPoint(file string) (bool, error) { return stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev, nil } -// readProcMounts reads /proc/mounts and produces a hash of the contents. If the out -// argument is not nil, this fills it with MountPoint structs. -func readProcMounts(out *[]MountPoint) (uint32, error) { - file, err := os.Open("/proc/mounts") +func listProcMounts(mountFilePath string) ([]MountPoint, error) { + hash1, err := readProcMounts(mountFilePath, nil) + if err != nil { + return nil, err + } + + for i := 0; i < maxListTries; i++ { + mps := []MountPoint{} + hash2, err := readProcMounts(mountFilePath, &mps) + if err != nil { + return nil, err + } + if hash1 == hash2 { + // Success + return mps, nil + } + hash1 = hash2 + } + return nil, fmt.Errorf("failed to get a consistent snapshot of %v after %d tries", mountFilePath, maxListTries) +} + +// readProcMounts reads the given mountFilePath (normally /proc/mounts) and produces a hash +// of the contents. If the out argument is not nil, this fills it with MountPoint structs. +func readProcMounts(mountFilePath string, out *[]MountPoint) (uint32, error) { + file, err := os.Open(mountFilePath) if err != nil { return 0, err } diff --git a/pkg/util/mount/nsenter_mount.go b/pkg/util/mount/nsenter_mount.go new file mode 100644 index 00000000000..197f57efaa1 --- /dev/null +++ b/pkg/util/mount/nsenter_mount.go @@ -0,0 +1,153 @@ +// +build linux + +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +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 mount + +import ( + "path/filepath" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/exec" + "github.com/golang/glog" +) + +// NsenterMounter is part of experimental support for running the kubelet +// in a container. Currently, all docker containers receive their own mount +// namespaces. NsenterMounter works by executing nsenter to run commands in +// the host's mount namespace. +// +// NsenterMounter requires: +// +// 1. Docker >= 1.6 due to the dependency on the slave propagation mode +// of the bind-mount of the kubelet root directory in the container. +// Docker 1.5 used a private propagation mode for bind-mounts, so mounts +// performed in the host's mount namespace do not propagate out to the +// bind-mount in this docker version. +// 2. The host's root filesystem must be available at /rootfs +// 3. The nsenter binary must be at /nsenter in the container's filesystem. +// 4. The Kubelet process must have CAP_SYS_ADMIN (required by nsenter); at +// the present, this effectively means that the kubelet is running in a +// privileged container. +// +// For more information about mount propagation modes, see: +// https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt +type NsenterMounter struct{} + +// NsenterMounter implements mount.Interface +var _ = Interface(&NsenterMounter{}) + +const ( + hostRootFsPath = "/rootfs" + hostProcMountsPath = "/rootfs/proc/mounts" + nsenterPath = "/nsenter" +) + +// Mount runs mount(8) in the host's root mount namespace. Aside from this +// aspect, Mount has the same semantics as the mounter returned by mount.New() +func (*NsenterMounter) Mount(source string, target string, fstype string, options []string) error { + bind, bindRemountOpts := isBind(options) + + if bind { + err := doNsenterMount(source, target, fstype, []string{"bind"}) + if err != nil { + return err + } + return doNsenterMount(source, target, fstype, bindRemountOpts) + } + + return doNsenterMount(source, target, fstype, options) +} + +// doNsenterMount nsenter's the host's mount namespace and performs the +// requested mount. +func doNsenterMount(source, target, fstype string, options []string) error { + glog.V(5).Infof("nsenter Mounting %s %s %s %v", source, target, fstype, options) + args := makeNsenterArgs(source, target, fstype, options) + + glog.V(5).Infof("Mount command: %v %v", nsenterPath, args) + exec := exec.New() + outputBytes, err := exec.Command(nsenterPath, args...).CombinedOutput() + if len(outputBytes) != 0 { + glog.V(5).Infof("Output from mount command: %v", string(outputBytes)) + } + + return err +} + +// makeNsenterArgs makes a list of argument to nsenter in order to do the +// requested mount. +func makeNsenterArgs(source, target, fstype string, options []string) []string { + nsenterArgs := []string{ + "--mount=/rootfs/proc/1/ns/mnt", + "/usr/bin/mount", + } + + args := makeMountArgs(source, target, fstype, options) + + return append(nsenterArgs, args...) +} + +// Unmount runs umount(8) in the host's mount namespace. +func (*NsenterMounter) Unmount(target string) error { + args := []string{ + "--mount=/rootfs/proc/1/ns/mnt", + "/usr/bin/umount", + target, + } + + glog.V(5).Infof("Unmount command: %v %v", nsenterPath, args) + exec := exec.New() + outputBytes, err := exec.Command(nsenterPath, args...).CombinedOutput() + if len(outputBytes) != 0 { + glog.V(5).Infof("Output from mount command: %v", string(outputBytes)) + } + + return err +} + +// List returns a list of all mounted filesystems in the host's mount namespace. +func (*NsenterMounter) List() ([]MountPoint, error) { + return listProcMounts(hostProcMountsPath) +} + +// IsMountPoint determines whether a path is a mountpoint by calling findmnt +// in the host's root mount namespace. +func (*NsenterMounter) IsMountPoint(file string) (bool, error) { + file, err := filepath.Abs(file) + if err != nil { + return false, err + } + + args := []string{"--mount=/rootfs/proc/1/ns/mnt", "/usr/bin/findmnt", "-o", "target", "--noheadings", "--target", file} + glog.V(5).Infof("findmnt command: %v %v", nsenterPath, args) + + exec := exec.New() + out, err := exec.Command(nsenterPath, args...).CombinedOutput() + if err != nil { + // If findmnt didn't run, just claim it's not a mount point. + return false, nil + } + strOut := strings.TrimSuffix(string(out), "\n") + + glog.V(5).Infof("IsMountPoint findmnt output: %v", strOut) + if strOut == file { + return true, nil + } + + return false, nil +}