diff --git a/pkg/util/mount/BUILD b/pkg/util/mount/BUILD index f4e40b669f9..3fdbbddbccb 100644 --- a/pkg/util/mount/BUILD +++ b/pkg/util/mount/BUILD @@ -6,6 +6,11 @@ go_library( "doc.go", "exec.go", "fake.go", + "fake_hostutil.go", + "hostutil.go", + "hostutil_linux.go", + "hostutil_unsupported.go", + "hostutil_windows.go", "mount.go", "mount_helper_common.go", "mount_helper_unix.go", @@ -20,11 +25,38 @@ go_library( "//vendor/k8s.io/klog:go_default_library", "//vendor/k8s.io/utils/exec:go_default_library", ] + select({ + "@io_bazel_rules_go//go/platform:android": [ + "//vendor/k8s.io/utils/io:go_default_library", + ], + "@io_bazel_rules_go//go/platform:darwin": [ + "//vendor/k8s.io/utils/io:go_default_library", + ], + "@io_bazel_rules_go//go/platform:dragonfly": [ + "//vendor/k8s.io/utils/io:go_default_library", + ], + "@io_bazel_rules_go//go/platform:freebsd": [ + "//vendor/k8s.io/utils/io:go_default_library", + ], "@io_bazel_rules_go//go/platform:linux": [ "//vendor/golang.org/x/sys/unix:go_default_library", "//vendor/k8s.io/utils/io:go_default_library", "//vendor/k8s.io/utils/path:go_default_library", ], + "@io_bazel_rules_go//go/platform:nacl": [ + "//vendor/k8s.io/utils/io:go_default_library", + ], + "@io_bazel_rules_go//go/platform:netbsd": [ + "//vendor/k8s.io/utils/io:go_default_library", + ], + "@io_bazel_rules_go//go/platform:openbsd": [ + "//vendor/k8s.io/utils/io:go_default_library", + ], + "@io_bazel_rules_go//go/platform:plan9": [ + "//vendor/k8s.io/utils/io:go_default_library", + ], + "@io_bazel_rules_go//go/platform:solaris": [ + "//vendor/k8s.io/utils/io:go_default_library", + ], "@io_bazel_rules_go//go/platform:windows": [ "//vendor/k8s.io/utils/keymutex:go_default_library", "//vendor/k8s.io/utils/path:go_default_library", @@ -36,7 +68,11 @@ go_library( go_test( name = "go_default_test", srcs = [ + "hostutil_linux_test.go", + "hostutil_windows_test.go", "mount_helper_test.go", + "mount_helper_unix_test.go", + "mount_helper_windows_test.go", "mount_linux_test.go", "mount_test.go", "mount_windows_test.go", diff --git a/pkg/util/mount/fake.go b/pkg/util/mount/fake.go index 3b85e672198..5aa34a399e6 100644 --- a/pkg/util/mount/fake.go +++ b/pkg/util/mount/fake.go @@ -17,7 +17,6 @@ limitations under the License. package mount import ( - "errors" "os" "path/filepath" "sync" @@ -185,96 +184,3 @@ func (f *FakeMounter) GetMountRefs(pathname string) ([]string, error) { } return getMountRefsByDev(f, realpath) } - -// FakeHostUtil is a fake mount.HostUtils implementation for testing -type FakeHostUtil struct { - MountPoints []MountPoint - Filesystem map[string]FileType - - mutex sync.Mutex -} - -var _ HostUtils = &FakeHostUtil{} - -// DeviceOpened checks if block device referenced by pathname is in use by -// checking if is listed as a device in the in-memory mountpoint table. -func (hu *FakeHostUtil) DeviceOpened(pathname string) (bool, error) { - hu.mutex.Lock() - defer hu.mutex.Unlock() - - for _, mp := range hu.MountPoints { - if mp.Device == pathname { - return true, nil - } - } - return false, nil -} - -// PathIsDevice always returns true -func (hu *FakeHostUtil) PathIsDevice(pathname string) (bool, error) { - return true, nil -} - -// GetDeviceNameFromMount given a mount point, find the volume id -func (hu *FakeHostUtil) GetDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { - return getDeviceNameFromMount(mounter, mountPath, pluginMountDir) -} - -// MakeRShared checks if path is shared and bind-mounts it as rshared if needed. -// No-op for testing -func (hu *FakeHostUtil) MakeRShared(path string) error { - return nil -} - -// GetFileType checks for file/directory/socket/block/character devices. -// Defaults to Directory if otherwise unspecified. -func (hu *FakeHostUtil) GetFileType(pathname string) (FileType, error) { - if t, ok := hu.Filesystem[pathname]; ok { - return t, nil - } - return FileType("Directory"), nil -} - -// MakeDir creates a new directory. -// No-op for testing -func (hu *FakeHostUtil) MakeDir(pathname string) error { - return nil -} - -// MakeFile creates a new file. -// No-op for testing -func (hu *FakeHostUtil) MakeFile(pathname string) error { - return nil -} - -// PathExists checks if pathname exists. -func (hu *FakeHostUtil) PathExists(pathname string) (bool, error) { - if _, ok := hu.Filesystem[pathname]; ok { - return true, nil - } - return false, nil -} - -// EvalHostSymlinks returns the path name after evaluating symlinks. -// No-op for testing -func (hu *FakeHostUtil) EvalHostSymlinks(pathname string) (string, error) { - return pathname, nil -} - -// GetOwner returns the integer ID for the user and group of the given path -// Not implemented for testing -func (hu *FakeHostUtil) GetOwner(pathname string) (int64, int64, error) { - return -1, -1, errors.New("GetOwner not implemented") -} - -// GetSELinuxSupport tests if pathname is on a mount that supports SELinux. -// Not implemented for testing -func (hu *FakeHostUtil) GetSELinuxSupport(pathname string) (bool, error) { - return false, errors.New("GetSELinuxSupport not implemented") -} - -// GetMode returns permissions of pathname. -// Not implemented for testing -func (hu *FakeHostUtil) GetMode(pathname string) (os.FileMode, error) { - return 0, errors.New("not implemented") -} diff --git a/pkg/util/mount/fake_hostutil.go b/pkg/util/mount/fake_hostutil.go new file mode 100644 index 00000000000..c85ff70081f --- /dev/null +++ b/pkg/util/mount/fake_hostutil.go @@ -0,0 +1,116 @@ +/* +Copyright 2015 The Kubernetes Authors. + +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 ( + "errors" + "os" + "sync" +) + +// FakeHostUtil is a fake mount.HostUtils implementation for testing +type FakeHostUtil struct { + MountPoints []MountPoint + Filesystem map[string]FileType + + mutex sync.Mutex +} + +var _ HostUtils = &FakeHostUtil{} + +// DeviceOpened checks if block device referenced by pathname is in use by +// checking if is listed as a device in the in-memory mountpoint table. +func (hu *FakeHostUtil) DeviceOpened(pathname string) (bool, error) { + hu.mutex.Lock() + defer hu.mutex.Unlock() + + for _, mp := range hu.MountPoints { + if mp.Device == pathname { + return true, nil + } + } + return false, nil +} + +// PathIsDevice always returns true +func (hu *FakeHostUtil) PathIsDevice(pathname string) (bool, error) { + return true, nil +} + +// GetDeviceNameFromMount given a mount point, find the volume id +func (hu *FakeHostUtil) GetDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { + return getDeviceNameFromMount(mounter, mountPath, pluginMountDir) +} + +// MakeRShared checks if path is shared and bind-mounts it as rshared if needed. +// No-op for testing +func (hu *FakeHostUtil) MakeRShared(path string) error { + return nil +} + +// GetFileType checks for file/directory/socket/block/character devices. +// Defaults to Directory if otherwise unspecified. +func (hu *FakeHostUtil) GetFileType(pathname string) (FileType, error) { + if t, ok := hu.Filesystem[pathname]; ok { + return t, nil + } + return FileType("Directory"), nil +} + +// MakeDir creates a new directory. +// No-op for testing +func (hu *FakeHostUtil) MakeDir(pathname string) error { + return nil +} + +// MakeFile creates a new file. +// No-op for testing +func (hu *FakeHostUtil) MakeFile(pathname string) error { + return nil +} + +// PathExists checks if pathname exists. +func (hu *FakeHostUtil) PathExists(pathname string) (bool, error) { + if _, ok := hu.Filesystem[pathname]; ok { + return true, nil + } + return false, nil +} + +// EvalHostSymlinks returns the path name after evaluating symlinks. +// No-op for testing +func (hu *FakeHostUtil) EvalHostSymlinks(pathname string) (string, error) { + return pathname, nil +} + +// GetOwner returns the integer ID for the user and group of the given path +// Not implemented for testing +func (hu *FakeHostUtil) GetOwner(pathname string) (int64, int64, error) { + return -1, -1, errors.New("GetOwner not implemented") +} + +// GetSELinuxSupport tests if pathname is on a mount that supports SELinux. +// Not implemented for testing +func (hu *FakeHostUtil) GetSELinuxSupport(pathname string) (bool, error) { + return false, errors.New("GetSELinuxSupport not implemented") +} + +// GetMode returns permissions of pathname. +// Not implemented for testing +func (hu *FakeHostUtil) GetMode(pathname string) (os.FileMode, error) { + return 0, errors.New("not implemented") +} diff --git a/pkg/util/mount/hostutil.go b/pkg/util/mount/hostutil.go new file mode 100644 index 00000000000..25d8c17ce2a --- /dev/null +++ b/pkg/util/mount/hostutil.go @@ -0,0 +1,114 @@ +/* +Copyright 2014 The Kubernetes Authors. + +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. +*/ + +// TODO(thockin): This whole pkg is pretty linux-centric. As soon as we have +// an alternate platform, we will need to abstract further. + +package mount + +import ( + "fmt" + "os" +) + +// FileType enumerates the known set of possible file types. +type FileType string + +const ( + // FileTypeBlockDev defines a constant for the block device FileType. + FileTypeBlockDev FileType = "BlockDevice" + // FileTypeCharDev defines a constant for the character device FileType. + FileTypeCharDev FileType = "CharDevice" + // FileTypeDirectory defines a constant for the directory FileType. + FileTypeDirectory FileType = "Directory" + // FileTypeFile defines a constant for the file FileType. + FileTypeFile FileType = "File" + // FileTypeSocket defines a constant for the socket FileType. + FileTypeSocket FileType = "Socket" + // FileTypeUnknown defines a constant for an unknown FileType. + FileTypeUnknown FileType = "" +) + +// HostUtils defines the set of methods for interacting with paths on a host. +type HostUtils interface { + // DeviceOpened determines if the device (e.g. /dev/sdc) is in use elsewhere + // on the system, i.e. still mounted. + DeviceOpened(pathname string) (bool, error) + // PathIsDevice determines if a path is a device. + PathIsDevice(pathname string) (bool, error) + // GetDeviceNameFromMount finds the device name by checking the mount path + // to get the global mount path within its plugin directory. + GetDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) + // MakeRShared checks that given path is on a mount with 'rshared' mount + // propagation. If not, it bind-mounts the path as rshared. + MakeRShared(path string) error + // GetFileType checks for file/directory/socket/block/character devices. + GetFileType(pathname string) (FileType, error) + // MakeFile creates an empty file. + MakeFile(pathname string) error + // MakeDir creates a new directory. + MakeDir(pathname string) error + // PathExists tests if the given path already exists + // Error is returned on any other error than "file not found". + PathExists(pathname string) (bool, error) + // EvalHostSymlinks returns the path name after evaluating symlinks. + EvalHostSymlinks(pathname string) (string, error) + // GetOwner returns the integer ID for the user and group of the given path + GetOwner(pathname string) (int64, int64, error) + // GetSELinuxSupport returns true if given path is on a mount that supports + // SELinux. + GetSELinuxSupport(pathname string) (bool, error) + // GetMode returns permissions of the path. + GetMode(pathname string) (os.FileMode, error) +} + +// Compile-time check to ensure all HostUtil implementations satisfy +// the HostUtils Interface. +var _ HostUtils = &hostUtil{} + +// getFileType checks for file/directory/socket and block/character devices. +func getFileType(pathname string) (FileType, error) { + var pathType FileType + info, err := os.Stat(pathname) + if os.IsNotExist(err) { + return pathType, fmt.Errorf("path %q does not exist", pathname) + } + // err in call to os.Stat + if err != nil { + return pathType, err + } + + // checks whether the mode is the target mode. + isSpecificMode := func(mode, targetMode os.FileMode) bool { + return mode&targetMode == targetMode + } + + mode := info.Mode() + if mode.IsDir() { + return FileTypeDirectory, nil + } else if mode.IsRegular() { + return FileTypeFile, nil + } else if isSpecificMode(mode, os.ModeSocket) { + return FileTypeSocket, nil + } else if isSpecificMode(mode, os.ModeDevice) { + if isSpecificMode(mode, os.ModeCharDevice) { + return FileTypeCharDev, nil + } + return FileTypeBlockDev, nil + } + + return pathType, fmt.Errorf("only recognise file, directory, socket, block device and character device") +} diff --git a/pkg/util/mount/hostutil_linux.go b/pkg/util/mount/hostutil_linux.go new file mode 100644 index 00000000000..e995a5c8715 --- /dev/null +++ b/pkg/util/mount/hostutil_linux.go @@ -0,0 +1,294 @@ +// +build linux + +/* +Copyright 2014 The Kubernetes Authors. + +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 ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + "syscall" + + "golang.org/x/sys/unix" + "k8s.io/klog" + utilpath "k8s.io/utils/path" +) + +type hostUtil struct { +} + +// NewHostUtil returns a struct that implements the HostUtils interface on +// linux platforms +func NewHostUtil() HostUtils { + return &hostUtil{} +} + +// DeviceOpened checks if block device in use by calling Open with O_EXCL flag. +// If pathname is not a device, log and return false with nil error. +// If open returns errno EBUSY, return true with nil error. +// If open returns nil, return false with nil error. +// Otherwise, return false with error +func (hu *hostUtil) DeviceOpened(pathname string) (bool, error) { + return ExclusiveOpenFailsOnDevice(pathname) +} + +// PathIsDevice uses FileInfo returned from os.Stat to check if path refers +// to a device. +func (hu *hostUtil) PathIsDevice(pathname string) (bool, error) { + pathType, err := hu.GetFileType(pathname) + isDevice := pathType == FileTypeCharDev || pathType == FileTypeBlockDev + return isDevice, err +} + +// ExclusiveOpenFailsOnDevice is shared with NsEnterMounter +func ExclusiveOpenFailsOnDevice(pathname string) (bool, error) { + var isDevice bool + finfo, err := os.Stat(pathname) + if os.IsNotExist(err) { + isDevice = false + } + // err in call to os.Stat + if err != nil { + return false, fmt.Errorf( + "PathIsDevice failed for path %q: %v", + pathname, + err) + } + // path refers to a device + if finfo.Mode()&os.ModeDevice != 0 { + isDevice = true + } + + if !isDevice { + klog.Errorf("Path %q is not referring to a device.", pathname) + return false, nil + } + fd, errno := unix.Open(pathname, unix.O_RDONLY|unix.O_EXCL|unix.O_CLOEXEC, 0) + // If the device is in use, open will return an invalid fd. + // When this happens, it is expected that Close will fail and throw an error. + defer unix.Close(fd) + if errno == nil { + // device not in use + return false, nil + } else if errno == unix.EBUSY { + // device is in use + return true, nil + } + // error during call to Open + return false, errno +} + +//GetDeviceNameFromMount: given a mount point, find the device name from its global mount point +func (hu *hostUtil) GetDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { + return GetDeviceNameFromMountLinux(mounter, mountPath, pluginMountDir) +} + +func getDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { + return GetDeviceNameFromMountLinux(mounter, mountPath, pluginMountDir) +} + +// GetDeviceNameFromMountLinux find the device name from /proc/mounts in which +// the mount path reference should match the given plugin mount directory. In case no mount path reference +// matches, returns the volume name taken from its given mountPath +// This implementation is shared with NsEnterMounter +func GetDeviceNameFromMountLinux(mounter Interface, mountPath, pluginMountDir string) (string, error) { + refs, err := mounter.GetMountRefs(mountPath) + if err != nil { + klog.V(4).Infof("GetMountRefs failed for mount path %q: %v", mountPath, err) + return "", err + } + if len(refs) == 0 { + klog.V(4).Infof("Directory %s is not mounted", mountPath) + return "", fmt.Errorf("directory %s is not mounted", mountPath) + } + for _, ref := range refs { + if strings.HasPrefix(ref, pluginMountDir) { + volumeID, err := filepath.Rel(pluginMountDir, ref) + if err != nil { + klog.Errorf("Failed to get volume id from mount %s - %v", mountPath, err) + return "", err + } + return volumeID, nil + } + } + + return path.Base(mountPath), nil +} + +func (hu *hostUtil) MakeRShared(path string) error { + return DoMakeRShared(path, procMountInfoPath) +} + +func (hu *hostUtil) GetFileType(pathname string) (FileType, error) { + return getFileType(pathname) +} + +func (hu *hostUtil) MakeDir(pathname string) error { + err := os.MkdirAll(pathname, os.FileMode(0755)) + if err != nil { + if !os.IsExist(err) { + return err + } + } + return nil +} + +func (hu *hostUtil) MakeFile(pathname string) error { + f, err := os.OpenFile(pathname, os.O_CREATE, os.FileMode(0644)) + defer f.Close() + if err != nil { + if !os.IsExist(err) { + return err + } + } + return nil +} + +func (hu *hostUtil) PathExists(pathname string) (bool, error) { + return utilpath.Exists(utilpath.CheckFollowSymlink, pathname) +} + +func (hu *hostUtil) EvalHostSymlinks(pathname string) (string, error) { + return filepath.EvalSymlinks(pathname) +} + +// isShared returns true, if given path is on a mount point that has shared +// mount propagation. +func isShared(mount string, mountInfoPath string) (bool, error) { + info, err := findMountInfo(mount, mountInfoPath) + if err != nil { + return false, err + } + + // parse optional parameters + for _, opt := range info.optionalFields { + if strings.HasPrefix(opt, "shared:") { + return true, nil + } + } + return false, nil +} + +func findMountInfo(path, mountInfoPath string) (mountInfo, error) { + infos, err := parseMountInfo(mountInfoPath) + if err != nil { + return mountInfo{}, err + } + + // process /proc/xxx/mountinfo in backward order and find the first mount + // point that is prefix of 'path' - that's the mount where path resides + var info *mountInfo + for i := len(infos) - 1; i >= 0; i-- { + if PathWithinBase(path, infos[i].mountPoint) { + info = &infos[i] + break + } + } + if info == nil { + return mountInfo{}, fmt.Errorf("cannot find mount point for %q", path) + } + return *info, nil +} + +// DoMakeRShared is common implementation of MakeRShared on Linux. It checks if +// path is shared and bind-mounts it as rshared if needed. mountCmd and +// mountArgs are expected to contain mount-like command, DoMakeRShared will add +// '--bind ' and '--make-rshared ' to mountArgs. +func DoMakeRShared(path string, mountInfoFilename string) error { + shared, err := isShared(path, mountInfoFilename) + if err != nil { + return err + } + if shared { + klog.V(4).Infof("Directory %s is already on a shared mount", path) + return nil + } + + klog.V(2).Infof("Bind-mounting %q with shared mount propagation", path) + // mount --bind /var/lib/kubelet /var/lib/kubelet + if err := syscall.Mount(path, path, "" /*fstype*/, syscall.MS_BIND, "" /*data*/); err != nil { + return fmt.Errorf("failed to bind-mount %s: %v", path, err) + } + + // mount --make-rshared /var/lib/kubelet + if err := syscall.Mount(path, path, "" /*fstype*/, syscall.MS_SHARED|syscall.MS_REC, "" /*data*/); err != nil { + return fmt.Errorf("failed to make %s rshared: %v", path, err) + } + + return nil +} + +// GetSELinux is common implementation of GetSELinuxSupport on Linux. +func GetSELinux(path string, mountInfoFilename string) (bool, error) { + info, err := findMountInfo(path, mountInfoFilename) + if err != nil { + return false, err + } + + // "seclabel" can be both in mount options and super options. + for _, opt := range info.superOptions { + if opt == "seclabel" { + return true, nil + } + } + for _, opt := range info.mountOptions { + if opt == "seclabel" { + return true, nil + } + } + return false, nil +} + +func (hu *hostUtil) GetSELinuxSupport(pathname string) (bool, error) { + return GetSELinux(pathname, procMountInfoPath) +} + +// GetOwner returns the integer ID for the user and group of the given path +func (hu *hostUtil) GetOwner(pathname string) (int64, int64, error) { + realpath, err := filepath.EvalSymlinks(pathname) + if err != nil { + return -1, -1, err + } + return GetOwnerLinux(realpath) +} + +func (hu *hostUtil) GetMode(pathname string) (os.FileMode, error) { + return GetModeLinux(pathname) +} + +// GetOwnerLinux is shared between Linux and NsEnterMounter +// pathname must already be evaluated for symlinks +func GetOwnerLinux(pathname string) (int64, int64, error) { + info, err := os.Stat(pathname) + if err != nil { + return -1, -1, err + } + stat := info.Sys().(*syscall.Stat_t) + return int64(stat.Uid), int64(stat.Gid), nil +} + +// GetModeLinux is shared between Linux and NsEnterMounter +func GetModeLinux(pathname string) (os.FileMode, error) { + info, err := os.Stat(pathname) + if err != nil { + return 0, err + } + return info.Mode(), nil +} diff --git a/pkg/util/mount/hostutil_linux_test.go b/pkg/util/mount/hostutil_linux_test.go new file mode 100644 index 00000000000..aec45afeb84 --- /dev/null +++ b/pkg/util/mount/hostutil_linux_test.go @@ -0,0 +1,313 @@ +// +build linux + +/* +Copyright 2014 The Kubernetes Authors. + +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 ( + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + "strings" + "testing" + + "k8s.io/utils/exec" +) + +func TestIsSharedSuccess(t *testing.T) { + successMountInfo := + `62 0 253:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +76 62 8:1 / /boot rw,relatime shared:29 - ext4 /dev/sda1 rw,seclabel,data=ordered +78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel +80 62 0:42 / /var/lib/nfs/rpc_pipefs rw,relatime shared:31 - rpc_pipefs sunrpc rw +82 62 0:43 / /var/lib/foo rw,relatime shared:32 - tmpfs tmpfs rw +83 63 0:44 / /var/lib/bar rw,relatime - tmpfs tmpfs rw +227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +224 62 253:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +` + tempDir, filename, err := writeFile(successMountInfo) + if err != nil { + t.Fatalf("cannot create temporary file: %v", err) + } + defer os.RemoveAll(tempDir) + + tests := []struct { + name string + path string + expectedResult bool + }{ + { + // /var/lib/kubelet is a directory on mount '/' that is shared + // This is the most common case. + "shared", + "/var/lib/kubelet", + true, + }, + { + // 8a2a... is a directory on mount /var/lib/docker/devicemapper + // that is private. + "private", + "/var/lib/docker/devicemapper/mnt/8a2a5c19eefb06d6f851dfcb240f8c113427f5b49b19658b5c60168e88267693/", + false, + }, + { + // 'directory' is a directory on mount + // /var/lib/docker/devicemapper/test/shared that is shared, but one + // of its parent is private. + "nested-shared", + "/var/lib/docker/devicemapper/test/shared/my/test/directory", + true, + }, + { + // /var/lib/foo is a mount point and it's shared + "shared-mount", + "/var/lib/foo", + true, + }, + { + // /var/lib/bar is a mount point and it's private + "private-mount", + "/var/lib/bar", + false, + }, + } + for _, test := range tests { + ret, err := isShared(test.path, filename) + if err != nil { + t.Errorf("test %s got unexpected error: %v", test.name, err) + } + if ret != test.expectedResult { + t.Errorf("test %s expected %v, got %v", test.name, test.expectedResult, ret) + } + } +} + +func TestIsSharedFailure(t *testing.T) { + errorTests := []struct { + name string + content string + }{ + { + // the first line is too short + name: "too-short-line", + content: `62 0 253:0 / / rw,relatime +76 62 8:1 / /boot rw,relatime shared:29 - ext4 /dev/sda1 rw,seclabel,data=ordered +78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel +80 62 0:42 / /var/lib/nfs/rpc_pipefs rw,relatime shared:31 - rpc_pipefs sunrpc rw +227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +224 62 253:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +`, + }, + { + // there is no root mount + name: "no-root-mount", + content: `76 62 8:1 / /boot rw,relatime shared:29 - ext4 /dev/sda1 rw,seclabel,data=ordered +78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel +80 62 0:42 / /var/lib/nfs/rpc_pipefs rw,relatime shared:31 - rpc_pipefs sunrpc rw +227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +224 62 253:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +`, + }, + } + for _, test := range errorTests { + tempDir, filename, err := writeFile(test.content) + if err != nil { + t.Fatalf("cannot create temporary file: %v", err) + } + defer os.RemoveAll(tempDir) + + _, err = isShared("/", filename) + if err == nil { + t.Errorf("test %q: expected error, got none", test.name) + } + } +} + +func TestGetSELinuxSupport(t *testing.T) { + info := + `62 0 253:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel +83 63 0:44 / /var/lib/bar rw,relatime - tmpfs tmpfs rw +227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +150 23 1:58 / /media/nfs_vol rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 +` + tempDir, filename, err := writeFile(info) + if err != nil { + t.Fatalf("cannot create temporary file: %v", err) + } + defer os.RemoveAll(tempDir) + + tests := []struct { + name string + mountPoint string + expectedResult bool + }{ + { + "ext4 on /", + "/", + true, + }, + { + "tmpfs on /var/lib/bar", + "/var/lib/bar", + false, + }, + { + "nfsv4", + "/media/nfs_vol", + false, + }, + } + + for _, test := range tests { + out, err := GetSELinux(test.mountPoint, filename) + if err != nil { + t.Errorf("Test %s failed with error: %s", test.name, err) + } + if test.expectedResult != out { + t.Errorf("Test %s failed: expected %v, got %v", test.name, test.expectedResult, out) + } + } +} + +func createSocketFile(socketDir string) (string, error) { + testSocketFile := filepath.Join(socketDir, "mt.sock") + + // Switch to volume path and create the socket file + // socket file can not have length of more than 108 character + // and hence we must use relative path + oldDir, _ := os.Getwd() + + err := os.Chdir(socketDir) + if err != nil { + return "", err + } + defer func() { + os.Chdir(oldDir) + }() + _, socketCreateError := net.Listen("unix", "mt.sock") + return testSocketFile, socketCreateError +} + +func TestGetFileType(t *testing.T) { + hu := NewHostUtil() + + testCase := []struct { + name string + expectedType FileType + setUp func() (string, string, error) + }{ + { + "Directory Test", + FileTypeDirectory, + func() (string, string, error) { + tempDir, err := ioutil.TempDir("", "test-get-filetype-") + return tempDir, tempDir, err + }, + }, + { + "File Test", + FileTypeFile, + func() (string, string, error) { + tempFile, err := ioutil.TempFile("", "test-get-filetype") + if err != nil { + return "", "", err + } + tempFile.Close() + return tempFile.Name(), tempFile.Name(), nil + }, + }, + { + "Socket Test", + FileTypeSocket, + func() (string, string, error) { + tempDir, err := ioutil.TempDir("", "test-get-filetype-") + if err != nil { + return "", "", err + } + tempSocketFile, err := createSocketFile(tempDir) + return tempSocketFile, tempDir, err + }, + }, + { + "Block Device Test", + FileTypeBlockDev, + func() (string, string, error) { + tempDir, err := ioutil.TempDir("", "test-get-filetype-") + if err != nil { + return "", "", err + } + + tempBlockFile := filepath.Join(tempDir, "test_blk_dev") + outputBytes, err := exec.New().Command("mknod", tempBlockFile, "b", "89", "1").CombinedOutput() + if err != nil { + err = fmt.Errorf("%v: %s ", err, outputBytes) + } + return tempBlockFile, tempDir, err + }, + }, + { + "Character Device Test", + FileTypeCharDev, + func() (string, string, error) { + tempDir, err := ioutil.TempDir("", "test-get-filetype-") + if err != nil { + return "", "", err + } + + tempCharFile := filepath.Join(tempDir, "test_char_dev") + outputBytes, err := exec.New().Command("mknod", tempCharFile, "c", "89", "1").CombinedOutput() + if err != nil { + err = fmt.Errorf("%v: %s ", err, outputBytes) + } + return tempCharFile, tempDir, err + }, + }, + } + + for idx, tc := range testCase { + path, cleanUpPath, err := tc.setUp() + if err != nil { + // Locally passed, but upstream CI is not friendly to create such device files + // Leave "Operation not permitted" out, which can be covered in an e2e test + if isOperationNotPermittedError(err) { + continue + } + t.Fatalf("[%d-%s] unexpected error : %v", idx, tc.name, err) + } + if len(cleanUpPath) > 0 { + defer os.RemoveAll(cleanUpPath) + } + + fileType, err := hu.GetFileType(path) + if err != nil { + t.Fatalf("[%d-%s] unexpected error : %v", idx, tc.name, err) + } + if fileType != tc.expectedType { + t.Fatalf("[%d-%s] expected %s, but got %s", idx, tc.name, tc.expectedType, fileType) + } + } +} + +func isOperationNotPermittedError(err error) bool { + if strings.Contains(err.Error(), "Operation not permitted") { + return true + } + return false +} diff --git a/pkg/util/mount/hostutil_unsupported.go b/pkg/util/mount/hostutil_unsupported.go new file mode 100644 index 00000000000..d4b05137156 --- /dev/null +++ b/pkg/util/mount/hostutil_unsupported.go @@ -0,0 +1,85 @@ +// +build !linux,!windows + +/* +Copyright 2014 The Kubernetes Authors. + +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 ( + "os" +) + +type hostUtil struct{} + +// NewHostUtil returns a struct that implements the HostUtils interface on +// unsupported platforms +func NewHostUtil() HostUtils { + return &hostUtil{} +} + +// DeviceOpened determines if the device is in use elsewhere +func (hu *hostUtil) DeviceOpened(pathname string) (bool, error) { + return false, errUnsupported +} + +// PathIsDevice determines if a path is a device. +func (hu *hostUtil) PathIsDevice(pathname string) (bool, error) { + return true, errUnsupported +} + +// GetDeviceNameFromMount finds the device name by checking the mount path +// to get the global mount path within its plugin directory +func (hu *hostUtil) GetDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { + return "", errUnsupported +} + +func (hu *hostUtil) MakeRShared(path string) error { + return errUnsupported +} + +func (hu *hostUtil) GetFileType(pathname string) (FileType, error) { + return FileType("fake"), errUnsupported +} + +func (hu *hostUtil) MakeFile(pathname string) error { + return errUnsupported +} + +func (hu *hostUtil) MakeDir(pathname string) error { + return errUnsupported +} + +func (hu *hostUtil) PathExists(pathname string) (bool, error) { + return true, errUnsupported +} + +// EvalHostSymlinks returns the path name after evaluating symlinks +func (hu *hostUtil) EvalHostSymlinks(pathname string) (string, error) { + return "", errUnsupported +} + +// GetOwner returns the integer ID for the user and group of the given path +func (hu *hostUtil) GetOwner(pathname string) (int64, int64, error) { + return -1, -1, errUnsupported +} + +func (hu *hostUtil) GetSELinuxSupport(pathname string) (bool, error) { + return false, errUnsupported +} + +func (hu *hostUtil) GetMode(pathname string) (os.FileMode, error) { + return 0, errUnsupported +} diff --git a/pkg/util/mount/hostutil_windows.go b/pkg/util/mount/hostutil_windows.go new file mode 100644 index 00000000000..20f340a9cde --- /dev/null +++ b/pkg/util/mount/hostutil_windows.go @@ -0,0 +1,145 @@ +// +build windows + +/* +Copyright 2017 The Kubernetes Authors. + +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 ( + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "k8s.io/klog" + + utilpath "k8s.io/utils/path" +) + +type hostUtil struct{} + +// NewHostUtil returns a struct that implements the HostUtils interface on +// windows platforms +func NewHostUtil() HostUtils { + return &hostUtil{} +} + +// GetDeviceNameFromMount given a mnt point, find the device +func (hu *hostUtil) GetDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { + return getDeviceNameFromMount(mounter, mountPath, pluginMountDir) +} + +// getDeviceNameFromMount find the device(drive) name in which +// the mount path reference should match the given plugin mount directory. In case no mount path reference +// matches, returns the volume name taken from its given mountPath +func getDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { + refs, err := mounter.GetMountRefs(mountPath) + if err != nil { + klog.V(4).Infof("GetMountRefs failed for mount path %q: %v", mountPath, err) + return "", err + } + if len(refs) == 0 { + return "", fmt.Errorf("directory %s is not mounted", mountPath) + } + basemountPath := NormalizeWindowsPath(pluginMountDir) + for _, ref := range refs { + if strings.Contains(ref, basemountPath) { + volumeID, err := filepath.Rel(NormalizeWindowsPath(basemountPath), ref) + if err != nil { + klog.Errorf("Failed to get volume id from mount %s - %v", mountPath, err) + return "", err + } + return volumeID, nil + } + } + + return path.Base(mountPath), nil +} + +// DeviceOpened determines if the device is in use elsewhere +func (hu *hostUtil) DeviceOpened(pathname string) (bool, error) { + return false, nil +} + +// PathIsDevice determines if a path is a device. +func (hu *hostUtil) PathIsDevice(pathname string) (bool, error) { + return false, nil +} + +// MakeRShared checks that given path is on a mount with 'rshared' mount +// propagation. Empty implementation here. +func (hu *hostUtil) MakeRShared(path string) error { + return nil +} + +// GetFileType checks for sockets/block/character devices +func (hu *(hostUtil)) GetFileType(pathname string) (FileType, error) { + return getFileType(pathname) +} + +// MakeFile creates a new directory +func (hu *hostUtil) MakeDir(pathname string) error { + err := os.MkdirAll(pathname, os.FileMode(0755)) + if err != nil { + if !os.IsExist(err) { + return err + } + } + return nil +} + +// MakeFile creates an empty file +func (hu *hostUtil) MakeFile(pathname string) error { + f, err := os.OpenFile(pathname, os.O_CREATE, os.FileMode(0644)) + defer f.Close() + if err != nil { + if !os.IsExist(err) { + return err + } + } + return nil +} + +// PathExists checks whether the path exists +func (hu *hostUtil) PathExists(pathname string) (bool, error) { + return utilpath.Exists(utilpath.CheckFollowSymlink, pathname) +} + +// EvalHostSymlinks returns the path name after evaluating symlinks +func (hu *hostUtil) EvalHostSymlinks(pathname string) (string, error) { + return filepath.EvalSymlinks(pathname) +} + +// GetOwner returns the integer ID for the user and group of the given path +// Note that on windows, it always returns 0. We actually don't set Group on +// windows platform, see SetVolumeOwnership implementation. +func (hu *hostUtil) GetOwner(pathname string) (int64, int64, error) { + return -1, -1, nil +} + +func (hu *hostUtil) GetSELinuxSupport(pathname string) (bool, error) { + // Windows does not support SELinux. + return false, nil +} + +func (hu *hostUtil) GetMode(pathname string) (os.FileMode, error) { + info, err := os.Stat(pathname) + if err != nil { + return 0, err + } + return info.Mode(), nil +} diff --git a/pkg/util/mount/hostutil_windows_test.go b/pkg/util/mount/hostutil_windows_test.go new file mode 100644 index 00000000000..1a17c05d29e --- /dev/null +++ b/pkg/util/mount/hostutil_windows_test.go @@ -0,0 +1,74 @@ +// +build windows + +/* +Copyright 2017 The Kubernetes Authors. + +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 ( + "io/ioutil" + "os" + "testing" +) + +func TestGetFileType(t *testing.T) { + hu := NewHostUtil() + + testCase := []struct { + name string + expectedType FileType + setUp func() (string, string, error) + }{ + { + "Directory Test", + FileTypeDirectory, + func() (string, string, error) { + tempDir, err := ioutil.TempDir("", "test-get-filetype-") + return tempDir, tempDir, err + }, + }, + { + "File Test", + FileTypeFile, + func() (string, string, error) { + tempFile, err := ioutil.TempFile("", "test-get-filetype") + if err != nil { + return "", "", err + } + tempFile.Close() + return tempFile.Name(), tempFile.Name(), nil + }, + }, + } + + for idx, tc := range testCase { + path, cleanUpPath, err := tc.setUp() + if err != nil { + t.Fatalf("[%d-%s] unexpected error : %v", idx, tc.name, err) + } + if len(cleanUpPath) > 0 { + defer os.RemoveAll(cleanUpPath) + } + + fileType, err := hu.GetFileType(path) + if err != nil { + t.Fatalf("[%d-%s] unexpected error : %v", idx, tc.name, err) + } + if fileType != tc.expectedType { + t.Fatalf("[%d-%s] expected %s, but got %s", idx, tc.name, tc.expectedType, fileType) + } + } +} diff --git a/pkg/util/mount/mount.go b/pkg/util/mount/mount.go index fe39605c9c9..c303e129d02 100644 --- a/pkg/util/mount/mount.go +++ b/pkg/util/mount/mount.go @@ -20,31 +20,14 @@ limitations under the License. package mount import ( - "fmt" "os" "path/filepath" "strings" ) -// FileType enumerates the known set of possible file types. -type FileType string - const ( // Default mount command if mounter path is not specified. defaultMountCommand = "mount" - - // FileTypeBlockDev defines a constant for the block device FileType. - FileTypeBlockDev FileType = "BlockDevice" - // FileTypeCharDev defines a constant for the character device FileType. - FileTypeCharDev FileType = "CharDevice" - // FileTypeDirectory defines a constant for the directory FileType. - FileTypeDirectory FileType = "Directory" - // FileTypeFile defines a constant for the file FileType. - FileTypeFile FileType = "File" - // FileTypeSocket defines a constant for the socket FileType. - FileTypeSocket FileType = "Socket" - // FileTypeUnknown defines a constant for an unknown FileType. - FileTypeUnknown FileType = "" ) // Interface defines the set of methods to allow for mount operations on a system. @@ -72,39 +55,6 @@ type Interface interface { GetMountRefs(pathname string) ([]string, error) } -// HostUtils defines the set of methods for interacting with paths on a host. -type HostUtils interface { - // DeviceOpened determines if the device (e.g. /dev/sdc) is in use elsewhere - // on the system, i.e. still mounted. - DeviceOpened(pathname string) (bool, error) - // PathIsDevice determines if a path is a device. - PathIsDevice(pathname string) (bool, error) - // GetDeviceNameFromMount finds the device name by checking the mount path - // to get the global mount path within its plugin directory. - GetDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) - // MakeRShared checks that given path is on a mount with 'rshared' mount - // propagation. If not, it bind-mounts the path as rshared. - MakeRShared(path string) error - // GetFileType checks for file/directory/socket/block/character devices. - GetFileType(pathname string) (FileType, error) - // MakeFile creates an empty file. - MakeFile(pathname string) error - // MakeDir creates a new directory. - MakeDir(pathname string) error - // PathExists tests if the given path already exists - // Error is returned on any other error than "file not found". - PathExists(pathname string) (bool, error) - // EvalHostSymlinks returns the path name after evaluating symlinks. - EvalHostSymlinks(pathname string) (string, error) - // GetOwner returns the integer ID for the user and group of the given path - GetOwner(pathname string) (int64, int64, error) - // GetSELinuxSupport returns true if given path is on a mount that supports - // SELinux. - GetSELinuxSupport(pathname string) (bool, error) - // GetMode returns permissions of the path. - GetMode(pathname string) (os.FileMode, error) -} - // Exec is an interface for executing commands on systems. type Exec interface { // Run executes a command and returns its stdout + stderr combined in one @@ -116,10 +66,6 @@ type Exec interface { // the mount interface. var _ Interface = &Mounter{} -// Compile-time check to ensure all HostUtil implementations satisfy -// the HostUtils Interface. -var _ HostUtils = &hostUtil{} - // MountPoint represents a single line in /proc/mounts or /etc/fstab. type MountPoint struct { Device string @@ -320,37 +266,3 @@ func StartsWithBackstep(rel string) bool { // normalize to / and check for ../ return rel == ".." || strings.HasPrefix(filepath.ToSlash(rel), "../") } - -// getFileType checks for file/directory/socket and block/character devices. -func getFileType(pathname string) (FileType, error) { - var pathType FileType - info, err := os.Stat(pathname) - if os.IsNotExist(err) { - return pathType, fmt.Errorf("path %q does not exist", pathname) - } - // err in call to os.Stat - if err != nil { - return pathType, err - } - - // checks whether the mode is the target mode. - isSpecificMode := func(mode, targetMode os.FileMode) bool { - return mode&targetMode == targetMode - } - - mode := info.Mode() - if mode.IsDir() { - return FileTypeDirectory, nil - } else if mode.IsRegular() { - return FileTypeFile, nil - } else if isSpecificMode(mode, os.ModeSocket) { - return FileTypeSocket, nil - } else if isSpecificMode(mode, os.ModeDevice) { - if isSpecificMode(mode, os.ModeCharDevice) { - return FileTypeCharDev, nil - } - return FileTypeBlockDev, nil - } - - return pathType, fmt.Errorf("only recognise file, directory, socket, block device and character device") -} diff --git a/pkg/util/mount/mount_helper_unix.go b/pkg/util/mount/mount_helper_unix.go index 880a89e1596..440576322b3 100644 --- a/pkg/util/mount/mount_helper_unix.go +++ b/pkg/util/mount/mount_helper_unix.go @@ -19,8 +19,20 @@ limitations under the License. package mount import ( + "fmt" "os" + "strconv" + "strings" "syscall" + + utilio "k8s.io/utils/io" +) + +const ( + // At least number of fields per line in /proc//mountinfo. + expectedAtLeastNumFieldsPerMountInfo = 10 + // How many times to retry for a consistent read of /proc/mounts. + maxListTries = 3 ) // IsCorruptedMnt return true if err is about corrupted mount point @@ -42,3 +54,80 @@ func IsCorruptedMnt(err error) bool { return underlyingError == syscall.ENOTCONN || underlyingError == syscall.ESTALE || underlyingError == syscall.EIO || underlyingError == syscall.EACCES } + +// This represents a single line in /proc//mountinfo. +type mountInfo struct { + // Unique ID for the mount (maybe reused after umount). + id int + // The ID of the parent mount (or of self for the root of this mount namespace's mount tree). + parentID int + // The value of `st_dev` for files on this filesystem. + majorMinor string + // The pathname of the directory in the filesystem which forms the root of this mount. + root string + // Mount source, filesystem-specific information. e.g. device, tmpfs name. + source string + // Mount point, the pathname of the mount point. + mountPoint string + // Optional fieds, zero or more fields of the form "tag[:value]". + optionalFields []string + // The filesystem type in the form "type[.subtype]". + fsType string + // Per-mount options. + mountOptions []string + // Per-superblock options. + superOptions []string +} + +// parseMountInfo parses /proc/xxx/mountinfo. +func parseMountInfo(filename string) ([]mountInfo, error) { + content, err := utilio.ConsistentRead(filename, maxListTries) + if err != nil { + return []mountInfo{}, err + } + contentStr := string(content) + infos := []mountInfo{} + + for _, line := range strings.Split(contentStr, "\n") { + if line == "" { + // the last split() item is empty string following the last \n + continue + } + // See `man proc` for authoritative description of format of the file. + fields := strings.Fields(line) + if len(fields) < expectedAtLeastNumFieldsPerMountInfo { + return nil, fmt.Errorf("wrong number of fields in (expected at least %d, got %d): %s", expectedAtLeastNumFieldsPerMountInfo, len(fields), line) + } + id, err := strconv.Atoi(fields[0]) + if err != nil { + return nil, err + } + parentID, err := strconv.Atoi(fields[1]) + if err != nil { + return nil, err + } + info := mountInfo{ + id: id, + parentID: parentID, + majorMinor: fields[2], + root: fields[3], + mountPoint: fields[4], + mountOptions: strings.Split(fields[5], ","), + } + // All fields until "-" are "optional fields". + i := 6 + for ; i < len(fields) && fields[i] != "-"; i++ { + info.optionalFields = append(info.optionalFields, fields[i]) + } + // Parse the rest 3 fields. + i++ + if len(fields)-i < 3 { + return nil, fmt.Errorf("expect 3 fields in %s, got %d", line, len(fields)-i) + } + info.fsType = fields[i] + info.source = fields[i+1] + info.superOptions = strings.Split(fields[i+2], ",") + infos = append(infos, info) + } + return infos, nil +} diff --git a/pkg/util/mount/mount_helper_unix_test.go b/pkg/util/mount/mount_helper_unix_test.go new file mode 100644 index 00000000000..5c1ebdb4a22 --- /dev/null +++ b/pkg/util/mount/mount_helper_unix_test.go @@ -0,0 +1,215 @@ +// +build !windows + +/* +Copyright 2019 The Kubernetes Authors. + +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 ( + "io/ioutil" + "os" + "path/filepath" + "reflect" + "testing" +) + +func writeFile(content string) (string, string, error) { + tempDir, err := ioutil.TempDir("", "mounter_shared_test") + if err != nil { + return "", "", err + } + filename := filepath.Join(tempDir, "mountinfo") + err = ioutil.WriteFile(filename, []byte(content), 0600) + if err != nil { + os.RemoveAll(tempDir) + return "", "", err + } + return tempDir, filename, nil +} + +func TestParseMountInfo(t *testing.T) { + info := + `62 0 253:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel +80 62 0:42 / /var/lib/nfs/rpc_pipefs rw,relatime shared:31 - rpc_pipefs sunrpc rw +82 62 0:43 / /var/lib/foo rw,relatime shared:32 - tmpfs tmpfs rw +83 63 0:44 / /var/lib/bar rw,relatime - tmpfs tmpfs rw +227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +224 62 253:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered +76 17 8:1 / /mnt/stateful_partition rw,nosuid,nodev,noexec,relatime - ext4 /dev/sda1 rw,commit=30,data=ordered +80 17 8:1 /var /var rw,nosuid,nodev,noexec,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered +189 80 8:1 /var/lib/kubelet /var/lib/kubelet rw,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered +818 77 8:40 / /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdc rw,data=ordered +819 78 8:48 / /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered +900 100 8:48 /dir1 /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volume-subpaths/vol1/subpath1/0 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered +901 101 8:1 /dir1 /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volume-subpaths/vol1/subpath1/1 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered +902 102 8:1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volumes/kubernetes.io~empty-dir/vol1/dir1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volume-subpaths/vol1/subpath1/0 rw,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered +903 103 8:1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volumes/kubernetes.io~empty-dir/vol2/dir1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volume-subpaths/vol1/subpath1/1 rw,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered +178 25 253:0 /etc/bar /var/lib/kubelet/pods/12345/volume-subpaths/vol1/subpath1/0 rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered +698 186 0:41 /tmp1/dir1 /var/lib/kubelet/pods/41135147-e697-11e7-9342-42010a800002/volume-subpaths/vol1/subpath1/0 rw shared:26 - tmpfs tmpfs rw +918 77 8:50 / /var/lib/kubelet/pods/2345/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdc rw,data=ordered +919 78 8:58 / /var/lib/kubelet/pods/2345/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered +920 100 8:50 /dir1 /var/lib/kubelet/pods/2345/volume-subpaths/vol1/subpath1/0 rw,relatime shared:290 - ext4 /dev/sdc rw,data=ordered +150 23 1:58 / /media/nfs_vol rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 +151 24 1:58 / /media/nfs_bindmount rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs/foo rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 +134 23 0:58 / /var/lib/kubelet/pods/43219158-e5e1-11e7-a392-0e858b8eaf40/volumes/kubernetes.io~nfs/nfs1 rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 +187 23 0:58 / /var/lib/kubelet/pods/1fc5ea21-eff4-11e7-ac80-0e858b8eaf40/volumes/kubernetes.io~nfs/nfs2 rw,relatime shared:96 - nfs4 172.18.4.223:/srv/nfs2 rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 +188 24 0:58 / /var/lib/kubelet/pods/43219158-e5e1-11e7-a392-0e858b8eaf40/volume-subpaths/nfs1/subpath1/0 rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs/foo rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 +347 60 0:71 / /var/lib/kubelet/pods/13195d46-f9fa-11e7-bbf1-5254007a695a/volumes/kubernetes.io~nfs/vol2 rw,relatime shared:170 - nfs 172.17.0.3:/exports/2 rw,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=172.17.0.3,mountvers=3,mountport=20048,mountproto=udp,local_lock=none,addr=172.17.0.3 +222 24 253:0 /tmp/src /mnt/dst rw,relatime shared:1 - ext4 /dev/mapper/vagrant--vg-root rw,errors=remount-ro,data=ordered +28 18 0:24 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755 +29 28 0:25 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +31 28 0:27 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,cpuset +32 28 0:28 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,cpu,cpuacct +33 28 0:29 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,freezer +34 28 0:30 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,net_cls,net_prio +35 28 0:31 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,pids +36 28 0:32 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,devices +37 28 0:33 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,hugetlb +38 28 0:34 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,blkio +39 28 0:35 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,memory +40 28 0:36 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,perf_event +` + tempDir, filename, err := writeFile(info) + if err != nil { + t.Fatalf("cannot create temporary file: %v", err) + } + defer os.RemoveAll(tempDir) + + tests := []struct { + name string + id int + expectedInfo mountInfo + }{ + { + "simple bind mount", + 189, + mountInfo{ + id: 189, + parentID: 80, + majorMinor: "8:1", + root: "/var/lib/kubelet", + source: "/dev/sda1", + mountPoint: "/var/lib/kubelet", + optionalFields: []string{"shared:30"}, + fsType: "ext4", + mountOptions: []string{"rw", "relatime"}, + superOptions: []string{"rw", "commit=30", "data=ordered"}, + }, + }, + { + "bind mount a directory", + 222, + mountInfo{ + id: 222, + parentID: 24, + majorMinor: "253:0", + root: "/tmp/src", + source: "/dev/mapper/vagrant--vg-root", + mountPoint: "/mnt/dst", + optionalFields: []string{"shared:1"}, + fsType: "ext4", + mountOptions: []string{"rw", "relatime"}, + superOptions: []string{"rw", "errors=remount-ro", "data=ordered"}, + }, + }, + { + "more than one optional fields", + 224, + mountInfo{ + id: 224, + parentID: 62, + majorMinor: "253:0", + root: "/var/lib/docker/devicemapper/test/shared", + source: "/dev/mapper/ssd-root", + mountPoint: "/var/lib/docker/devicemapper/test/shared", + optionalFields: []string{"master:1", "shared:44"}, + fsType: "ext4", + mountOptions: []string{"rw", "relatime"}, + superOptions: []string{"rw", "seclabel", "data=ordered"}, + }, + }, + { + "cgroup-mountpoint", + 28, + mountInfo{ + id: 28, + parentID: 18, + majorMinor: "0:24", + root: "/", + source: "tmpfs", + mountPoint: "/sys/fs/cgroup", + optionalFields: []string{"shared:9"}, + fsType: "tmpfs", + mountOptions: []string{"ro", "nosuid", "nodev", "noexec"}, + superOptions: []string{"ro", "mode=755"}, + }, + }, + { + "cgroup-subsystem-systemd-mountpoint", + 29, + mountInfo{ + id: 29, + parentID: 28, + majorMinor: "0:25", + root: "/", + source: "cgroup", + mountPoint: "/sys/fs/cgroup/systemd", + optionalFields: []string{"shared:10"}, + fsType: "cgroup", + mountOptions: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}, + superOptions: []string{"rw", "xattr", "release_agent=/lib/systemd/systemd-cgroups-agent", "name=systemd"}, + }, + }, + { + "cgroup-subsystem-cpuset-mountpoint", + 31, + mountInfo{ + id: 31, + parentID: 28, + majorMinor: "0:27", + root: "/", + source: "cgroup", + mountPoint: "/sys/fs/cgroup/cpuset", + optionalFields: []string{"shared:13"}, + fsType: "cgroup", + mountOptions: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}, + superOptions: []string{"rw", "cpuset"}, + }, + }, + } + + infos, err := parseMountInfo(filename) + if err != nil { + t.Fatalf("Cannot parse %s: %s", filename, err) + } + + for _, test := range tests { + found := false + for _, info := range infos { + if info.id == test.id { + found = true + if !reflect.DeepEqual(info, test.expectedInfo) { + t.Errorf("Test case %q:\n expected: %+v\n got: %+v", test.name, test.expectedInfo, info) + } + break + } + } + if !found { + t.Errorf("Test case %q: mountPoint %d not found", test.name, test.id) + } + } +} diff --git a/pkg/util/mount/mount_helper_windows.go b/pkg/util/mount/mount_helper_windows.go index e9b3c657792..69ab3deaeb6 100644 --- a/pkg/util/mount/mount_helper_windows.go +++ b/pkg/util/mount/mount_helper_windows.go @@ -19,7 +19,10 @@ limitations under the License. package mount import ( + "fmt" "os" + "strconv" + "strings" "syscall" "k8s.io/klog" @@ -66,3 +69,25 @@ func IsCorruptedMnt(err error) bool { return false } + +func NormalizeWindowsPath(path string) string { + normalizedPath := strings.Replace(path, "/", "\\", -1) + if strings.HasPrefix(normalizedPath, "\\") { + normalizedPath = "c:" + normalizedPath + } + return normalizedPath +} + +// ValidateDiskNumber : disk number should be a number in [0, 99] +func ValidateDiskNumber(disk string) error { + diskNum, err := strconv.Atoi(disk) + if err != nil { + return fmt.Errorf("wrong disk number format: %q, err:%v", disk, err) + } + + if diskNum < 0 || diskNum > 99 { + return fmt.Errorf("disk number out of range: %q", disk) + } + + return nil +} diff --git a/pkg/util/mount/mount_helper_windows_test.go b/pkg/util/mount/mount_helper_windows_test.go new file mode 100644 index 00000000000..7f5e6eb1827 --- /dev/null +++ b/pkg/util/mount/mount_helper_windows_test.go @@ -0,0 +1,65 @@ +// +build windows + +/* +Copyright 2017 The Kubernetes Authors. + +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 ( + "testing" +) + +func TestNormalizeWindowsPath(t *testing.T) { + path := `/var/lib/kubelet/pods/146f8428-83e7-11e7-8dd4-000d3a31dac4/volumes/kubernetes.io~azure-disk` + normalizedPath := NormalizeWindowsPath(path) + if normalizedPath != `c:\var\lib\kubelet\pods\146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~azure-disk` { + t.Errorf("normizeWindowsPath test failed, normalizedPath : %q", normalizedPath) + } + + path = `/var/lib/kubelet/pods/146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~azure-disk` + normalizedPath = NormalizeWindowsPath(path) + if normalizedPath != `c:\var\lib\kubelet\pods\146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~azure-disk` { + t.Errorf("normizeWindowsPath test failed, normalizedPath : %q", normalizedPath) + } + + path = `/` + normalizedPath = NormalizeWindowsPath(path) + if normalizedPath != `c:\` { + t.Errorf("normizeWindowsPath test failed, normalizedPath : %q", normalizedPath) + } +} + +func TestValidateDiskNumber(t *testing.T) { + diskNum := "0" + if err := ValidateDiskNumber(diskNum); err != nil { + t.Errorf("TestValidateDiskNumber test failed, disk number : %s", diskNum) + } + + diskNum = "99" + if err := ValidateDiskNumber(diskNum); err != nil { + t.Errorf("TestValidateDiskNumber test failed, disk number : %s", diskNum) + } + + diskNum = "ab" + if err := ValidateDiskNumber(diskNum); err == nil { + t.Errorf("TestValidateDiskNumber test failed, disk number : %s", diskNum) + } + + diskNum = "100" + if err := ValidateDiskNumber(diskNum); err == nil { + t.Errorf("TestValidateDiskNumber test failed, disk number : %s", diskNum) + } +} diff --git a/pkg/util/mount/mount_linux.go b/pkg/util/mount/mount_linux.go index 1d5e71961ed..792ac43d8f2 100644 --- a/pkg/util/mount/mount_linux.go +++ b/pkg/util/mount/mount_linux.go @@ -23,26 +23,19 @@ import ( "fmt" "os" "os/exec" - "path" "path/filepath" "strconv" "strings" "syscall" - "golang.org/x/sys/unix" "k8s.io/klog" utilexec "k8s.io/utils/exec" utilio "k8s.io/utils/io" - utilpath "k8s.io/utils/path" ) 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. expectedNumFieldsPerLine = 6 - // At least number of fields per line in /proc//mountinfo. - expectedAtLeastNumFieldsPerMountInfo = 10 // Location of the mount file to use procMountsPath = "/proc/mounts" // Location of the mountinfo file @@ -452,345 +445,6 @@ func parseProcMounts(content []byte) ([]MountPoint, error) { return out, nil } -type hostUtil struct { -} - -// NewHostUtil returns a struct that implements the HostUtils interface on -// linux platforms -func NewHostUtil() HostUtils { - return &hostUtil{} -} - -// DeviceOpened checks if block device in use by calling Open with O_EXCL flag. -// If pathname is not a device, log and return false with nil error. -// If open returns errno EBUSY, return true with nil error. -// If open returns nil, return false with nil error. -// Otherwise, return false with error -func (hu *hostUtil) DeviceOpened(pathname string) (bool, error) { - return ExclusiveOpenFailsOnDevice(pathname) -} - -// PathIsDevice uses FileInfo returned from os.Stat to check if path refers -// to a device. -func (hu *hostUtil) PathIsDevice(pathname string) (bool, error) { - pathType, err := hu.GetFileType(pathname) - isDevice := pathType == FileTypeCharDev || pathType == FileTypeBlockDev - return isDevice, err -} - -// ExclusiveOpenFailsOnDevice is shared with NsEnterMounter -func ExclusiveOpenFailsOnDevice(pathname string) (bool, error) { - var isDevice bool - finfo, err := os.Stat(pathname) - if os.IsNotExist(err) { - isDevice = false - } - // err in call to os.Stat - if err != nil { - return false, fmt.Errorf( - "PathIsDevice failed for path %q: %v", - pathname, - err) - } - // path refers to a device - if finfo.Mode()&os.ModeDevice != 0 { - isDevice = true - } - - if !isDevice { - klog.Errorf("Path %q is not referring to a device.", pathname) - return false, nil - } - fd, errno := unix.Open(pathname, unix.O_RDONLY|unix.O_EXCL|unix.O_CLOEXEC, 0) - // If the device is in use, open will return an invalid fd. - // When this happens, it is expected that Close will fail and throw an error. - defer unix.Close(fd) - if errno == nil { - // device not in use - return false, nil - } else if errno == unix.EBUSY { - // device is in use - return true, nil - } - // error during call to Open - return false, errno -} - -//GetDeviceNameFromMount: given a mount point, find the device name from its global mount point -func (hu *hostUtil) GetDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { - return GetDeviceNameFromMountLinux(mounter, mountPath, pluginMountDir) -} - -func getDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { - return GetDeviceNameFromMountLinux(mounter, mountPath, pluginMountDir) -} - -// GetDeviceNameFromMountLinux find the device name from /proc/mounts in which -// the mount path reference should match the given plugin mount directory. In case no mount path reference -// matches, returns the volume name taken from its given mountPath -// This implementation is shared with NsEnterMounter -func GetDeviceNameFromMountLinux(mounter Interface, mountPath, pluginMountDir string) (string, error) { - refs, err := mounter.GetMountRefs(mountPath) - if err != nil { - klog.V(4).Infof("GetMountRefs failed for mount path %q: %v", mountPath, err) - return "", err - } - if len(refs) == 0 { - klog.V(4).Infof("Directory %s is not mounted", mountPath) - return "", fmt.Errorf("directory %s is not mounted", mountPath) - } - for _, ref := range refs { - if strings.HasPrefix(ref, pluginMountDir) { - volumeID, err := filepath.Rel(pluginMountDir, ref) - if err != nil { - klog.Errorf("Failed to get volume id from mount %s - %v", mountPath, err) - return "", err - } - return volumeID, nil - } - } - - return path.Base(mountPath), nil -} - -func (hu *hostUtil) MakeRShared(path string) error { - return DoMakeRShared(path, procMountInfoPath) -} - -func (hu *hostUtil) GetFileType(pathname string) (FileType, error) { - return getFileType(pathname) -} - -func (hu *hostUtil) MakeDir(pathname string) error { - err := os.MkdirAll(pathname, os.FileMode(0755)) - if err != nil { - if !os.IsExist(err) { - return err - } - } - return nil -} - -func (hu *hostUtil) MakeFile(pathname string) error { - f, err := os.OpenFile(pathname, os.O_CREATE, os.FileMode(0644)) - defer f.Close() - if err != nil { - if !os.IsExist(err) { - return err - } - } - return nil -} - -func (hu *hostUtil) PathExists(pathname string) (bool, error) { - return utilpath.Exists(utilpath.CheckFollowSymlink, pathname) -} - -func (hu *hostUtil) EvalHostSymlinks(pathname string) (string, error) { - return filepath.EvalSymlinks(pathname) -} - -// isShared returns true, if given path is on a mount point that has shared -// mount propagation. -func isShared(mount string, mountInfoPath string) (bool, error) { - info, err := findMountInfo(mount, mountInfoPath) - if err != nil { - return false, err - } - - // parse optional parameters - for _, opt := range info.optionalFields { - if strings.HasPrefix(opt, "shared:") { - return true, nil - } - } - return false, nil -} - -// This represents a single line in /proc//mountinfo. -type mountInfo struct { - // Unique ID for the mount (maybe reused after umount). - id int - // The ID of the parent mount (or of self for the root of this mount namespace's mount tree). - parentID int - // The value of `st_dev` for files on this filesystem. - majorMinor string - // The pathname of the directory in the filesystem which forms the root of this mount. - root string - // Mount source, filesystem-specific information. e.g. device, tmpfs name. - source string - // Mount point, the pathname of the mount point. - mountPoint string - // Optional fieds, zero or more fields of the form "tag[:value]". - optionalFields []string - // The filesystem type in the form "type[.subtype]". - fsType string - // Per-mount options. - mountOptions []string - // Per-superblock options. - superOptions []string -} - -// parseMountInfo parses /proc/xxx/mountinfo. -func parseMountInfo(filename string) ([]mountInfo, error) { - content, err := utilio.ConsistentRead(filename, maxListTries) - if err != nil { - return []mountInfo{}, err - } - contentStr := string(content) - infos := []mountInfo{} - - for _, line := range strings.Split(contentStr, "\n") { - if line == "" { - // the last split() item is empty string following the last \n - continue - } - // See `man proc` for authoritative description of format of the file. - fields := strings.Fields(line) - if len(fields) < expectedAtLeastNumFieldsPerMountInfo { - return nil, fmt.Errorf("wrong number of fields in (expected at least %d, got %d): %s", expectedAtLeastNumFieldsPerMountInfo, len(fields), line) - } - id, err := strconv.Atoi(fields[0]) - if err != nil { - return nil, err - } - parentID, err := strconv.Atoi(fields[1]) - if err != nil { - return nil, err - } - info := mountInfo{ - id: id, - parentID: parentID, - majorMinor: fields[2], - root: fields[3], - mountPoint: fields[4], - mountOptions: strings.Split(fields[5], ","), - } - // All fields until "-" are "optional fields". - i := 6 - for ; i < len(fields) && fields[i] != "-"; i++ { - info.optionalFields = append(info.optionalFields, fields[i]) - } - // Parse the rest 3 fields. - i++ - if len(fields)-i < 3 { - return nil, fmt.Errorf("expect 3 fields in %s, got %d", line, len(fields)-i) - } - info.fsType = fields[i] - info.source = fields[i+1] - info.superOptions = strings.Split(fields[i+2], ",") - infos = append(infos, info) - } - return infos, nil -} - -func findMountInfo(path, mountInfoPath string) (mountInfo, error) { - infos, err := parseMountInfo(mountInfoPath) - if err != nil { - return mountInfo{}, err - } - - // process /proc/xxx/mountinfo in backward order and find the first mount - // point that is prefix of 'path' - that's the mount where path resides - var info *mountInfo - for i := len(infos) - 1; i >= 0; i-- { - if PathWithinBase(path, infos[i].mountPoint) { - info = &infos[i] - break - } - } - if info == nil { - return mountInfo{}, fmt.Errorf("cannot find mount point for %q", path) - } - return *info, nil -} - -// DoMakeRShared is common implementation of MakeRShared on Linux. It checks if -// path is shared and bind-mounts it as rshared if needed. mountCmd and -// mountArgs are expected to contain mount-like command, DoMakeRShared will add -// '--bind ' and '--make-rshared ' to mountArgs. -func DoMakeRShared(path string, mountInfoFilename string) error { - shared, err := isShared(path, mountInfoFilename) - if err != nil { - return err - } - if shared { - klog.V(4).Infof("Directory %s is already on a shared mount", path) - return nil - } - - klog.V(2).Infof("Bind-mounting %q with shared mount propagation", path) - // mount --bind /var/lib/kubelet /var/lib/kubelet - if err := syscall.Mount(path, path, "" /*fstype*/, syscall.MS_BIND, "" /*data*/); err != nil { - return fmt.Errorf("failed to bind-mount %s: %v", path, err) - } - - // mount --make-rshared /var/lib/kubelet - if err := syscall.Mount(path, path, "" /*fstype*/, syscall.MS_SHARED|syscall.MS_REC, "" /*data*/); err != nil { - return fmt.Errorf("failed to make %s rshared: %v", path, err) - } - - return nil -} - -// GetSELinux is common implementation of GetSELinuxSupport on Linux. -func GetSELinux(path string, mountInfoFilename string) (bool, error) { - info, err := findMountInfo(path, mountInfoFilename) - if err != nil { - return false, err - } - - // "seclabel" can be both in mount options and super options. - for _, opt := range info.superOptions { - if opt == "seclabel" { - return true, nil - } - } - for _, opt := range info.mountOptions { - if opt == "seclabel" { - return true, nil - } - } - return false, nil -} - -func (hu *hostUtil) GetSELinuxSupport(pathname string) (bool, error) { - return GetSELinux(pathname, procMountInfoPath) -} - -// GetOwner returns the integer ID for the user and group of the given path -func (hu *hostUtil) GetOwner(pathname string) (int64, int64, error) { - realpath, err := filepath.EvalSymlinks(pathname) - if err != nil { - return -1, -1, err - } - return GetOwnerLinux(realpath) -} - -func (hu *hostUtil) GetMode(pathname string) (os.FileMode, error) { - return GetModeLinux(pathname) -} - -// GetOwnerLinux is shared between Linux and NsEnterMounter -// pathname must already be evaluated for symlinks -func GetOwnerLinux(pathname string) (int64, int64, error) { - info, err := os.Stat(pathname) - if err != nil { - return -1, -1, err - } - stat := info.Sys().(*syscall.Stat_t) - return int64(stat.Uid), int64(stat.Gid), nil -} - -// GetModeLinux is shared between Linux and NsEnterMounter -func GetModeLinux(pathname string) (os.FileMode, error) { - info, err := os.Stat(pathname) - if err != nil { - return 0, err - } - return info.Mode(), nil -} - // SearchMountPoints finds all mount references to the source, returns a list of // mountpoints. // This function assumes source cannot be device. diff --git a/pkg/util/mount/mount_linux_test.go b/pkg/util/mount/mount_linux_test.go index e8ff8251d8f..da4b989bc9b 100644 --- a/pkg/util/mount/mount_linux_test.go +++ b/pkg/util/mount/mount_linux_test.go @@ -19,16 +19,10 @@ limitations under the License. package mount import ( - "fmt" "io/ioutil" - "net" "os" - "path/filepath" "reflect" - "strings" "testing" - - "k8s.io/utils/exec" ) func TestReadProcMountsFrom(t *testing.T) { @@ -209,129 +203,6 @@ func TestGetMountRefsByDev(t *testing.T) { } } -func writeFile(content string) (string, string, error) { - tempDir, err := ioutil.TempDir("", "mounter_shared_test") - if err != nil { - return "", "", err - } - filename := filepath.Join(tempDir, "mountinfo") - err = ioutil.WriteFile(filename, []byte(content), 0600) - if err != nil { - os.RemoveAll(tempDir) - return "", "", err - } - return tempDir, filename, nil -} - -func TestIsSharedSuccess(t *testing.T) { - successMountInfo := - `62 0 253:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -76 62 8:1 / /boot rw,relatime shared:29 - ext4 /dev/sda1 rw,seclabel,data=ordered -78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel -80 62 0:42 / /var/lib/nfs/rpc_pipefs rw,relatime shared:31 - rpc_pipefs sunrpc rw -82 62 0:43 / /var/lib/foo rw,relatime shared:32 - tmpfs tmpfs rw -83 63 0:44 / /var/lib/bar rw,relatime - tmpfs tmpfs rw -227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -224 62 253:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -` - tempDir, filename, err := writeFile(successMountInfo) - if err != nil { - t.Fatalf("cannot create temporary file: %v", err) - } - defer os.RemoveAll(tempDir) - - tests := []struct { - name string - path string - expectedResult bool - }{ - { - // /var/lib/kubelet is a directory on mount '/' that is shared - // This is the most common case. - "shared", - "/var/lib/kubelet", - true, - }, - { - // 8a2a... is a directory on mount /var/lib/docker/devicemapper - // that is private. - "private", - "/var/lib/docker/devicemapper/mnt/8a2a5c19eefb06d6f851dfcb240f8c113427f5b49b19658b5c60168e88267693/", - false, - }, - { - // 'directory' is a directory on mount - // /var/lib/docker/devicemapper/test/shared that is shared, but one - // of its parent is private. - "nested-shared", - "/var/lib/docker/devicemapper/test/shared/my/test/directory", - true, - }, - { - // /var/lib/foo is a mount point and it's shared - "shared-mount", - "/var/lib/foo", - true, - }, - { - // /var/lib/bar is a mount point and it's private - "private-mount", - "/var/lib/bar", - false, - }, - } - for _, test := range tests { - ret, err := isShared(test.path, filename) - if err != nil { - t.Errorf("test %s got unexpected error: %v", test.name, err) - } - if ret != test.expectedResult { - t.Errorf("test %s expected %v, got %v", test.name, test.expectedResult, ret) - } - } -} - -func TestIsSharedFailure(t *testing.T) { - errorTests := []struct { - name string - content string - }{ - { - // the first line is too short - name: "too-short-line", - content: `62 0 253:0 / / rw,relatime -76 62 8:1 / /boot rw,relatime shared:29 - ext4 /dev/sda1 rw,seclabel,data=ordered -78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel -80 62 0:42 / /var/lib/nfs/rpc_pipefs rw,relatime shared:31 - rpc_pipefs sunrpc rw -227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -224 62 253:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -`, - }, - { - // there is no root mount - name: "no-root-mount", - content: `76 62 8:1 / /boot rw,relatime shared:29 - ext4 /dev/sda1 rw,seclabel,data=ordered -78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel -80 62 0:42 / /var/lib/nfs/rpc_pipefs rw,relatime shared:31 - rpc_pipefs sunrpc rw -227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -224 62 253:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -`, - }, - } - for _, test := range errorTests { - tempDir, filename, err := writeFile(test.content) - if err != nil { - t.Fatalf("cannot create temporary file: %v", err) - } - defer os.RemoveAll(tempDir) - - _, err = isShared("/", filename) - if err == nil { - t.Errorf("test %q: expected error, got none", test.name) - } - } -} - func TestPathWithinBase(t *testing.T) { tests := []struct { name string @@ -420,353 +291,6 @@ func TestPathWithinBase(t *testing.T) { } } -func TestParseMountInfo(t *testing.T) { - info := - `62 0 253:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel -80 62 0:42 / /var/lib/nfs/rpc_pipefs rw,relatime shared:31 - rpc_pipefs sunrpc rw -82 62 0:43 / /var/lib/foo rw,relatime shared:32 - tmpfs tmpfs rw -83 63 0:44 / /var/lib/bar rw,relatime - tmpfs tmpfs rw -227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -224 62 253:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -76 17 8:1 / /mnt/stateful_partition rw,nosuid,nodev,noexec,relatime - ext4 /dev/sda1 rw,commit=30,data=ordered -80 17 8:1 /var /var rw,nosuid,nodev,noexec,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered -189 80 8:1 /var/lib/kubelet /var/lib/kubelet rw,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered -818 77 8:40 / /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdc rw,data=ordered -819 78 8:48 / /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered -900 100 8:48 /dir1 /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volume-subpaths/vol1/subpath1/0 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered -901 101 8:1 /dir1 /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volume-subpaths/vol1/subpath1/1 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered -902 102 8:1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volumes/kubernetes.io~empty-dir/vol1/dir1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volume-subpaths/vol1/subpath1/0 rw,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered -903 103 8:1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volumes/kubernetes.io~empty-dir/vol2/dir1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volume-subpaths/vol1/subpath1/1 rw,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered -178 25 253:0 /etc/bar /var/lib/kubelet/pods/12345/volume-subpaths/vol1/subpath1/0 rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered -698 186 0:41 /tmp1/dir1 /var/lib/kubelet/pods/41135147-e697-11e7-9342-42010a800002/volume-subpaths/vol1/subpath1/0 rw shared:26 - tmpfs tmpfs rw -918 77 8:50 / /var/lib/kubelet/pods/2345/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdc rw,data=ordered -919 78 8:58 / /var/lib/kubelet/pods/2345/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered -920 100 8:50 /dir1 /var/lib/kubelet/pods/2345/volume-subpaths/vol1/subpath1/0 rw,relatime shared:290 - ext4 /dev/sdc rw,data=ordered -150 23 1:58 / /media/nfs_vol rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 -151 24 1:58 / /media/nfs_bindmount rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs/foo rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 -134 23 0:58 / /var/lib/kubelet/pods/43219158-e5e1-11e7-a392-0e858b8eaf40/volumes/kubernetes.io~nfs/nfs1 rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 -187 23 0:58 / /var/lib/kubelet/pods/1fc5ea21-eff4-11e7-ac80-0e858b8eaf40/volumes/kubernetes.io~nfs/nfs2 rw,relatime shared:96 - nfs4 172.18.4.223:/srv/nfs2 rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 -188 24 0:58 / /var/lib/kubelet/pods/43219158-e5e1-11e7-a392-0e858b8eaf40/volume-subpaths/nfs1/subpath1/0 rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs/foo rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 -347 60 0:71 / /var/lib/kubelet/pods/13195d46-f9fa-11e7-bbf1-5254007a695a/volumes/kubernetes.io~nfs/vol2 rw,relatime shared:170 - nfs 172.17.0.3:/exports/2 rw,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=172.17.0.3,mountvers=3,mountport=20048,mountproto=udp,local_lock=none,addr=172.17.0.3 -222 24 253:0 /tmp/src /mnt/dst rw,relatime shared:1 - ext4 /dev/mapper/vagrant--vg-root rw,errors=remount-ro,data=ordered -28 18 0:24 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755 -29 28 0:25 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd -31 28 0:27 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,cpuset -32 28 0:28 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,cpu,cpuacct -33 28 0:29 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,freezer -34 28 0:30 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,net_cls,net_prio -35 28 0:31 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,pids -36 28 0:32 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,devices -37 28 0:33 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,hugetlb -38 28 0:34 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,blkio -39 28 0:35 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,memory -40 28 0:36 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,perf_event -` - tempDir, filename, err := writeFile(info) - if err != nil { - t.Fatalf("cannot create temporary file: %v", err) - } - defer os.RemoveAll(tempDir) - - tests := []struct { - name string - id int - expectedInfo mountInfo - }{ - { - "simple bind mount", - 189, - mountInfo{ - id: 189, - parentID: 80, - majorMinor: "8:1", - root: "/var/lib/kubelet", - source: "/dev/sda1", - mountPoint: "/var/lib/kubelet", - optionalFields: []string{"shared:30"}, - fsType: "ext4", - mountOptions: []string{"rw", "relatime"}, - superOptions: []string{"rw", "commit=30", "data=ordered"}, - }, - }, - { - "bind mount a directory", - 222, - mountInfo{ - id: 222, - parentID: 24, - majorMinor: "253:0", - root: "/tmp/src", - source: "/dev/mapper/vagrant--vg-root", - mountPoint: "/mnt/dst", - optionalFields: []string{"shared:1"}, - fsType: "ext4", - mountOptions: []string{"rw", "relatime"}, - superOptions: []string{"rw", "errors=remount-ro", "data=ordered"}, - }, - }, - { - "more than one optional fields", - 224, - mountInfo{ - id: 224, - parentID: 62, - majorMinor: "253:0", - root: "/var/lib/docker/devicemapper/test/shared", - source: "/dev/mapper/ssd-root", - mountPoint: "/var/lib/docker/devicemapper/test/shared", - optionalFields: []string{"master:1", "shared:44"}, - fsType: "ext4", - mountOptions: []string{"rw", "relatime"}, - superOptions: []string{"rw", "seclabel", "data=ordered"}, - }, - }, - { - "cgroup-mountpoint", - 28, - mountInfo{ - id: 28, - parentID: 18, - majorMinor: "0:24", - root: "/", - source: "tmpfs", - mountPoint: "/sys/fs/cgroup", - optionalFields: []string{"shared:9"}, - fsType: "tmpfs", - mountOptions: []string{"ro", "nosuid", "nodev", "noexec"}, - superOptions: []string{"ro", "mode=755"}, - }, - }, - { - "cgroup-subsystem-systemd-mountpoint", - 29, - mountInfo{ - id: 29, - parentID: 28, - majorMinor: "0:25", - root: "/", - source: "cgroup", - mountPoint: "/sys/fs/cgroup/systemd", - optionalFields: []string{"shared:10"}, - fsType: "cgroup", - mountOptions: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}, - superOptions: []string{"rw", "xattr", "release_agent=/lib/systemd/systemd-cgroups-agent", "name=systemd"}, - }, - }, - { - "cgroup-subsystem-cpuset-mountpoint", - 31, - mountInfo{ - id: 31, - parentID: 28, - majorMinor: "0:27", - root: "/", - source: "cgroup", - mountPoint: "/sys/fs/cgroup/cpuset", - optionalFields: []string{"shared:13"}, - fsType: "cgroup", - mountOptions: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}, - superOptions: []string{"rw", "cpuset"}, - }, - }, - } - - infos, err := parseMountInfo(filename) - if err != nil { - t.Fatalf("Cannot parse %s: %s", filename, err) - } - - for _, test := range tests { - found := false - for _, info := range infos { - if info.id == test.id { - found = true - if !reflect.DeepEqual(info, test.expectedInfo) { - t.Errorf("Test case %q:\n expected: %+v\n got: %+v", test.name, test.expectedInfo, info) - } - break - } - } - if !found { - t.Errorf("Test case %q: mountPoint %d not found", test.name, test.id) - } - } -} - -func TestGetSELinuxSupport(t *testing.T) { - info := - `62 0 253:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel -83 63 0:44 / /var/lib/bar rw,relatime - tmpfs tmpfs rw -227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered -150 23 1:58 / /media/nfs_vol rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 -` - tempDir, filename, err := writeFile(info) - if err != nil { - t.Fatalf("cannot create temporary file: %v", err) - } - defer os.RemoveAll(tempDir) - - tests := []struct { - name string - mountPoint string - expectedResult bool - }{ - { - "ext4 on /", - "/", - true, - }, - { - "tmpfs on /var/lib/bar", - "/var/lib/bar", - false, - }, - { - "nfsv4", - "/media/nfs_vol", - false, - }, - } - - for _, test := range tests { - out, err := GetSELinux(test.mountPoint, filename) - if err != nil { - t.Errorf("Test %s failed with error: %s", test.name, err) - } - if test.expectedResult != out { - t.Errorf("Test %s failed: expected %v, got %v", test.name, test.expectedResult, out) - } - } -} - -func createSocketFile(socketDir string) (string, error) { - testSocketFile := filepath.Join(socketDir, "mt.sock") - - // Switch to volume path and create the socket file - // socket file can not have length of more than 108 character - // and hence we must use relative path - oldDir, _ := os.Getwd() - - err := os.Chdir(socketDir) - if err != nil { - return "", err - } - defer func() { - os.Chdir(oldDir) - }() - _, socketCreateError := net.Listen("unix", "mt.sock") - return testSocketFile, socketCreateError -} - -func TestGetFileType(t *testing.T) { - hu := NewHostUtil() - - testCase := []struct { - name string - expectedType FileType - setUp func() (string, string, error) - }{ - { - "Directory Test", - FileTypeDirectory, - func() (string, string, error) { - tempDir, err := ioutil.TempDir("", "test-get-filetype-") - return tempDir, tempDir, err - }, - }, - { - "File Test", - FileTypeFile, - func() (string, string, error) { - tempFile, err := ioutil.TempFile("", "test-get-filetype") - if err != nil { - return "", "", err - } - tempFile.Close() - return tempFile.Name(), tempFile.Name(), nil - }, - }, - { - "Socket Test", - FileTypeSocket, - func() (string, string, error) { - tempDir, err := ioutil.TempDir("", "test-get-filetype-") - if err != nil { - return "", "", err - } - tempSocketFile, err := createSocketFile(tempDir) - return tempSocketFile, tempDir, err - }, - }, - { - "Block Device Test", - FileTypeBlockDev, - func() (string, string, error) { - tempDir, err := ioutil.TempDir("", "test-get-filetype-") - if err != nil { - return "", "", err - } - - tempBlockFile := filepath.Join(tempDir, "test_blk_dev") - outputBytes, err := exec.New().Command("mknod", tempBlockFile, "b", "89", "1").CombinedOutput() - if err != nil { - err = fmt.Errorf("%v: %s ", err, outputBytes) - } - return tempBlockFile, tempDir, err - }, - }, - { - "Character Device Test", - FileTypeCharDev, - func() (string, string, error) { - tempDir, err := ioutil.TempDir("", "test-get-filetype-") - if err != nil { - return "", "", err - } - - tempCharFile := filepath.Join(tempDir, "test_char_dev") - outputBytes, err := exec.New().Command("mknod", tempCharFile, "c", "89", "1").CombinedOutput() - if err != nil { - err = fmt.Errorf("%v: %s ", err, outputBytes) - } - return tempCharFile, tempDir, err - }, - }, - } - - for idx, tc := range testCase { - path, cleanUpPath, err := tc.setUp() - if err != nil { - // Locally passed, but upstream CI is not friendly to create such device files - // Leave "Operation not permitted" out, which can be covered in an e2e test - if isOperationNotPermittedError(err) { - continue - } - t.Fatalf("[%d-%s] unexpected error : %v", idx, tc.name, err) - } - if len(cleanUpPath) > 0 { - defer os.RemoveAll(cleanUpPath) - } - - fileType, err := hu.GetFileType(path) - if err != nil { - t.Fatalf("[%d-%s] unexpected error : %v", idx, tc.name, err) - } - if fileType != tc.expectedType { - t.Fatalf("[%d-%s] expected %s, but got %s", idx, tc.name, tc.expectedType, fileType) - } - } -} - -func isOperationNotPermittedError(err error) bool { - if strings.Contains(err.Error(), "Operation not permitted") { - return true - } - return false -} - func TestSearchMountPoints(t *testing.T) { base := ` 19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw diff --git a/pkg/util/mount/mount_unsupported.go b/pkg/util/mount/mount_unsupported.go index 7cdf75ba5ee..1e2aed63aa2 100644 --- a/pkg/util/mount/mount_unsupported.go +++ b/pkg/util/mount/mount_unsupported.go @@ -20,7 +20,6 @@ package mount import ( "errors" - "os" ) // Mounter implements mount.Interface for unsupported platforms @@ -80,65 +79,3 @@ func (mounter *SafeFormatAndMount) diskLooksUnformatted(disk string) (bool, erro func getDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { return "", errUnsupported } - -type hostUtil struct{} - -// NewHostUtil returns a struct that implements the HostUtils interface on -// unsupported platforms -func NewHostUtil() HostUtils { - return &hostUtil{} -} - -// DeviceOpened determines if the device is in use elsewhere -func (hu *hostUtil) DeviceOpened(pathname string) (bool, error) { - return false, errUnsupported -} - -// PathIsDevice determines if a path is a device. -func (hu *hostUtil) PathIsDevice(pathname string) (bool, error) { - return true, errUnsupported -} - -// GetDeviceNameFromMount finds the device name by checking the mount path -// to get the global mount path within its plugin directory -func (hu *hostUtil) GetDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { - return "", errUnsupported -} - -func (hu *hostUtil) MakeRShared(path string) error { - return errUnsupported -} - -func (hu *hostUtil) GetFileType(pathname string) (FileType, error) { - return FileType("fake"), errUnsupported -} - -func (hu *hostUtil) MakeFile(pathname string) error { - return errUnsupported -} - -func (hu *hostUtil) MakeDir(pathname string) error { - return errUnsupported -} - -func (hu *hostUtil) PathExists(pathname string) (bool, error) { - return true, errUnsupported -} - -// EvalHostSymlinks returns the path name after evaluating symlinks -func (hu *hostUtil) EvalHostSymlinks(pathname string) (string, error) { - return "", errUnsupported -} - -// GetOwner returns the integer ID for the user and group of the given path -func (hu *hostUtil) GetOwner(pathname string) (int64, int64, error) { - return -1, -1, errUnsupported -} - -func (hu *hostUtil) GetSELinuxSupport(pathname string) (bool, error) { - return false, errUnsupported -} - -func (hu *hostUtil) GetMode(pathname string) (os.FileMode, error) { - return 0, errUnsupported -} diff --git a/pkg/util/mount/mount_windows.go b/pkg/util/mount/mount_windows.go index 0dde2e8b23e..4d6600280f0 100644 --- a/pkg/util/mount/mount_windows.go +++ b/pkg/util/mount/mount_windows.go @@ -22,15 +22,11 @@ import ( "fmt" "os" "os/exec" - "path" "path/filepath" - "strconv" "strings" "k8s.io/klog" "k8s.io/utils/keymutex" - - utilpath "k8s.io/utils/path" ) // Mounter provides the default implementation of mount.Interface @@ -55,7 +51,7 @@ var getSMBMountMutex = keymutex.NewHashed(0) // Mount : mounts source to target with given options. // currently only supports cifs(smb), bind mount(for disk) func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { - target = normalizeWindowsPath(target) + target = NormalizeWindowsPath(target) if source == "tmpfs" { klog.V(3).Infof("mounting source (%q), target (%q), with options (%q)", source, target, options) @@ -74,7 +70,7 @@ func (mounter *Mounter) Mount(source string, target string, fstype string, optio // tell it's going to mount azure disk or azure file according to options if bind, _, _ := IsBind(options); bind { // mount azure disk - bindSource = normalizeWindowsPath(source) + bindSource = NormalizeWindowsPath(source) } else { if len(options) < 2 { klog.Warningf("mount options(%q) command number(%d) less than 2, source:%q, target:%q, skip mounting", @@ -155,7 +151,7 @@ func removeSMBMapping(remotepath string) (string, error) { // Unmount unmounts the target. func (mounter *Mounter) Unmount(target string) error { klog.V(4).Infof("azureMount: Unmount target (%q)", target) - target = normalizeWindowsPath(target) + target = NormalizeWindowsPath(target) if output, err := exec.Command("cmd", "/c", "rmdir", target).CombinedOutput(); err != nil { klog.Errorf("rmdir failed: %v, output: %q", err, string(output)) return err @@ -198,7 +194,7 @@ func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { // GetMountRefs : empty implementation here since there is no place to query all mount points on Windows func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { - windowsPath := normalizeWindowsPath(pathname) + windowsPath := NormalizeWindowsPath(pathname) pathExists, pathErr := PathExists(windowsPath) if !pathExists { return []string{}, nil @@ -238,7 +234,7 @@ func (mounter *SafeFormatAndMount) formatAndMount(source string, target string, return err } driverPath := driveLetter + ":" - target = normalizeWindowsPath(target) + target = NormalizeWindowsPath(target) klog.V(4).Infof("Attempting to formatAndMount disk: %s %s %s", fstype, driverPath, target) if output, err := mounter.Exec.Run("cmd", "/c", "mklink", "/D", target, driverPath); err != nil { klog.Errorf("mklink failed: %v, output: %q", err, string(output)) @@ -247,28 +243,6 @@ func (mounter *SafeFormatAndMount) formatAndMount(source string, target string, return nil } -func normalizeWindowsPath(path string) string { - normalizedPath := strings.Replace(path, "/", "\\", -1) - if strings.HasPrefix(normalizedPath, "\\") { - normalizedPath = "c:" + normalizedPath - } - return normalizedPath -} - -// ValidateDiskNumber : disk number should be a number in [0, 99] -func ValidateDiskNumber(disk string) error { - diskNum, err := strconv.Atoi(disk) - if err != nil { - return fmt.Errorf("wrong disk number format: %q, err:%v", disk, err) - } - - if diskNum < 0 || diskNum > 99 { - return fmt.Errorf("disk number out of range: %q", disk) - } - - return nil -} - // Get drive letter according to windows disk number func getDriveLetterByDiskNumber(diskNum string, exec Exec) (string, error) { cmd := fmt.Sprintf("(Get-Partition -DiskNumber %s).DriveLetter", diskNum) @@ -308,117 +282,3 @@ func getAllParentLinks(path string) ([]string, error) { return links, nil } - -type hostUtil struct{} - -// NewHostUtil returns a struct that implements the HostUtils interface on -// windows platforms -func NewHostUtil() HostUtils { - return &hostUtil{} -} - -// GetDeviceNameFromMount given a mnt point, find the device -func (hu *hostUtil) GetDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { - return getDeviceNameFromMount(mounter, mountPath, pluginMountDir) -} - -// getDeviceNameFromMount find the device(drive) name in which -// the mount path reference should match the given plugin mount directory. In case no mount path reference -// matches, returns the volume name taken from its given mountPath -func getDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { - refs, err := mounter.GetMountRefs(mountPath) - if err != nil { - klog.V(4).Infof("GetMountRefs failed for mount path %q: %v", mountPath, err) - return "", err - } - if len(refs) == 0 { - return "", fmt.Errorf("directory %s is not mounted", mountPath) - } - basemountPath := normalizeWindowsPath(pluginMountDir) - for _, ref := range refs { - if strings.Contains(ref, basemountPath) { - volumeID, err := filepath.Rel(normalizeWindowsPath(basemountPath), ref) - if err != nil { - klog.Errorf("Failed to get volume id from mount %s - %v", mountPath, err) - return "", err - } - return volumeID, nil - } - } - - return path.Base(mountPath), nil -} - -// DeviceOpened determines if the device is in use elsewhere -func (hu *hostUtil) DeviceOpened(pathname string) (bool, error) { - return false, nil -} - -// PathIsDevice determines if a path is a device. -func (hu *hostUtil) PathIsDevice(pathname string) (bool, error) { - return false, nil -} - -// MakeRShared checks that given path is on a mount with 'rshared' mount -// propagation. Empty implementation here. -func (hu *hostUtil) MakeRShared(path string) error { - return nil -} - -// GetFileType checks for sockets/block/character devices -func (hu *(hostUtil)) GetFileType(pathname string) (FileType, error) { - return getFileType(pathname) -} - -// MakeFile creates a new directory -func (hu *hostUtil) MakeDir(pathname string) error { - err := os.MkdirAll(pathname, os.FileMode(0755)) - if err != nil { - if !os.IsExist(err) { - return err - } - } - return nil -} - -// MakeFile creates an empty file -func (hu *hostUtil) MakeFile(pathname string) error { - f, err := os.OpenFile(pathname, os.O_CREATE, os.FileMode(0644)) - defer f.Close() - if err != nil { - if !os.IsExist(err) { - return err - } - } - return nil -} - -// PathExists checks whether the path exists -func (hu *hostUtil) PathExists(pathname string) (bool, error) { - return utilpath.Exists(utilpath.CheckFollowSymlink, pathname) -} - -// EvalHostSymlinks returns the path name after evaluating symlinks -func (hu *hostUtil) EvalHostSymlinks(pathname string) (string, error) { - return filepath.EvalSymlinks(pathname) -} - -// GetOwner returns the integer ID for the user and group of the given path -// Note that on windows, it always returns 0. We actually don't set Group on -// windows platform, see SetVolumeOwnership implementation. -func (hu *hostUtil) GetOwner(pathname string) (int64, int64, error) { - return -1, -1, nil -} - -func (hu *hostUtil) GetSELinuxSupport(pathname string) (bool, error) { - // Windows does not support SELinux. - return false, nil -} - -func (hu *hostUtil) GetMode(pathname string) (os.FileMode, error) { - info, err := os.Stat(pathname) - if err != nil { - return 0, err - } - return info.Mode(), nil -} diff --git a/pkg/util/mount/mount_windows_test.go b/pkg/util/mount/mount_windows_test.go index 9af846ddd9b..dcd8b161df6 100644 --- a/pkg/util/mount/mount_windows_test.go +++ b/pkg/util/mount/mount_windows_test.go @@ -30,48 +30,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNormalizeWindowsPath(t *testing.T) { - path := `/var/lib/kubelet/pods/146f8428-83e7-11e7-8dd4-000d3a31dac4/volumes/kubernetes.io~azure-disk` - normalizedPath := normalizeWindowsPath(path) - if normalizedPath != `c:\var\lib\kubelet\pods\146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~azure-disk` { - t.Errorf("normizeWindowsPath test failed, normalizedPath : %q", normalizedPath) - } - - path = `/var/lib/kubelet/pods/146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~azure-disk` - normalizedPath = normalizeWindowsPath(path) - if normalizedPath != `c:\var\lib\kubelet\pods\146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~azure-disk` { - t.Errorf("normizeWindowsPath test failed, normalizedPath : %q", normalizedPath) - } - - path = `/` - normalizedPath = normalizeWindowsPath(path) - if normalizedPath != `c:\` { - t.Errorf("normizeWindowsPath test failed, normalizedPath : %q", normalizedPath) - } -} - -func TestValidateDiskNumber(t *testing.T) { - diskNum := "0" - if err := ValidateDiskNumber(diskNum); err != nil { - t.Errorf("TestValidateDiskNumber test failed, disk number : %s", diskNum) - } - - diskNum = "99" - if err := ValidateDiskNumber(diskNum); err != nil { - t.Errorf("TestValidateDiskNumber test failed, disk number : %s", diskNum) - } - - diskNum = "ab" - if err := ValidateDiskNumber(diskNum); err == nil { - t.Errorf("TestValidateDiskNumber test failed, disk number : %s", diskNum) - } - - diskNum = "100" - if err := ValidateDiskNumber(diskNum); err == nil { - t.Errorf("TestValidateDiskNumber test failed, disk number : %s", diskNum) - } -} - func makeLink(link, target string) error { if output, err := exec.Command("cmd", "/c", "mklink", "/D", link, target).CombinedOutput(); err != nil { return fmt.Errorf("mklink failed: %v, link(%q) target(%q) output: %q", err, link, target, string(output)) @@ -174,55 +132,6 @@ func TestPathWithinBase(t *testing.T) { } } -func TestGetFileType(t *testing.T) { - hu := NewHostUtil() - - testCase := []struct { - name string - expectedType FileType - setUp func() (string, string, error) - }{ - { - "Directory Test", - FileTypeDirectory, - func() (string, string, error) { - tempDir, err := ioutil.TempDir("", "test-get-filetype-") - return tempDir, tempDir, err - }, - }, - { - "File Test", - FileTypeFile, - func() (string, string, error) { - tempFile, err := ioutil.TempFile("", "test-get-filetype") - if err != nil { - return "", "", err - } - tempFile.Close() - return tempFile.Name(), tempFile.Name(), nil - }, - }, - } - - for idx, tc := range testCase { - path, cleanUpPath, err := tc.setUp() - if err != nil { - t.Fatalf("[%d-%s] unexpected error : %v", idx, tc.name, err) - } - if len(cleanUpPath) > 0 { - defer os.RemoveAll(cleanUpPath) - } - - fileType, err := hu.GetFileType(path) - if err != nil { - t.Fatalf("[%d-%s] unexpected error : %v", idx, tc.name, err) - } - if fileType != tc.expectedType { - t.Fatalf("[%d-%s] expected %s, but got %s", idx, tc.name, tc.expectedType, fileType) - } - } -} - func TestIsLikelyNotMountPoint(t *testing.T) { mounter := Mounter{"fake/path"}