From 6a21076039ec41da36327ca56b9de78dd89873c9 Mon Sep 17 00:00:00 2001 From: Travis Rhoden Date: Thu, 22 Aug 2019 10:37:37 -0600 Subject: [PATCH 1/5] copy mount files to hostutils --- pkg/util/mount/{ => hostutils}/fake.go | 0 pkg/util/mount/hostutils/fake_hostutil.go | 280 ++++++ .../mount/{mount.go => hostutils/hostutil.go} | 0 .../hostutil_linux.go} | 0 .../hostutil_linux_test.go} | 0 .../hostutil_unsupported.go} | 0 .../hostutil_windows.go} | 0 .../hostutil_windows_test.go} | 0 pkg/util/mount/hostutils/mount.go | 373 +++++++ pkg/util/mount/hostutils/mount_linux.go | 840 ++++++++++++++++ pkg/util/mount/hostutils/mount_linux_test.go | 918 ++++++++++++++++++ pkg/util/mount/hostutils/mount_unsupported.go | 144 +++ pkg/util/mount/hostutils/mount_windows.go | 424 ++++++++ .../mount/hostutils/mount_windows_test.go | 424 ++++++++ 14 files changed, 3403 insertions(+) rename pkg/util/mount/{ => hostutils}/fake.go (100%) create mode 100644 pkg/util/mount/hostutils/fake_hostutil.go rename pkg/util/mount/{mount.go => hostutils/hostutil.go} (100%) rename pkg/util/mount/{mount_linux.go => hostutils/hostutil_linux.go} (100%) rename pkg/util/mount/{mount_linux_test.go => hostutils/hostutil_linux_test.go} (100%) rename pkg/util/mount/{mount_unsupported.go => hostutils/hostutil_unsupported.go} (100%) rename pkg/util/mount/{mount_windows.go => hostutils/hostutil_windows.go} (100%) rename pkg/util/mount/{mount_windows_test.go => hostutils/hostutil_windows_test.go} (100%) create mode 100644 pkg/util/mount/hostutils/mount.go create mode 100644 pkg/util/mount/hostutils/mount_linux.go create mode 100644 pkg/util/mount/hostutils/mount_linux_test.go create mode 100644 pkg/util/mount/hostutils/mount_unsupported.go create mode 100644 pkg/util/mount/hostutils/mount_windows.go create mode 100644 pkg/util/mount/hostutils/mount_windows_test.go diff --git a/pkg/util/mount/fake.go b/pkg/util/mount/hostutils/fake.go similarity index 100% rename from pkg/util/mount/fake.go rename to pkg/util/mount/hostutils/fake.go diff --git a/pkg/util/mount/hostutils/fake_hostutil.go b/pkg/util/mount/hostutils/fake_hostutil.go new file mode 100644 index 00000000000..3b85e672198 --- /dev/null +++ b/pkg/util/mount/hostutils/fake_hostutil.go @@ -0,0 +1,280 @@ +/* +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" + "path/filepath" + "sync" + + "k8s.io/klog" +) + +// FakeMounter implements mount.Interface for tests. +type FakeMounter struct { + MountPoints []MountPoint + Log []FakeAction + // Error to return for a path when calling IsLikelyNotMountPoint + MountCheckErrors map[string]error + // Some tests run things in parallel, make sure the mounter does not produce + // any golang's DATA RACE warnings. + mutex sync.Mutex +} + +var _ Interface = &FakeMounter{} + +const ( + // FakeActionMount is the string for specifying mount as FakeAction.Action + FakeActionMount = "mount" + // FakeActionUnmount is the string for specifying unmount as FakeAction.Action + FakeActionUnmount = "unmount" +) + +// FakeAction objects are logged every time a fake mount or unmount is called. +type FakeAction struct { + Action string // "mount" or "unmount" + Target string // applies to both mount and unmount actions + Source string // applies only to "mount" actions + FSType string // applies only to "mount" actions +} + +// ResetLog clears all the log entries in FakeMounter +func (f *FakeMounter) ResetLog() { + f.mutex.Lock() + defer f.mutex.Unlock() + + f.Log = []FakeAction{} +} + +// Mount records the mount event and updates the in-memory mount points for FakeMounter +func (f *FakeMounter) Mount(source string, target string, fstype string, options []string) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + opts := []string{} + + for _, option := range options { + // find 'bind' option + if option == "bind" { + // This is a bind-mount. In order to mimic linux behaviour, we must + // use the original device of the bind-mount as the real source. + // E.g. when mounted /dev/sda like this: + // $ mount /dev/sda /mnt/test + // $ mount -o bind /mnt/test /mnt/bound + // then /proc/mount contains: + // /dev/sda /mnt/test + // /dev/sda /mnt/bound + // (and not /mnt/test /mnt/bound) + // I.e. we must use /dev/sda as source instead of /mnt/test in the + // bind mount. + for _, mnt := range f.MountPoints { + if source == mnt.Path { + source = mnt.Device + break + } + } + } + // reuse MountPoint.Opts field to mark mount as readonly + opts = append(opts, option) + } + + // If target is a symlink, get its absolute path + absTarget, err := filepath.EvalSymlinks(target) + if err != nil { + absTarget = target + } + f.MountPoints = append(f.MountPoints, MountPoint{Device: source, Path: absTarget, Type: fstype, Opts: opts}) + klog.V(5).Infof("Fake mounter: mounted %s to %s", source, absTarget) + f.Log = append(f.Log, FakeAction{Action: FakeActionMount, Target: absTarget, Source: source, FSType: fstype}) + return nil +} + +// Unmount records the unmount event and updates the in-memory mount points for FakeMounter +func (f *FakeMounter) Unmount(target string) error { + f.mutex.Lock() + defer f.mutex.Unlock() + + // If target is a symlink, get its absolute path + absTarget, err := filepath.EvalSymlinks(target) + if err != nil { + absTarget = target + } + + newMountpoints := []MountPoint{} + for _, mp := range f.MountPoints { + if mp.Path == absTarget { + klog.V(5).Infof("Fake mounter: unmounted %s from %s", mp.Device, absTarget) + // Don't copy it to newMountpoints + continue + } + newMountpoints = append(newMountpoints, MountPoint{Device: mp.Device, Path: mp.Path, Type: mp.Type}) + } + f.MountPoints = newMountpoints + f.Log = append(f.Log, FakeAction{Action: FakeActionUnmount, Target: absTarget}) + delete(f.MountCheckErrors, target) + return nil +} + +// List returns all the in-memory mountpoints for FakeMounter +func (f *FakeMounter) List() ([]MountPoint, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + return f.MountPoints, nil +} + +// IsMountPointMatch tests if dir and mp are the same path +func (f *FakeMounter) IsMountPointMatch(mp MountPoint, dir string) bool { + return mp.Path == dir +} + +// IsLikelyNotMountPoint determines whether a path is a mountpoint by checking +// if the absolute path to file is in the in-memory mountpoints +func (f *FakeMounter) IsLikelyNotMountPoint(file string) (bool, error) { + f.mutex.Lock() + defer f.mutex.Unlock() + + err := f.MountCheckErrors[file] + if err != nil { + return false, err + } + + _, err = os.Stat(file) + if err != nil { + return true, err + } + + // If file is a symlink, get its absolute path + absFile, err := filepath.EvalSymlinks(file) + if err != nil { + absFile = file + } + + for _, mp := range f.MountPoints { + if mp.Path == absFile { + klog.V(5).Infof("isLikelyNotMountPoint for %s: mounted %s, false", file, mp.Path) + return false, nil + } + } + klog.V(5).Infof("isLikelyNotMountPoint for %s: true", file) + return true, nil +} + +// GetMountRefs finds all mount references to the path, returns a +// list of paths. +func (f *FakeMounter) GetMountRefs(pathname string) ([]string, error) { + realpath, err := filepath.EvalSymlinks(pathname) + if err != nil { + // Ignore error in FakeMounter, because we actually didn't create files. + realpath = pathname + } + 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/mount.go b/pkg/util/mount/hostutils/hostutil.go similarity index 100% rename from pkg/util/mount/mount.go rename to pkg/util/mount/hostutils/hostutil.go diff --git a/pkg/util/mount/mount_linux.go b/pkg/util/mount/hostutils/hostutil_linux.go similarity index 100% rename from pkg/util/mount/mount_linux.go rename to pkg/util/mount/hostutils/hostutil_linux.go diff --git a/pkg/util/mount/mount_linux_test.go b/pkg/util/mount/hostutils/hostutil_linux_test.go similarity index 100% rename from pkg/util/mount/mount_linux_test.go rename to pkg/util/mount/hostutils/hostutil_linux_test.go diff --git a/pkg/util/mount/mount_unsupported.go b/pkg/util/mount/hostutils/hostutil_unsupported.go similarity index 100% rename from pkg/util/mount/mount_unsupported.go rename to pkg/util/mount/hostutils/hostutil_unsupported.go diff --git a/pkg/util/mount/mount_windows.go b/pkg/util/mount/hostutils/hostutil_windows.go similarity index 100% rename from pkg/util/mount/mount_windows.go rename to pkg/util/mount/hostutils/hostutil_windows.go diff --git a/pkg/util/mount/mount_windows_test.go b/pkg/util/mount/hostutils/hostutil_windows_test.go similarity index 100% rename from pkg/util/mount/mount_windows_test.go rename to pkg/util/mount/hostutils/hostutil_windows_test.go diff --git a/pkg/util/mount/hostutils/mount.go b/pkg/util/mount/hostutils/mount.go new file mode 100644 index 00000000000..5f0fe1867f4 --- /dev/null +++ b/pkg/util/mount/hostutils/mount.go @@ -0,0 +1,373 @@ +/* +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" + "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. +type Interface interface { + // Mount mounts source to target as fstype with given options. + Mount(source string, target string, fstype string, options []string) error + // Unmount unmounts given target. + Unmount(target string) error + // List returns a list of all mounted filesystems. This can be large. + // On some platforms, reading mounts directly from the OS is not guaranteed + // consistent (i.e. it could change between chunked reads). This is guaranteed + // to be consistent. + List() ([]MountPoint, error) + // IsMountPointMatch determines if the mountpoint matches the dir. + IsMountPointMatch(mp MountPoint, dir string) bool + // IsLikelyNotMountPoint uses heuristics to determine if a directory + // is not a mountpoint. + // It should return ErrNotExist when the directory does not exist. + // IsLikelyNotMountPoint does NOT properly detect all mountpoint types + // most notably linux bind mounts and symbolic link. + IsLikelyNotMountPoint(file string) (bool, error) + // GetMountRefs finds all mount references to the path, returns a + // list of paths. Path could be a mountpoint path, device or a normal + // directory (for bind mount). + 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 + // stream. + Run(cmd string, args ...string) ([]byte, error) +} + +// Compile-time check to ensure all Mounter implementations satisfy +// 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 + Path string + Type string + Opts []string + Freq int + Pass int +} + +// SafeFormatAndMount probes a device to see if it is formatted. +// Namely it checks to see if a file system is present. If so it +// mounts it otherwise the device is formatted first then mounted. +type SafeFormatAndMount struct { + Interface + Exec +} + +// FormatAndMount formats the given disk, if needed, and mounts it. +// That is if the disk is not formatted and it is not being mounted as +// read-only it will format it first then mount it. Otherwise, if the +// disk is already formatted or it is being mounted as read-only, it +// will be mounted without formatting. +func (mounter *SafeFormatAndMount) FormatAndMount(source string, target string, fstype string, options []string) error { + return mounter.formatAndMount(source, target, fstype, options) +} + +// getMountRefsByDev finds all references to the device provided +// by mountPath; returns a list of paths. +// Note that mountPath should be path after the evaluation of any symblolic links. +func getMountRefsByDev(mounter Interface, mountPath string) ([]string, error) { + mps, err := mounter.List() + if err != nil { + return nil, err + } + + // Finding the device mounted to mountPath. + diskDev := "" + for i := range mps { + if mountPath == mps[i].Path { + diskDev = mps[i].Device + break + } + } + + // Find all references to the device. + var refs []string + for i := range mps { + if mps[i].Device == diskDev || mps[i].Device == mountPath { + if mps[i].Path != mountPath { + refs = append(refs, mps[i].Path) + } + } + } + return refs, nil +} + +// GetDeviceNameFromMount given a mnt point, find the device from /proc/mounts +// returns the device name, reference count, and error code. +func GetDeviceNameFromMount(mounter Interface, mountPath string) (string, int, error) { + mps, err := mounter.List() + if err != nil { + return "", 0, err + } + + // Find the device name. + // FIXME if multiple devices mounted on the same mount path, only the first one is returned. + device := "" + // If mountPath is symlink, need get its target path. + slTarget, err := filepath.EvalSymlinks(mountPath) + if err != nil { + slTarget = mountPath + } + for i := range mps { + if mps[i].Path == slTarget { + device = mps[i].Device + break + } + } + + // Find all references to the device. + refCount := 0 + for i := range mps { + if mps[i].Device == device { + refCount++ + } + } + return device, refCount, nil +} + +// IsNotMountPoint determines if a directory is a mountpoint. +// It should return ErrNotExist when the directory does not exist. +// IsNotMountPoint is more expensive than IsLikelyNotMountPoint. +// IsNotMountPoint detects bind mounts in linux. +// IsNotMountPoint enumerates all the mountpoints using List() and +// the list of mountpoints may be large, then it uses +// IsMountPointMatch to evaluate whether the directory is a mountpoint. +func IsNotMountPoint(mounter Interface, file string) (bool, error) { + // IsLikelyNotMountPoint provides a quick check + // to determine whether file IS A mountpoint. + notMnt, notMntErr := mounter.IsLikelyNotMountPoint(file) + if notMntErr != nil && os.IsPermission(notMntErr) { + // We were not allowed to do the simple stat() check, e.g. on NFS with + // root_squash. Fall back to /proc/mounts check below. + notMnt = true + notMntErr = nil + } + if notMntErr != nil { + return notMnt, notMntErr + } + // identified as mountpoint, so return this fact. + if notMnt == false { + return notMnt, nil + } + + // Resolve any symlinks in file, kernel would do the same and use the resolved path in /proc/mounts. + hu := NewHostUtil() + resolvedFile, err := hu.EvalHostSymlinks(file) + if err != nil { + return true, err + } + + // check all mountpoints since IsLikelyNotMountPoint + // is not reliable for some mountpoint types. + mountPoints, mountPointsErr := mounter.List() + if mountPointsErr != nil { + return notMnt, mountPointsErr + } + for _, mp := range mountPoints { + if mounter.IsMountPointMatch(mp, resolvedFile) { + notMnt = false + break + } + } + return notMnt, nil +} + +// IsBind detects whether a bind mount is being requested and makes the remount options to +// use in case of bind mount, due to the fact that bind mount doesn't respect mount options. +// The list equals: +// options - 'bind' + 'remount' (no duplicate) +func IsBind(options []string) (bool, []string, []string) { + // Because we have an FD opened on the subpath bind mount, the "bind" option + // needs to be included, otherwise the mount target will error as busy if you + // remount as readonly. + // + // As a consequence, all read only bind mounts will no longer change the underlying + // volume mount to be read only. + bindRemountOpts := []string{"bind", "remount"} + bind := false + bindOpts := []string{"bind"} + + // _netdev is a userspace mount option and does not automatically get added when + // bind mount is created and hence we must carry it over. + if checkForNetDev(options) { + bindOpts = append(bindOpts, "_netdev") + } + + for _, option := range options { + switch option { + case "bind": + bind = true + break + case "remount": + break + default: + bindRemountOpts = append(bindRemountOpts, option) + } + } + + return bind, bindOpts, bindRemountOpts +} + +func checkForNetDev(options []string) bool { + for _, option := range options { + if option == "_netdev" { + return true + } + } + return false +} + +// HasMountRefs checks if the given mountPath has mountRefs. +// TODO: this is a workaround for the unmount device issue caused by gci mounter. +// In GCI cluster, if gci mounter is used for mounting, the container started by mounter +// script will cause additional mounts created in the container. Since these mounts are +// irrelevant to the original mounts, they should be not considered when checking the +// mount references. Current solution is to filter out those mount paths that contain +// the string of original mount path. +// Plan to work on better approach to solve this issue. +func HasMountRefs(mountPath string, mountRefs []string) bool { + for _, ref := range mountRefs { + if !strings.Contains(ref, mountPath) { + return true + } + } + return false +} + +// PathWithinBase checks if give path is within given base directory. +func PathWithinBase(fullPath, basePath string) bool { + rel, err := filepath.Rel(basePath, fullPath) + if err != nil { + return false + } + if StartsWithBackstep(rel) { + // Needed to escape the base path. + return false + } + return true +} + +// StartsWithBackstep checks if the given path starts with a backstep segment. +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/hostutils/mount_linux.go b/pkg/util/mount/hostutils/mount_linux.go new file mode 100644 index 00000000000..1d5e71961ed --- /dev/null +++ b/pkg/util/mount/hostutils/mount_linux.go @@ -0,0 +1,840 @@ +// +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 ( + "errors" + "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 + procMountInfoPath = "/proc/self/mountinfo" + // 'fsck' found errors and corrected them + fsckErrorsCorrected = 1 + // 'fsck' found errors but exited without correcting them + fsckErrorsUncorrected = 4 +) + +// Mounter provides the default implementation of mount.Interface +// for the linux platform. This implementation assumes that the +// kubelet is running in the host's root mount namespace. +type Mounter struct { + mounterPath string + withSystemd bool +} + +// New returns a mount.Interface for the current system. +// It provides options to override the default mounter behavior. +// mounterPath allows using an alternative to `/bin/mount` for mounting. +func New(mounterPath string) Interface { + return &Mounter{ + mounterPath: mounterPath, + withSystemd: detectSystemd(), + } +} + +// Mount mounts source to target as fstype with given options. 'source' and 'fstype' must +// be an empty string in case it's not required, e.g. for remount, or for auto filesystem +// type, where kernel handles fstype for you. The mount 'options' is a list of options, +// currently come from mount(8), e.g. "ro", "remount", "bind", etc. If no more option is +// required, call Mount with an empty string list or nil. +func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { + // Path to mounter binary if containerized mounter is needed. Otherwise, it is set to empty. + // All Linux distros are expected to be shipped with a mount utility that a support bind mounts. + mounterPath := "" + bind, bindOpts, bindRemountOpts := IsBind(options) + if bind { + err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts) + if err != nil { + return err + } + return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts) + } + // The list of filesystems that require containerized mounter on GCI image cluster + fsTypesNeedMounter := map[string]struct{}{ + "nfs": {}, + "glusterfs": {}, + "ceph": {}, + "cifs": {}, + } + if _, ok := fsTypesNeedMounter[fstype]; ok { + mounterPath = mounter.mounterPath + } + return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options) +} + +// doMount runs the mount command. mounterPath is the path to mounter binary if containerized mounter is used. +func (mounter *Mounter) doMount(mounterPath string, mountCmd string, source string, target string, fstype string, options []string) error { + mountArgs := MakeMountArgs(source, target, fstype, options) + if len(mounterPath) > 0 { + mountArgs = append([]string{mountCmd}, mountArgs...) + mountCmd = mounterPath + } + + if mounter.withSystemd { + // Try to run mount via systemd-run --scope. This will escape the + // service where kubelet runs and any fuse daemons will be started in a + // specific scope. kubelet service than can be restarted without killing + // these fuse daemons. + // + // Complete command line (when mounterPath is not used): + // systemd-run --description=... --scope -- mount -t + // + // Expected flow: + // * systemd-run creates a transient scope (=~ cgroup) and executes its + // argument (/bin/mount) there. + // * mount does its job, forks a fuse daemon if necessary and finishes. + // (systemd-run --scope finishes at this point, returning mount's exit + // code and stdout/stderr - thats one of --scope benefits). + // * systemd keeps the fuse daemon running in the scope (i.e. in its own + // cgroup) until the fuse daemon dies (another --scope benefit). + // Kubelet service can be restarted and the fuse daemon survives. + // * When the fuse daemon dies (e.g. during unmount) systemd removes the + // scope automatically. + // + // systemd-mount is not used because it's too new for older distros + // (CentOS 7, Debian Jessie). + mountCmd, mountArgs = AddSystemdScope("systemd-run", target, mountCmd, mountArgs) + } else { + // No systemd-run on the host (or we failed to check it), assume kubelet + // does not run as a systemd service. + // No code here, mountCmd and mountArgs are already populated. + } + + klog.V(4).Infof("Mounting cmd (%s) with arguments (%s)", mountCmd, mountArgs) + command := exec.Command(mountCmd, mountArgs...) + output, err := command.CombinedOutput() + if err != nil { + args := strings.Join(mountArgs, " ") + klog.Errorf("Mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s\n", err, mountCmd, args, string(output)) + return fmt.Errorf("mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s", + err, mountCmd, args, string(output)) + } + return err +} + +// detectSystemd returns true if OS runs with systemd as init. When not sure +// (permission errors, ...), it returns false. +// There may be different ways how to detect systemd, this one makes sure that +// systemd-runs (needed by Mount()) works. +func detectSystemd() bool { + if _, err := exec.LookPath("systemd-run"); err != nil { + klog.V(2).Infof("Detected OS without systemd") + return false + } + // Try to run systemd-run --scope /bin/true, that should be enough + // to make sure that systemd is really running and not just installed, + // which happens when running in a container with a systemd-based image + // but with different pid 1. + cmd := exec.Command("systemd-run", "--description=Kubernetes systemd probe", "--scope", "true") + output, err := cmd.CombinedOutput() + if err != nil { + klog.V(2).Infof("Cannot run systemd-run, assuming non-systemd OS") + klog.V(4).Infof("systemd-run failed with: %v", err) + klog.V(4).Infof("systemd-run output: %s", string(output)) + return false + } + klog.V(2).Infof("Detected OS with systemd") + return true +} + +// MakeMountArgs makes the arguments to the mount(8) command. +// Implementation is shared with NsEnterMounter +func MakeMountArgs(source, target, fstype string, options []string) []string { + // Build mount command as follows: + // mount [-t $fstype] [-o $options] [$source] $target + mountArgs := []string{} + if len(fstype) > 0 { + mountArgs = append(mountArgs, "-t", fstype) + } + if len(options) > 0 { + mountArgs = append(mountArgs, "-o", strings.Join(options, ",")) + } + if len(source) > 0 { + mountArgs = append(mountArgs, source) + } + mountArgs = append(mountArgs, target) + + return mountArgs +} + +// AddSystemdScope adds "system-run --scope" to given command line +// implementation is shared with NsEnterMounter +func AddSystemdScope(systemdRunPath, mountName, command string, args []string) (string, []string) { + descriptionArg := fmt.Sprintf("--description=Kubernetes transient mount for %s", mountName) + systemdRunArgs := []string{descriptionArg, "--scope", "--", command} + return systemdRunPath, append(systemdRunArgs, args...) +} + +// Unmount unmounts the target. +func (mounter *Mounter) Unmount(target string) error { + klog.V(4).Infof("Unmounting %s", target) + command := exec.Command("umount", target) + output, err := command.CombinedOutput() + if err != nil { + return fmt.Errorf("unmount failed: %v\nUnmounting arguments: %s\nOutput: %s", err, target, string(output)) + } + return nil +} + +// List returns a list of all mounted filesystems. +func (*Mounter) List() ([]MountPoint, error) { + return ListProcMounts(procMountsPath) +} + +// IsMountPointMatch returns true if the path in mp is the same as dir +func (mounter *Mounter) IsMountPointMatch(mp MountPoint, dir string) bool { + deletedDir := fmt.Sprintf("%s\\040(deleted)", dir) + return ((mp.Path == dir) || (mp.Path == deletedDir)) +} + +// IsLikelyNotMountPoint determines if a directory is not a mountpoint. +// It is fast but not necessarily ALWAYS correct. If the path is in fact +// a bind mount from one part of a mount to another it will not be detected. +// It also can not distinguish between mountpoints and symbolic links. +// mkdir /tmp/a /tmp/b; mount --bind /tmp/a /tmp/b; IsLikelyNotMountPoint("/tmp/b") +// will return true. When in fact /tmp/b is a mount point. If this situation +// is of interest to you, don't use this function... +func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { + stat, err := os.Stat(file) + if err != nil { + return true, err + } + rootStat, err := os.Stat(filepath.Dir(strings.TrimSuffix(file, "/"))) + if err != nil { + return true, err + } + // If the directory has a different device as parent, then it is a mountpoint. + if stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev { + return false, nil + } + + return true, nil +} + +// GetMountRefs finds all mount references to pathname, returns a +// list of paths. Path could be a mountpoint path, device or a normal +// directory (for bind mount). +func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { + pathExists, pathErr := PathExists(pathname) + if !pathExists { + return []string{}, nil + } else if IsCorruptedMnt(pathErr) { + klog.Warningf("GetMountRefs found corrupted mount at %s, treating as unmounted path", pathname) + return []string{}, nil + } else if pathErr != nil { + return nil, fmt.Errorf("error checking path %s: %v", pathname, pathErr) + } + realpath, err := filepath.EvalSymlinks(pathname) + if err != nil { + return nil, err + } + return SearchMountPoints(realpath, procMountInfoPath) +} + +// formatAndMount uses unix utils to format and mount the given disk +func (mounter *SafeFormatAndMount) formatAndMount(source string, target string, fstype string, options []string) error { + readOnly := false + for _, option := range options { + if option == "ro" { + readOnly = true + break + } + } + + options = append(options, "defaults") + + if !readOnly { + // Run fsck on the disk to fix repairable issues, only do this for volumes requested as rw. + klog.V(4).Infof("Checking for issues with fsck on disk: %s", source) + args := []string{"-a", source} + out, err := mounter.Exec.Run("fsck", args...) + if err != nil { + ee, isExitError := err.(utilexec.ExitError) + switch { + case err == utilexec.ErrExecutableNotFound: + klog.Warningf("'fsck' not found on system; continuing mount without running 'fsck'.") + case isExitError && ee.ExitStatus() == fsckErrorsCorrected: + klog.Infof("Device %s has errors which were corrected by fsck.", source) + case isExitError && ee.ExitStatus() == fsckErrorsUncorrected: + return fmt.Errorf("'fsck' found errors on device %s but could not correct them: %s", source, string(out)) + case isExitError && ee.ExitStatus() > fsckErrorsUncorrected: + klog.Infof("`fsck` error %s", string(out)) + } + } + } + + // Try to mount the disk + klog.V(4).Infof("Attempting to mount disk: %s %s %s", fstype, source, target) + mountErr := mounter.Interface.Mount(source, target, fstype, options) + if mountErr != nil { + // Mount failed. This indicates either that the disk is unformatted or + // it contains an unexpected filesystem. + existingFormat, err := mounter.GetDiskFormat(source) + if err != nil { + return err + } + if existingFormat == "" { + if readOnly { + // Don't attempt to format if mounting as readonly, return an error to reflect this. + return errors.New("failed to mount unformatted volume as read only") + } + + // Disk is unformatted so format it. + args := []string{source} + // Use 'ext4' as the default + if len(fstype) == 0 { + fstype = "ext4" + } + + if fstype == "ext4" || fstype == "ext3" { + args = []string{ + "-F", // Force flag + "-m0", // Zero blocks reserved for super-user + source, + } + } + klog.Infof("Disk %q appears to be unformatted, attempting to format as type: %q with options: %v", source, fstype, args) + _, err := mounter.Exec.Run("mkfs."+fstype, args...) + if err == nil { + // the disk has been formatted successfully try to mount it again. + klog.Infof("Disk successfully formatted (mkfs): %s - %s %s", fstype, source, target) + return mounter.Interface.Mount(source, target, fstype, options) + } + klog.Errorf("format of disk %q failed: type:(%q) target:(%q) options:(%q)error:(%v)", source, fstype, target, options, err) + return err + } + // Disk is already formatted and failed to mount + if len(fstype) == 0 || fstype == existingFormat { + // This is mount error + return mountErr + } + // Block device is formatted with unexpected filesystem, let the user know + return fmt.Errorf("failed to mount the volume as %q, it already contains %s. Mount error: %v", fstype, existingFormat, mountErr) + } + return mountErr +} + +// GetDiskFormat uses 'blkid' to see if the given disk is unformatted +func (mounter *SafeFormatAndMount) GetDiskFormat(disk string) (string, error) { + args := []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", disk} + klog.V(4).Infof("Attempting to determine if disk %q is formatted using blkid with args: (%v)", disk, args) + dataOut, err := mounter.Exec.Run("blkid", args...) + output := string(dataOut) + klog.V(4).Infof("Output: %q, err: %v", output, err) + + if err != nil { + if exit, ok := err.(utilexec.ExitError); ok { + if exit.ExitStatus() == 2 { + // Disk device is unformatted. + // For `blkid`, if the specified token (TYPE/PTTYPE, etc) was + // not found, or no (specified) devices could be identified, an + // exit code of 2 is returned. + return "", nil + } + } + klog.Errorf("Could not determine if disk %q is formatted (%v)", disk, err) + return "", err + } + + var fstype, pttype string + + lines := strings.Split(output, "\n") + for _, l := range lines { + if len(l) <= 0 { + // Ignore empty line. + continue + } + cs := strings.Split(l, "=") + if len(cs) != 2 { + return "", fmt.Errorf("blkid returns invalid output: %s", output) + } + // TYPE is filesystem type, and PTTYPE is partition table type, according + // to https://www.kernel.org/pub/linux/utils/util-linux/v2.21/libblkid-docs/. + if cs[0] == "TYPE" { + fstype = cs[1] + } else if cs[0] == "PTTYPE" { + pttype = cs[1] + } + } + + if len(pttype) > 0 { + klog.V(4).Infof("Disk %s detected partition table type: %s", disk, pttype) + // Returns a special non-empty string as filesystem type, then kubelet + // will not format it. + return "unknown data, probably partitions", nil + } + + return fstype, nil +} + +// ListProcMounts is shared with NsEnterMounter +func ListProcMounts(mountFilePath string) ([]MountPoint, error) { + content, err := utilio.ConsistentRead(mountFilePath, maxListTries) + if err != nil { + return nil, err + } + return parseProcMounts(content) +} + +func parseProcMounts(content []byte) ([]MountPoint, error) { + out := []MountPoint{} + lines := strings.Split(string(content), "\n") + for _, line := range lines { + if line == "" { + // the last split() item is empty string following the last \n + continue + } + fields := strings.Fields(line) + if len(fields) != expectedNumFieldsPerLine { + return nil, fmt.Errorf("wrong number of fields (expected %d, got %d): %s", expectedNumFieldsPerLine, len(fields), line) + } + + mp := MountPoint{ + Device: fields[0], + Path: fields[1], + Type: fields[2], + Opts: strings.Split(fields[3], ","), + } + + freq, err := strconv.Atoi(fields[4]) + if err != nil { + return nil, err + } + mp.Freq = freq + + pass, err := strconv.Atoi(fields[5]) + if err != nil { + return nil, err + } + mp.Pass = pass + + out = append(out, mp) + } + 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. +// Some filesystems may share a source name, e.g. tmpfs. And for bind mounting, +// it's possible to mount a non-root path of a filesystem, so we need to use +// root path and major:minor to represent mount source uniquely. +// This implementation is shared between Linux and NsEnterMounter +func SearchMountPoints(hostSource, mountInfoPath string) ([]string, error) { + mis, err := parseMountInfo(mountInfoPath) + if err != nil { + return nil, err + } + + mountID := 0 + rootPath := "" + majorMinor := "" + + // Finding the underlying root path and major:minor if possible. + // We need search in backward order because it's possible for later mounts + // to overlap earlier mounts. + for i := len(mis) - 1; i >= 0; i-- { + if hostSource == mis[i].mountPoint || PathWithinBase(hostSource, mis[i].mountPoint) { + // If it's a mount point or path under a mount point. + mountID = mis[i].id + rootPath = filepath.Join(mis[i].root, strings.TrimPrefix(hostSource, mis[i].mountPoint)) + majorMinor = mis[i].majorMinor + break + } + } + + if rootPath == "" || majorMinor == "" { + return nil, fmt.Errorf("failed to get root path and major:minor for %s", hostSource) + } + + var refs []string + for i := range mis { + if mis[i].id == mountID { + // Ignore mount entry for mount source itself. + continue + } + if mis[i].root == rootPath && mis[i].majorMinor == majorMinor { + refs = append(refs, mis[i].mountPoint) + } + } + + return refs, nil +} diff --git a/pkg/util/mount/hostutils/mount_linux_test.go b/pkg/util/mount/hostutils/mount_linux_test.go new file mode 100644 index 00000000000..e8ff8251d8f --- /dev/null +++ b/pkg/util/mount/hostutils/mount_linux_test.go @@ -0,0 +1,918 @@ +// +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" + "reflect" + "strings" + "testing" + + "k8s.io/utils/exec" +) + +func TestReadProcMountsFrom(t *testing.T) { + successCase := + `/dev/0 /path/to/0 type0 flags 0 0 +/dev/1 /path/to/1 type1 flags 1 1 +/dev/2 /path/to/2 type2 flags,1,2=3 2 2 +` + // NOTE: readProcMountsFrom has been updated to using fnv.New32a() + mounts, err := parseProcMounts([]byte(successCase)) + if err != nil { + t.Errorf("expected success, got %v", err) + } + if len(mounts) != 3 { + t.Fatalf("expected 3 mounts, got %d", len(mounts)) + } + mp := MountPoint{"/dev/0", "/path/to/0", "type0", []string{"flags"}, 0, 0} + if !mountPointsEqual(&mounts[0], &mp) { + t.Errorf("got unexpected MountPoint[0]: %#v", mounts[0]) + } + mp = MountPoint{"/dev/1", "/path/to/1", "type1", []string{"flags"}, 1, 1} + if !mountPointsEqual(&mounts[1], &mp) { + t.Errorf("got unexpected MountPoint[1]: %#v", mounts[1]) + } + mp = MountPoint{"/dev/2", "/path/to/2", "type2", []string{"flags", "1", "2=3"}, 2, 2} + if !mountPointsEqual(&mounts[2], &mp) { + t.Errorf("got unexpected MountPoint[2]: %#v", mounts[2]) + } + + errorCases := []string{ + "/dev/0 /path/to/mount\n", + "/dev/1 /path/to/mount type flags a 0\n", + "/dev/2 /path/to/mount type flags 0 b\n", + } + for _, ec := range errorCases { + _, err := parseProcMounts([]byte(ec)) + if err == nil { + t.Errorf("expected error") + } + } +} + +func mountPointsEqual(a, b *MountPoint) bool { + if a.Device != b.Device || a.Path != b.Path || a.Type != b.Type || !reflect.DeepEqual(a.Opts, b.Opts) || a.Pass != b.Pass || a.Freq != b.Freq { + return false + } + return true +} + +func TestGetMountRefs(t *testing.T) { + fm := &FakeMounter{ + MountPoints: []MountPoint{ + {Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"}, + {Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2"}, + }, + } + + tests := []struct { + mountPath string + expectedRefs []string + }{ + { + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod", + []string{ + "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd", + }, + }, + { + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1", + []string{ + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2", + "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2", + }, + }, + { + "/var/fake/directory/that/doesnt/exist", + []string{}, + }, + } + + for i, test := range tests { + if refs, err := fm.GetMountRefs(test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { + t.Errorf("%d. getMountRefs(%q) = %v, %v; expected %v, nil", i, test.mountPath, refs, err, test.expectedRefs) + } + } +} + +func setEquivalent(set1, set2 []string) bool { + map1 := make(map[string]bool) + map2 := make(map[string]bool) + for _, s := range set1 { + map1[s] = true + } + for _, s := range set2 { + map2[s] = true + } + + for s := range map1 { + if !map2[s] { + return false + } + } + for s := range map2 { + if !map1[s] { + return false + } + } + return true +} + +func TestGetDeviceNameFromMount(t *testing.T) { + fm := &FakeMounter{ + MountPoints: []MountPoint{ + {Device: "/dev/disk/by-path/prefix-lun-1", + Path: "/mnt/111"}, + {Device: "/dev/disk/by-path/prefix-lun-1", + Path: "/mnt/222"}, + }, + } + + tests := []struct { + mountPath string + expectedDevice string + expectedRefs int + }{ + { + "/mnt/222", + "/dev/disk/by-path/prefix-lun-1", + 2, + }, + } + + for i, test := range tests { + if device, refs, err := GetDeviceNameFromMount(fm, test.mountPath); err != nil || test.expectedRefs != refs || test.expectedDevice != device { + t.Errorf("%d. GetDeviceNameFromMount(%s) = (%s, %d), %v; expected (%s,%d), nil", i, test.mountPath, device, refs, err, test.expectedDevice, test.expectedRefs) + } + } +} + +func TestGetMountRefsByDev(t *testing.T) { + fm := &FakeMounter{ + MountPoints: []MountPoint{ + {Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"}, + {Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2"}, + }, + } + + tests := []struct { + mountPath string + expectedRefs []string + }{ + { + "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd", + []string{ + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod", + }, + }, + { + "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2", + []string{ + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1", + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2", + }, + }, + } + + for i, test := range tests { + + if refs, err := getMountRefsByDev(fm, test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { + t.Errorf("%d. getMountRefsByDev(%q) = %v, %v; expected %v, nil", i, test.mountPath, refs, err, test.expectedRefs) + } + } +} + +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 + fullPath string + basePath string + expected bool + }{ + { + name: "good subpath", + fullPath: "/a/b/c", + basePath: "/a", + expected: true, + }, + { + name: "good subpath 2", + fullPath: "/a/b/c", + basePath: "/a/b", + expected: true, + }, + { + name: "good subpath end slash", + fullPath: "/a/b/c/", + basePath: "/a/b", + expected: true, + }, + { + name: "good subpath backticks", + fullPath: "/a/b/../c", + basePath: "/a", + expected: true, + }, + { + name: "good subpath equal", + fullPath: "/a/b/c", + basePath: "/a/b/c", + expected: true, + }, + { + name: "good subpath equal 2", + fullPath: "/a/b/c/", + basePath: "/a/b/c", + expected: true, + }, + { + name: "good subpath root", + fullPath: "/a", + basePath: "/", + expected: true, + }, + { + name: "bad subpath parent", + fullPath: "/a/b/c", + basePath: "/a/b/c/d", + expected: false, + }, + { + name: "bad subpath outside", + fullPath: "/b/c", + basePath: "/a/b/c", + expected: false, + }, + { + name: "bad subpath prefix", + fullPath: "/a/b/cd", + basePath: "/a/b/c", + expected: false, + }, + { + name: "bad subpath backticks", + fullPath: "/a/../b", + basePath: "/a", + expected: false, + }, + { + name: "configmap subpath", + fullPath: "/var/lib/kubelet/pods/uuid/volumes/kubernetes.io~configmap/config/..timestamp/file.txt", + basePath: "/var/lib/kubelet/pods/uuid/volumes/kubernetes.io~configmap/config", + expected: true, + }, + } + for _, test := range tests { + if PathWithinBase(test.fullPath, test.basePath) != test.expected { + t.Errorf("test %q failed: expected %v", test.name, test.expected) + } + + } +} + +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 +20 25 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw +21 25 0:6 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=4058156k,nr_inodes=1014539,mode=755 +22 21 0:14 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +23 25 0:19 / /run rw,nosuid,noexec,relatime shared:5 - tmpfs tmpfs rw,size=815692k,mode=755 +25 0 252:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered +26 19 0:12 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:8 - securityfs securityfs rw +27 21 0:21 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw +28 23 0:22 / /run/lock rw,nosuid,nodev,noexec,relatime shared:6 - tmpfs tmpfs rw,size=5120k +29 19 0:23 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755 +30 29 0:24 / /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 19 0:25 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:11 - pstore pstore rw +32 29 0:26 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,devices +33 29 0:27 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,freezer +34 29 0:28 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,pids +35 29 0:29 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,blkio +36 29 0:30 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,memory +37 29 0:31 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,perf_event +38 29 0:32 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,hugetlb +39 29 0:33 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,cpu,cpuacct +40 29 0:34 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,cpuset +41 29 0:35 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,net_cls,net_prio +58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordere +` + + testcases := []struct { + name string + source string + mountInfos string + expectedRefs []string + expectedErr error + }{ + { + "dir", + "/mnt/disks/vol1", + base, + nil, + nil, + }, + { + "dir-used", + "/mnt/disks/vol1", + base + ` +56 25 252:0 /mnt/disks/vol1 /var/lib/kubelet/pods/1890aef5-5a60-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered +57 25 0:45 / /mnt/disks/vol rw,relatime shared:36 - tmpfs tmpfs rw +`, + []string{"/var/lib/kubelet/pods/1890aef5-5a60-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, + nil, + }, + { + "tmpfs-vol", + "/mnt/disks/vol1", + base + `120 25 0:76 / /mnt/disks/vol1 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k +`, + nil, + nil, + }, + { + "tmpfs-vol-used-by-two-pods", + "/mnt/disks/vol1", + base + `120 25 0:76 / /mnt/disks/vol1 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k +196 25 0:76 / /var/lib/kubelet/pods/ade3ac21-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k +228 25 0:76 / /var/lib/kubelet/pods/ac60532d-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k +`, + []string{ + "/var/lib/kubelet/pods/ade3ac21-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585", + "/var/lib/kubelet/pods/ac60532d-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585", + }, + nil, + }, + { + "tmpfs-subdir-used-indirectly-via-bindmount-dir-by-one-pod", + "/mnt/vol1/foo", + base + `177 25 0:46 / /mnt/data rw,relatime shared:37 - tmpfs data rw +190 25 0:46 /vol1 /mnt/vol1 rw,relatime shared:37 - tmpfs data rw +191 25 0:46 /vol2 /mnt/vol2 rw,relatime shared:37 - tmpfs data rw +62 25 0:46 /vol1/foo /var/lib/kubelet/pods/e25f2f01-5b06-11e8-8694-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:37 - tmpfs data rw +`, + []string{"/var/lib/kubelet/pods/e25f2f01-5b06-11e8-8694-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, + nil, + }, + { + "dir-bindmounted", + "/mnt/disks/vol2", + base + `342 25 252:0 /mnt/disks/vol2 /mnt/disks/vol2 rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered +`, + nil, + nil, + }, + { + "dir-bindmounted-used-by-one-pod", + "/mnt/disks/vol2", + base + `342 25 252:0 /mnt/disks/vol2 /mnt/disks/vol2 rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered +77 25 252:0 /mnt/disks/vol2 /var/lib/kubelet/pods/f30dc360-5a5d-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-1fb30a1c rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered +`, + []string{"/var/lib/kubelet/pods/f30dc360-5a5d-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-1fb30a1c"}, + nil, + }, + { + "blockfs", + "/mnt/disks/blkvol1", + base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +`, + nil, + nil, + }, + { + "blockfs-used-by-one-pod", + "/mnt/disks/blkvol1", + base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +62 25 7:1 / /var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +`, + []string{"/var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, + nil, + }, + { + "blockfs-used-by-two-pods", + "/mnt/disks/blkvol1", + base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +62 25 7:1 / /var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +95 25 7:1 / /var/lib/kubelet/pods/4854a48b-5a64-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +`, + []string{"/var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test", + "/var/lib/kubelet/pods/4854a48b-5a64-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, + nil, + }, + } + tmpFile, err := ioutil.TempFile("", "test-get-filetype") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + for _, v := range testcases { + tmpFile.Truncate(0) + tmpFile.Seek(0, 0) + tmpFile.WriteString(v.mountInfos) + tmpFile.Sync() + refs, err := SearchMountPoints(v.source, tmpFile.Name()) + if !reflect.DeepEqual(refs, v.expectedRefs) { + t.Errorf("test %q: expected Refs: %#v, got %#v", v.name, v.expectedRefs, refs) + } + if !reflect.DeepEqual(err, v.expectedErr) { + t.Errorf("test %q: expected err: %v, got %v", v.name, v.expectedErr, err) + } + } +} diff --git a/pkg/util/mount/hostutils/mount_unsupported.go b/pkg/util/mount/hostutils/mount_unsupported.go new file mode 100644 index 00000000000..7cdf75ba5ee --- /dev/null +++ b/pkg/util/mount/hostutils/mount_unsupported.go @@ -0,0 +1,144 @@ +// +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 ( + "errors" + "os" +) + +// Mounter implements mount.Interface for unsupported platforms +type Mounter struct { + mounterPath string +} + +var errUnsupported = errors.New("util/mount on this platform is not supported") + +// New returns a mount.Interface for the current system. +// It provides options to override the default mounter behavior. +// mounterPath allows using an alternative to `/bin/mount` for mounting. +func New(mounterPath string) Interface { + return &Mounter{ + mounterPath: mounterPath, + } +} + +// Mount always returns an error on unsupported platforms +func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { + return errUnsupported +} + +// Unmount always returns an error on unsupported platforms +func (mounter *Mounter) Unmount(target string) error { + return errUnsupported +} + +// List always returns an error on unsupported platforms +func (mounter *Mounter) List() ([]MountPoint, error) { + return []MountPoint{}, errUnsupported +} + +// IsMountPointMatch returns true if the path in mp is the same as dir +func (mounter *Mounter) IsMountPointMatch(mp MountPoint, dir string) bool { + return (mp.Path == dir) +} + +// IsLikelyNotMountPoint always returns an error on unsupported platforms +func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { + return true, errUnsupported +} + +// GetMountRefs always returns an error on unsupported platforms +func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { + return nil, errUnsupported +} + +func (mounter *SafeFormatAndMount) formatAndMount(source string, target string, fstype string, options []string) error { + return mounter.Interface.Mount(source, target, fstype, options) +} + +func (mounter *SafeFormatAndMount) diskLooksUnformatted(disk string) (bool, error) { + return true, errUnsupported +} + +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/hostutils/mount_windows.go b/pkg/util/mount/hostutils/mount_windows.go new file mode 100644 index 00000000000..0dde2e8b23e --- /dev/null +++ b/pkg/util/mount/hostutils/mount_windows.go @@ -0,0 +1,424 @@ +// +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" + "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 +// for the windows platform. This implementation assumes that the +// kubelet is running in the host's root mount namespace. +type Mounter struct { + mounterPath string +} + +// New returns a mount.Interface for the current system. +// It provides options to override the default mounter behavior. +// mounterPath allows using an alternative to `/bin/mount` for mounting. +func New(mounterPath string) Interface { + return &Mounter{ + mounterPath: mounterPath, + } +} + +// acquire lock for smb mount +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) + + if source == "tmpfs" { + klog.V(3).Infof("mounting source (%q), target (%q), with options (%q)", source, target, options) + return os.MkdirAll(target, 0755) + } + + parentDir := filepath.Dir(target) + if err := os.MkdirAll(parentDir, 0755); err != nil { + return err + } + + klog.V(4).Infof("mount options(%q) source:%q, target:%q, fstype:%q, begin to mount", + options, source, target, fstype) + bindSource := source + + // 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) + } else { + if len(options) < 2 { + klog.Warningf("mount options(%q) command number(%d) less than 2, source:%q, target:%q, skip mounting", + options, len(options), source, target) + return nil + } + + // currently only cifs mount is supported + if strings.ToLower(fstype) != "cifs" { + return fmt.Errorf("only cifs mount is supported now, fstype: %q, mounting source (%q), target (%q), with options (%q)", fstype, source, target, options) + } + + // lock smb mount for the same source + getSMBMountMutex.LockKey(source) + defer getSMBMountMutex.UnlockKey(source) + + if output, err := newSMBMapping(options[0], options[1], source); err != nil { + if isSMBMappingExist(source) { + klog.V(2).Infof("SMB Mapping(%s) already exists, now begin to remove and remount", source) + if output, err := removeSMBMapping(source); err != nil { + return fmt.Errorf("Remove-SmbGlobalMapping failed: %v, output: %q", err, output) + } + if output, err := newSMBMapping(options[0], options[1], source); err != nil { + return fmt.Errorf("New-SmbGlobalMapping remount failed: %v, output: %q", err, output) + } + } else { + return fmt.Errorf("New-SmbGlobalMapping failed: %v, output: %q", err, output) + } + } + } + + if output, err := exec.Command("cmd", "/c", "mklink", "/D", target, bindSource).CombinedOutput(); err != nil { + klog.Errorf("mklink failed: %v, source(%q) target(%q) output: %q", err, bindSource, target, string(output)) + return err + } + + return nil +} + +// do the SMB mount with username, password, remotepath +// return (output, error) +func newSMBMapping(username, password, remotepath string) (string, error) { + if username == "" || password == "" || remotepath == "" { + return "", fmt.Errorf("invalid parameter(username: %s, password: %s, remoteapth: %s)", username, password, remotepath) + } + + // use PowerShell Environment Variables to store user input string to prevent command line injection + // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1 + cmdLine := `$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force` + + `;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord` + + `;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential` + cmd := exec.Command("powershell", "/c", cmdLine) + cmd.Env = append(os.Environ(), + fmt.Sprintf("smbuser=%s", username), + fmt.Sprintf("smbpassword=%s", password), + fmt.Sprintf("smbremotepath=%s", remotepath)) + + output, err := cmd.CombinedOutput() + return string(output), err +} + +// check whether remotepath is already mounted +func isSMBMappingExist(remotepath string) bool { + cmd := exec.Command("powershell", "/c", `Get-SmbGlobalMapping -RemotePath $Env:smbremotepath`) + cmd.Env = append(os.Environ(), fmt.Sprintf("smbremotepath=%s", remotepath)) + _, err := cmd.CombinedOutput() + return err == nil +} + +// remove SMB mapping +func removeSMBMapping(remotepath string) (string, error) { + cmd := exec.Command("powershell", "/c", `Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force`) + cmd.Env = append(os.Environ(), fmt.Sprintf("smbremotepath=%s", remotepath)) + output, err := cmd.CombinedOutput() + return string(output), err +} + +// Unmount unmounts the target. +func (mounter *Mounter) Unmount(target string) error { + klog.V(4).Infof("azureMount: Unmount target (%q)", 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 + } + return nil +} + +// List returns a list of all mounted filesystems. todo +func (mounter *Mounter) List() ([]MountPoint, error) { + return []MountPoint{}, nil +} + +// IsMountPointMatch determines if the mountpoint matches the dir +func (mounter *Mounter) IsMountPointMatch(mp MountPoint, dir string) bool { + return mp.Path == dir +} + +// IsLikelyNotMountPoint determines if a directory is not a mountpoint. +func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { + stat, err := os.Lstat(file) + if err != nil { + return true, err + } + // If current file is a symlink, then it is a mountpoint. + if stat.Mode()&os.ModeSymlink != 0 { + target, err := os.Readlink(file) + if err != nil { + return true, fmt.Errorf("readlink error: %v", err) + } + hu := NewHostUtil() + exists, err := hu.PathExists(target) + if err != nil { + return true, err + } + return !exists, nil + } + + return true, nil +} + +// 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) + pathExists, pathErr := PathExists(windowsPath) + if !pathExists { + return []string{}, nil + } else if IsCorruptedMnt(pathErr) { + klog.Warningf("GetMountRefs found corrupted mount at %s, treating as unmounted path", windowsPath) + return []string{}, nil + } else if pathErr != nil { + return nil, fmt.Errorf("error checking path %s: %v", windowsPath, pathErr) + } + return []string{pathname}, nil +} + +func (mounter *SafeFormatAndMount) formatAndMount(source string, target string, fstype string, options []string) error { + // Try to mount the disk + klog.V(4).Infof("Attempting to formatAndMount disk: %s %s %s", fstype, source, target) + + if err := ValidateDiskNumber(source); err != nil { + klog.Errorf("diskMount: formatAndMount failed, err: %v", err) + return err + } + + if len(fstype) == 0 { + // Use 'NTFS' as the default + fstype = "NTFS" + } + + // format disk if it is unformatted(raw) + cmd := fmt.Sprintf("Get-Disk -Number %s | Where partitionstyle -eq 'raw' | Initialize-Disk -PartitionStyle MBR -PassThru"+ + " | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -FileSystem %s -Confirm:$false", source, fstype) + if output, err := mounter.Exec.Run("powershell", "/c", cmd); err != nil { + return fmt.Errorf("diskMount: format disk failed, error: %v, output: %q", err, string(output)) + } + klog.V(4).Infof("diskMount: Disk successfully formatted, disk: %q, fstype: %q", source, fstype) + + driveLetter, err := getDriveLetterByDiskNumber(source, mounter.Exec) + if err != nil { + return err + } + driverPath := driveLetter + ":" + 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)) + return err + } + 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) + output, err := exec.Run("powershell", "/c", cmd) + if err != nil { + return "", fmt.Errorf("azureMount: Get Drive Letter failed: %v, output: %q", err, string(output)) + } + if len(string(output)) < 1 { + return "", fmt.Errorf("azureMount: Get Drive Letter failed, output is empty") + } + return string(output)[:1], nil +} + +// getAllParentLinks walks all symbolic links and return all the parent targets recursively +func getAllParentLinks(path string) ([]string, error) { + const maxIter = 255 + links := []string{} + for { + links = append(links, path) + if len(links) > maxIter { + return links, fmt.Errorf("unexpected length of parent links: %v", links) + } + + fi, err := os.Lstat(path) + if err != nil { + return links, fmt.Errorf("Lstat: %v", err) + } + if fi.Mode()&os.ModeSymlink == 0 { + break + } + + path, err = os.Readlink(path) + if err != nil { + return links, fmt.Errorf("Readlink error: %v", err) + } + } + + 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/hostutils/mount_windows_test.go b/pkg/util/mount/hostutils/mount_windows_test.go new file mode 100644 index 00000000000..9af846ddd9b --- /dev/null +++ b/pkg/util/mount/hostutils/mount_windows_test.go @@ -0,0 +1,424 @@ +// +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" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "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)) + } + return nil +} + +func removeLink(link string) error { + if output, err := exec.Command("cmd", "/c", "rmdir", link).CombinedOutput(); err != nil { + return fmt.Errorf("rmdir failed: %v, output: %q", err, string(output)) + } + return nil +} + +func setEquivalent(set1, set2 []string) bool { + map1 := make(map[string]bool) + map2 := make(map[string]bool) + for _, s := range set1 { + map1[s] = true + } + for _, s := range set2 { + map2[s] = true + } + + for s := range map1 { + if !map2[s] { + return false + } + } + for s := range map2 { + if !map1[s] { + return false + } + } + return true +} + +// this func must run in admin mode, otherwise it will fail +func TestGetMountRefs(t *testing.T) { + tests := []struct { + mountPath string + expectedRefs []string + }{ + { + mountPath: `c:\windows`, + expectedRefs: []string{`c:\windows`}, + }, + { + mountPath: `c:\doesnotexist`, + expectedRefs: []string{}, + }, + } + + mounter := Mounter{"fake/path"} + + for _, test := range tests { + if refs, err := mounter.GetMountRefs(test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { + t.Errorf("getMountRefs(%q) = %v, error: %v; expected %v", test.mountPath, refs, err, test.expectedRefs) + } + } +} + +func TestPathWithinBase(t *testing.T) { + tests := []struct { + fullPath string + basePath string + expectedResult bool + }{ + { + fullPath: `c:\tmp\a\b\c`, + basePath: `c:\tmp`, + expectedResult: true, + }, + { + fullPath: `c:\tmp1`, + basePath: `c:\tmp2`, + expectedResult: false, + }, + { + fullPath: `c:\tmp`, + basePath: `c:\tmp`, + expectedResult: true, + }, + { + fullPath: `c:\tmp`, + basePath: `c:\tmp\a\b\c`, + expectedResult: false, + }, + { + fullPath: `c:\kubelet\pods\uuid\volumes\kubernetes.io~configmap\config\..timestamp\file.txt`, + basePath: `c:\kubelet\pods\uuid\volumes\kubernetes.io~configmap\config`, + expectedResult: true, + }, + } + + for _, test := range tests { + result := PathWithinBase(test.fullPath, test.basePath) + assert.Equal(t, result, test.expectedResult, "Expect result not equal with PathWithinBase(%s, %s) return: %q, expected: %q", + test.fullPath, test.basePath, result, test.expectedResult) + } +} + +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"} + + tests := []struct { + fileName string + targetLinkName string + setUp func(base, fileName, targetLinkName string) error + expectedResult bool + expectError bool + }{ + { + "Dir", + "", + func(base, fileName, targetLinkName string) error { + return os.Mkdir(filepath.Join(base, fileName), 0750) + }, + true, + false, + }, + { + "InvalidDir", + "", + func(base, fileName, targetLinkName string) error { + return nil + }, + true, + true, + }, + { + "ValidSymLink", + "targetSymLink", + func(base, fileName, targetLinkName string) error { + targeLinkPath := filepath.Join(base, targetLinkName) + if err := os.Mkdir(targeLinkPath, 0750); err != nil { + return err + } + + filePath := filepath.Join(base, fileName) + if err := makeLink(filePath, targeLinkPath); err != nil { + return err + } + return nil + }, + false, + false, + }, + { + "InvalidSymLink", + "targetSymLink2", + func(base, fileName, targetLinkName string) error { + targeLinkPath := filepath.Join(base, targetLinkName) + if err := os.Mkdir(targeLinkPath, 0750); err != nil { + return err + } + + filePath := filepath.Join(base, fileName) + if err := makeLink(filePath, targeLinkPath); err != nil { + return err + } + return removeLink(targeLinkPath) + }, + true, + false, + }, + } + + for _, test := range tests { + base, err := ioutil.TempDir("", test.fileName) + if err != nil { + t.Fatalf(err.Error()) + } + + defer os.RemoveAll(base) + + if err := test.setUp(base, test.fileName, test.targetLinkName); err != nil { + t.Fatalf("unexpected error in setUp(%s, %s): %v", test.fileName, test.targetLinkName, err) + } + + filePath := filepath.Join(base, test.fileName) + result, err := mounter.IsLikelyNotMountPoint(filePath) + assert.Equal(t, result, test.expectedResult, "Expect result not equal with IsLikelyNotMountPoint(%s) return: %q, expected: %q", + filePath, result, test.expectedResult) + + if test.expectError { + assert.NotNil(t, err, "Expect error during IsLikelyNotMountPoint(%s)", filePath) + } else { + assert.Nil(t, err, "Expect error is nil during IsLikelyNotMountPoint(%s)", filePath) + } + } +} + +func TestFormatAndMount(t *testing.T) { + fakeMounter := ErrorMounter{&FakeMounter{}, 0, nil} + execCallback := func(cmd string, args ...string) ([]byte, error) { + for j := range args { + if strings.Contains(args[j], "Get-Disk -Number") { + return []byte("0"), nil + } + + if strings.Contains(args[j], "Get-Partition -DiskNumber") { + return []byte("0"), nil + } + + if strings.Contains(args[j], "mklink") { + return nil, nil + } + } + return nil, fmt.Errorf("Unexpected cmd %s, args %v", cmd, args) + } + fakeExec := NewFakeExec(execCallback) + + mounter := SafeFormatAndMount{ + Interface: &fakeMounter, + Exec: fakeExec, + } + + tests := []struct { + device string + target string + fstype string + mountOptions []string + expectError bool + }{ + { + "0", + "disk", + "NTFS", + []string{}, + false, + }, + { + "0", + "disk", + "", + []string{}, + false, + }, + { + "invalidDevice", + "disk", + "NTFS", + []string{}, + true, + }, + } + + for _, test := range tests { + base, err := ioutil.TempDir("", test.device) + if err != nil { + t.Fatalf(err.Error()) + } + defer os.RemoveAll(base) + + target := filepath.Join(base, test.target) + err = mounter.FormatAndMount(test.device, target, test.fstype, test.mountOptions) + if test.expectError { + assert.NotNil(t, err, "Expect error during FormatAndMount(%s, %s, %s, %v)", test.device, test.target, test.fstype, test.mountOptions) + } else { + assert.Nil(t, err, "Expect error is nil during FormatAndMount(%s, %s, %s, %v)", test.device, test.target, test.fstype, test.mountOptions) + } + } +} + +func TestNewSMBMapping(t *testing.T) { + tests := []struct { + username string + password string + remotepath string + expectError bool + }{ + { + "", + "password", + `\\remotepath`, + true, + }, + { + "username", + "", + `\\remotepath`, + true, + }, + { + "username", + "password", + "", + true, + }, + } + + for _, test := range tests { + _, err := newSMBMapping(test.username, test.password, test.remotepath) + if test.expectError { + assert.NotNil(t, err, "Expect error during newSMBMapping(%s, %s, %s, %v)", test.username, test.password, test.remotepath) + } else { + assert.Nil(t, err, "Expect error is nil during newSMBMapping(%s, %s, %s, %v)", test.username, test.password, test.remotepath) + } + } +} From 8bac9092870138eac2661a1518aa7bc3493d7675 Mon Sep 17 00:00:00 2001 From: Travis Rhoden Date: Thu, 22 Aug 2019 10:39:18 -0600 Subject: [PATCH 2/5] move files back to mount --- pkg/util/mount/{hostutils => }/fake.go | 0 pkg/util/mount/{hostutils => }/fake_hostutil.go | 0 pkg/util/mount/{hostutils => }/hostutil.go | 0 pkg/util/mount/{hostutils => }/hostutil_linux.go | 0 pkg/util/mount/{hostutils => }/hostutil_linux_test.go | 0 pkg/util/mount/{hostutils => }/hostutil_unsupported.go | 0 pkg/util/mount/{hostutils => }/hostutil_windows.go | 0 pkg/util/mount/{hostutils => }/hostutil_windows_test.go | 0 pkg/util/mount/{hostutils => }/mount.go | 0 pkg/util/mount/{hostutils => }/mount_linux.go | 0 pkg/util/mount/{hostutils => }/mount_linux_test.go | 0 pkg/util/mount/{hostutils => }/mount_unsupported.go | 0 pkg/util/mount/{hostutils => }/mount_windows.go | 0 pkg/util/mount/{hostutils => }/mount_windows_test.go | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename pkg/util/mount/{hostutils => }/fake.go (100%) rename pkg/util/mount/{hostutils => }/fake_hostutil.go (100%) rename pkg/util/mount/{hostutils => }/hostutil.go (100%) rename pkg/util/mount/{hostutils => }/hostutil_linux.go (100%) rename pkg/util/mount/{hostutils => }/hostutil_linux_test.go (100%) rename pkg/util/mount/{hostutils => }/hostutil_unsupported.go (100%) rename pkg/util/mount/{hostutils => }/hostutil_windows.go (100%) rename pkg/util/mount/{hostutils => }/hostutil_windows_test.go (100%) rename pkg/util/mount/{hostutils => }/mount.go (100%) rename pkg/util/mount/{hostutils => }/mount_linux.go (100%) rename pkg/util/mount/{hostutils => }/mount_linux_test.go (100%) rename pkg/util/mount/{hostutils => }/mount_unsupported.go (100%) rename pkg/util/mount/{hostutils => }/mount_windows.go (100%) rename pkg/util/mount/{hostutils => }/mount_windows_test.go (100%) diff --git a/pkg/util/mount/hostutils/fake.go b/pkg/util/mount/fake.go similarity index 100% rename from pkg/util/mount/hostutils/fake.go rename to pkg/util/mount/fake.go diff --git a/pkg/util/mount/hostutils/fake_hostutil.go b/pkg/util/mount/fake_hostutil.go similarity index 100% rename from pkg/util/mount/hostutils/fake_hostutil.go rename to pkg/util/mount/fake_hostutil.go diff --git a/pkg/util/mount/hostutils/hostutil.go b/pkg/util/mount/hostutil.go similarity index 100% rename from pkg/util/mount/hostutils/hostutil.go rename to pkg/util/mount/hostutil.go diff --git a/pkg/util/mount/hostutils/hostutil_linux.go b/pkg/util/mount/hostutil_linux.go similarity index 100% rename from pkg/util/mount/hostutils/hostutil_linux.go rename to pkg/util/mount/hostutil_linux.go diff --git a/pkg/util/mount/hostutils/hostutil_linux_test.go b/pkg/util/mount/hostutil_linux_test.go similarity index 100% rename from pkg/util/mount/hostutils/hostutil_linux_test.go rename to pkg/util/mount/hostutil_linux_test.go diff --git a/pkg/util/mount/hostutils/hostutil_unsupported.go b/pkg/util/mount/hostutil_unsupported.go similarity index 100% rename from pkg/util/mount/hostutils/hostutil_unsupported.go rename to pkg/util/mount/hostutil_unsupported.go diff --git a/pkg/util/mount/hostutils/hostutil_windows.go b/pkg/util/mount/hostutil_windows.go similarity index 100% rename from pkg/util/mount/hostutils/hostutil_windows.go rename to pkg/util/mount/hostutil_windows.go diff --git a/pkg/util/mount/hostutils/hostutil_windows_test.go b/pkg/util/mount/hostutil_windows_test.go similarity index 100% rename from pkg/util/mount/hostutils/hostutil_windows_test.go rename to pkg/util/mount/hostutil_windows_test.go diff --git a/pkg/util/mount/hostutils/mount.go b/pkg/util/mount/mount.go similarity index 100% rename from pkg/util/mount/hostutils/mount.go rename to pkg/util/mount/mount.go diff --git a/pkg/util/mount/hostutils/mount_linux.go b/pkg/util/mount/mount_linux.go similarity index 100% rename from pkg/util/mount/hostutils/mount_linux.go rename to pkg/util/mount/mount_linux.go diff --git a/pkg/util/mount/hostutils/mount_linux_test.go b/pkg/util/mount/mount_linux_test.go similarity index 100% rename from pkg/util/mount/hostutils/mount_linux_test.go rename to pkg/util/mount/mount_linux_test.go diff --git a/pkg/util/mount/hostutils/mount_unsupported.go b/pkg/util/mount/mount_unsupported.go similarity index 100% rename from pkg/util/mount/hostutils/mount_unsupported.go rename to pkg/util/mount/mount_unsupported.go diff --git a/pkg/util/mount/hostutils/mount_windows.go b/pkg/util/mount/mount_windows.go similarity index 100% rename from pkg/util/mount/hostutils/mount_windows.go rename to pkg/util/mount/mount_windows.go diff --git a/pkg/util/mount/hostutils/mount_windows_test.go b/pkg/util/mount/mount_windows_test.go similarity index 100% rename from pkg/util/mount/hostutils/mount_windows_test.go rename to pkg/util/mount/mount_windows_test.go From c55b19bc0a6ebde310ba439936f3839dbec2bc94 Mon Sep 17 00:00:00 2001 From: Travis Rhoden Date: Thu, 22 Aug 2019 10:42:02 -0600 Subject: [PATCH 3/5] copy testfiles to hostutil dir --- .../mount_helper_unix_test.go} | 0 .../mount_helper_windows_test.go} | 0 pkg/util/mount/hostutil/mount_linux_test.go | 918 ++++++++++++++++++ pkg/util/mount/hostutil/mount_windows_test.go | 424 ++++++++ 4 files changed, 1342 insertions(+) rename pkg/util/mount/{mount_linux_test.go => hostutil/mount_helper_unix_test.go} (100%) rename pkg/util/mount/{mount_windows_test.go => hostutil/mount_helper_windows_test.go} (100%) create mode 100644 pkg/util/mount/hostutil/mount_linux_test.go create mode 100644 pkg/util/mount/hostutil/mount_windows_test.go diff --git a/pkg/util/mount/mount_linux_test.go b/pkg/util/mount/hostutil/mount_helper_unix_test.go similarity index 100% rename from pkg/util/mount/mount_linux_test.go rename to pkg/util/mount/hostutil/mount_helper_unix_test.go diff --git a/pkg/util/mount/mount_windows_test.go b/pkg/util/mount/hostutil/mount_helper_windows_test.go similarity index 100% rename from pkg/util/mount/mount_windows_test.go rename to pkg/util/mount/hostutil/mount_helper_windows_test.go diff --git a/pkg/util/mount/hostutil/mount_linux_test.go b/pkg/util/mount/hostutil/mount_linux_test.go new file mode 100644 index 00000000000..e8ff8251d8f --- /dev/null +++ b/pkg/util/mount/hostutil/mount_linux_test.go @@ -0,0 +1,918 @@ +// +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" + "reflect" + "strings" + "testing" + + "k8s.io/utils/exec" +) + +func TestReadProcMountsFrom(t *testing.T) { + successCase := + `/dev/0 /path/to/0 type0 flags 0 0 +/dev/1 /path/to/1 type1 flags 1 1 +/dev/2 /path/to/2 type2 flags,1,2=3 2 2 +` + // NOTE: readProcMountsFrom has been updated to using fnv.New32a() + mounts, err := parseProcMounts([]byte(successCase)) + if err != nil { + t.Errorf("expected success, got %v", err) + } + if len(mounts) != 3 { + t.Fatalf("expected 3 mounts, got %d", len(mounts)) + } + mp := MountPoint{"/dev/0", "/path/to/0", "type0", []string{"flags"}, 0, 0} + if !mountPointsEqual(&mounts[0], &mp) { + t.Errorf("got unexpected MountPoint[0]: %#v", mounts[0]) + } + mp = MountPoint{"/dev/1", "/path/to/1", "type1", []string{"flags"}, 1, 1} + if !mountPointsEqual(&mounts[1], &mp) { + t.Errorf("got unexpected MountPoint[1]: %#v", mounts[1]) + } + mp = MountPoint{"/dev/2", "/path/to/2", "type2", []string{"flags", "1", "2=3"}, 2, 2} + if !mountPointsEqual(&mounts[2], &mp) { + t.Errorf("got unexpected MountPoint[2]: %#v", mounts[2]) + } + + errorCases := []string{ + "/dev/0 /path/to/mount\n", + "/dev/1 /path/to/mount type flags a 0\n", + "/dev/2 /path/to/mount type flags 0 b\n", + } + for _, ec := range errorCases { + _, err := parseProcMounts([]byte(ec)) + if err == nil { + t.Errorf("expected error") + } + } +} + +func mountPointsEqual(a, b *MountPoint) bool { + if a.Device != b.Device || a.Path != b.Path || a.Type != b.Type || !reflect.DeepEqual(a.Opts, b.Opts) || a.Pass != b.Pass || a.Freq != b.Freq { + return false + } + return true +} + +func TestGetMountRefs(t *testing.T) { + fm := &FakeMounter{ + MountPoints: []MountPoint{ + {Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"}, + {Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2"}, + }, + } + + tests := []struct { + mountPath string + expectedRefs []string + }{ + { + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod", + []string{ + "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd", + }, + }, + { + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1", + []string{ + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2", + "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2", + }, + }, + { + "/var/fake/directory/that/doesnt/exist", + []string{}, + }, + } + + for i, test := range tests { + if refs, err := fm.GetMountRefs(test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { + t.Errorf("%d. getMountRefs(%q) = %v, %v; expected %v, nil", i, test.mountPath, refs, err, test.expectedRefs) + } + } +} + +func setEquivalent(set1, set2 []string) bool { + map1 := make(map[string]bool) + map2 := make(map[string]bool) + for _, s := range set1 { + map1[s] = true + } + for _, s := range set2 { + map2[s] = true + } + + for s := range map1 { + if !map2[s] { + return false + } + } + for s := range map2 { + if !map1[s] { + return false + } + } + return true +} + +func TestGetDeviceNameFromMount(t *testing.T) { + fm := &FakeMounter{ + MountPoints: []MountPoint{ + {Device: "/dev/disk/by-path/prefix-lun-1", + Path: "/mnt/111"}, + {Device: "/dev/disk/by-path/prefix-lun-1", + Path: "/mnt/222"}, + }, + } + + tests := []struct { + mountPath string + expectedDevice string + expectedRefs int + }{ + { + "/mnt/222", + "/dev/disk/by-path/prefix-lun-1", + 2, + }, + } + + for i, test := range tests { + if device, refs, err := GetDeviceNameFromMount(fm, test.mountPath); err != nil || test.expectedRefs != refs || test.expectedDevice != device { + t.Errorf("%d. GetDeviceNameFromMount(%s) = (%s, %d), %v; expected (%s,%d), nil", i, test.mountPath, device, refs, err, test.expectedDevice, test.expectedRefs) + } + } +} + +func TestGetMountRefsByDev(t *testing.T) { + fm := &FakeMounter{ + MountPoints: []MountPoint{ + {Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"}, + {Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1"}, + {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2"}, + }, + } + + tests := []struct { + mountPath string + expectedRefs []string + }{ + { + "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd", + []string{ + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod", + }, + }, + { + "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2", + []string{ + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1", + "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2", + }, + }, + } + + for i, test := range tests { + + if refs, err := getMountRefsByDev(fm, test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { + t.Errorf("%d. getMountRefsByDev(%q) = %v, %v; expected %v, nil", i, test.mountPath, refs, err, test.expectedRefs) + } + } +} + +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 + fullPath string + basePath string + expected bool + }{ + { + name: "good subpath", + fullPath: "/a/b/c", + basePath: "/a", + expected: true, + }, + { + name: "good subpath 2", + fullPath: "/a/b/c", + basePath: "/a/b", + expected: true, + }, + { + name: "good subpath end slash", + fullPath: "/a/b/c/", + basePath: "/a/b", + expected: true, + }, + { + name: "good subpath backticks", + fullPath: "/a/b/../c", + basePath: "/a", + expected: true, + }, + { + name: "good subpath equal", + fullPath: "/a/b/c", + basePath: "/a/b/c", + expected: true, + }, + { + name: "good subpath equal 2", + fullPath: "/a/b/c/", + basePath: "/a/b/c", + expected: true, + }, + { + name: "good subpath root", + fullPath: "/a", + basePath: "/", + expected: true, + }, + { + name: "bad subpath parent", + fullPath: "/a/b/c", + basePath: "/a/b/c/d", + expected: false, + }, + { + name: "bad subpath outside", + fullPath: "/b/c", + basePath: "/a/b/c", + expected: false, + }, + { + name: "bad subpath prefix", + fullPath: "/a/b/cd", + basePath: "/a/b/c", + expected: false, + }, + { + name: "bad subpath backticks", + fullPath: "/a/../b", + basePath: "/a", + expected: false, + }, + { + name: "configmap subpath", + fullPath: "/var/lib/kubelet/pods/uuid/volumes/kubernetes.io~configmap/config/..timestamp/file.txt", + basePath: "/var/lib/kubelet/pods/uuid/volumes/kubernetes.io~configmap/config", + expected: true, + }, + } + for _, test := range tests { + if PathWithinBase(test.fullPath, test.basePath) != test.expected { + t.Errorf("test %q failed: expected %v", test.name, test.expected) + } + + } +} + +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 +20 25 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw +21 25 0:6 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=4058156k,nr_inodes=1014539,mode=755 +22 21 0:14 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +23 25 0:19 / /run rw,nosuid,noexec,relatime shared:5 - tmpfs tmpfs rw,size=815692k,mode=755 +25 0 252:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered +26 19 0:12 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:8 - securityfs securityfs rw +27 21 0:21 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw +28 23 0:22 / /run/lock rw,nosuid,nodev,noexec,relatime shared:6 - tmpfs tmpfs rw,size=5120k +29 19 0:23 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755 +30 29 0:24 / /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 19 0:25 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:11 - pstore pstore rw +32 29 0:26 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,devices +33 29 0:27 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,freezer +34 29 0:28 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,pids +35 29 0:29 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,blkio +36 29 0:30 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,memory +37 29 0:31 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,perf_event +38 29 0:32 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,hugetlb +39 29 0:33 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,cpu,cpuacct +40 29 0:34 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,cpuset +41 29 0:35 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,net_cls,net_prio +58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordere +` + + testcases := []struct { + name string + source string + mountInfos string + expectedRefs []string + expectedErr error + }{ + { + "dir", + "/mnt/disks/vol1", + base, + nil, + nil, + }, + { + "dir-used", + "/mnt/disks/vol1", + base + ` +56 25 252:0 /mnt/disks/vol1 /var/lib/kubelet/pods/1890aef5-5a60-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered +57 25 0:45 / /mnt/disks/vol rw,relatime shared:36 - tmpfs tmpfs rw +`, + []string{"/var/lib/kubelet/pods/1890aef5-5a60-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, + nil, + }, + { + "tmpfs-vol", + "/mnt/disks/vol1", + base + `120 25 0:76 / /mnt/disks/vol1 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k +`, + nil, + nil, + }, + { + "tmpfs-vol-used-by-two-pods", + "/mnt/disks/vol1", + base + `120 25 0:76 / /mnt/disks/vol1 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k +196 25 0:76 / /var/lib/kubelet/pods/ade3ac21-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k +228 25 0:76 / /var/lib/kubelet/pods/ac60532d-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k +`, + []string{ + "/var/lib/kubelet/pods/ade3ac21-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585", + "/var/lib/kubelet/pods/ac60532d-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585", + }, + nil, + }, + { + "tmpfs-subdir-used-indirectly-via-bindmount-dir-by-one-pod", + "/mnt/vol1/foo", + base + `177 25 0:46 / /mnt/data rw,relatime shared:37 - tmpfs data rw +190 25 0:46 /vol1 /mnt/vol1 rw,relatime shared:37 - tmpfs data rw +191 25 0:46 /vol2 /mnt/vol2 rw,relatime shared:37 - tmpfs data rw +62 25 0:46 /vol1/foo /var/lib/kubelet/pods/e25f2f01-5b06-11e8-8694-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:37 - tmpfs data rw +`, + []string{"/var/lib/kubelet/pods/e25f2f01-5b06-11e8-8694-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, + nil, + }, + { + "dir-bindmounted", + "/mnt/disks/vol2", + base + `342 25 252:0 /mnt/disks/vol2 /mnt/disks/vol2 rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered +`, + nil, + nil, + }, + { + "dir-bindmounted-used-by-one-pod", + "/mnt/disks/vol2", + base + `342 25 252:0 /mnt/disks/vol2 /mnt/disks/vol2 rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered +77 25 252:0 /mnt/disks/vol2 /var/lib/kubelet/pods/f30dc360-5a5d-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-1fb30a1c rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered +`, + []string{"/var/lib/kubelet/pods/f30dc360-5a5d-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-1fb30a1c"}, + nil, + }, + { + "blockfs", + "/mnt/disks/blkvol1", + base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +`, + nil, + nil, + }, + { + "blockfs-used-by-one-pod", + "/mnt/disks/blkvol1", + base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +62 25 7:1 / /var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +`, + []string{"/var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, + nil, + }, + { + "blockfs-used-by-two-pods", + "/mnt/disks/blkvol1", + base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +62 25 7:1 / /var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +95 25 7:1 / /var/lib/kubelet/pods/4854a48b-5a64-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered +`, + []string{"/var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test", + "/var/lib/kubelet/pods/4854a48b-5a64-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, + nil, + }, + } + tmpFile, err := ioutil.TempFile("", "test-get-filetype") + if err != nil { + t.Fatal(err) + } + defer os.Remove(tmpFile.Name()) + defer tmpFile.Close() + for _, v := range testcases { + tmpFile.Truncate(0) + tmpFile.Seek(0, 0) + tmpFile.WriteString(v.mountInfos) + tmpFile.Sync() + refs, err := SearchMountPoints(v.source, tmpFile.Name()) + if !reflect.DeepEqual(refs, v.expectedRefs) { + t.Errorf("test %q: expected Refs: %#v, got %#v", v.name, v.expectedRefs, refs) + } + if !reflect.DeepEqual(err, v.expectedErr) { + t.Errorf("test %q: expected err: %v, got %v", v.name, v.expectedErr, err) + } + } +} diff --git a/pkg/util/mount/hostutil/mount_windows_test.go b/pkg/util/mount/hostutil/mount_windows_test.go new file mode 100644 index 00000000000..9af846ddd9b --- /dev/null +++ b/pkg/util/mount/hostutil/mount_windows_test.go @@ -0,0 +1,424 @@ +// +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" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "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)) + } + return nil +} + +func removeLink(link string) error { + if output, err := exec.Command("cmd", "/c", "rmdir", link).CombinedOutput(); err != nil { + return fmt.Errorf("rmdir failed: %v, output: %q", err, string(output)) + } + return nil +} + +func setEquivalent(set1, set2 []string) bool { + map1 := make(map[string]bool) + map2 := make(map[string]bool) + for _, s := range set1 { + map1[s] = true + } + for _, s := range set2 { + map2[s] = true + } + + for s := range map1 { + if !map2[s] { + return false + } + } + for s := range map2 { + if !map1[s] { + return false + } + } + return true +} + +// this func must run in admin mode, otherwise it will fail +func TestGetMountRefs(t *testing.T) { + tests := []struct { + mountPath string + expectedRefs []string + }{ + { + mountPath: `c:\windows`, + expectedRefs: []string{`c:\windows`}, + }, + { + mountPath: `c:\doesnotexist`, + expectedRefs: []string{}, + }, + } + + mounter := Mounter{"fake/path"} + + for _, test := range tests { + if refs, err := mounter.GetMountRefs(test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { + t.Errorf("getMountRefs(%q) = %v, error: %v; expected %v", test.mountPath, refs, err, test.expectedRefs) + } + } +} + +func TestPathWithinBase(t *testing.T) { + tests := []struct { + fullPath string + basePath string + expectedResult bool + }{ + { + fullPath: `c:\tmp\a\b\c`, + basePath: `c:\tmp`, + expectedResult: true, + }, + { + fullPath: `c:\tmp1`, + basePath: `c:\tmp2`, + expectedResult: false, + }, + { + fullPath: `c:\tmp`, + basePath: `c:\tmp`, + expectedResult: true, + }, + { + fullPath: `c:\tmp`, + basePath: `c:\tmp\a\b\c`, + expectedResult: false, + }, + { + fullPath: `c:\kubelet\pods\uuid\volumes\kubernetes.io~configmap\config\..timestamp\file.txt`, + basePath: `c:\kubelet\pods\uuid\volumes\kubernetes.io~configmap\config`, + expectedResult: true, + }, + } + + for _, test := range tests { + result := PathWithinBase(test.fullPath, test.basePath) + assert.Equal(t, result, test.expectedResult, "Expect result not equal with PathWithinBase(%s, %s) return: %q, expected: %q", + test.fullPath, test.basePath, result, test.expectedResult) + } +} + +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"} + + tests := []struct { + fileName string + targetLinkName string + setUp func(base, fileName, targetLinkName string) error + expectedResult bool + expectError bool + }{ + { + "Dir", + "", + func(base, fileName, targetLinkName string) error { + return os.Mkdir(filepath.Join(base, fileName), 0750) + }, + true, + false, + }, + { + "InvalidDir", + "", + func(base, fileName, targetLinkName string) error { + return nil + }, + true, + true, + }, + { + "ValidSymLink", + "targetSymLink", + func(base, fileName, targetLinkName string) error { + targeLinkPath := filepath.Join(base, targetLinkName) + if err := os.Mkdir(targeLinkPath, 0750); err != nil { + return err + } + + filePath := filepath.Join(base, fileName) + if err := makeLink(filePath, targeLinkPath); err != nil { + return err + } + return nil + }, + false, + false, + }, + { + "InvalidSymLink", + "targetSymLink2", + func(base, fileName, targetLinkName string) error { + targeLinkPath := filepath.Join(base, targetLinkName) + if err := os.Mkdir(targeLinkPath, 0750); err != nil { + return err + } + + filePath := filepath.Join(base, fileName) + if err := makeLink(filePath, targeLinkPath); err != nil { + return err + } + return removeLink(targeLinkPath) + }, + true, + false, + }, + } + + for _, test := range tests { + base, err := ioutil.TempDir("", test.fileName) + if err != nil { + t.Fatalf(err.Error()) + } + + defer os.RemoveAll(base) + + if err := test.setUp(base, test.fileName, test.targetLinkName); err != nil { + t.Fatalf("unexpected error in setUp(%s, %s): %v", test.fileName, test.targetLinkName, err) + } + + filePath := filepath.Join(base, test.fileName) + result, err := mounter.IsLikelyNotMountPoint(filePath) + assert.Equal(t, result, test.expectedResult, "Expect result not equal with IsLikelyNotMountPoint(%s) return: %q, expected: %q", + filePath, result, test.expectedResult) + + if test.expectError { + assert.NotNil(t, err, "Expect error during IsLikelyNotMountPoint(%s)", filePath) + } else { + assert.Nil(t, err, "Expect error is nil during IsLikelyNotMountPoint(%s)", filePath) + } + } +} + +func TestFormatAndMount(t *testing.T) { + fakeMounter := ErrorMounter{&FakeMounter{}, 0, nil} + execCallback := func(cmd string, args ...string) ([]byte, error) { + for j := range args { + if strings.Contains(args[j], "Get-Disk -Number") { + return []byte("0"), nil + } + + if strings.Contains(args[j], "Get-Partition -DiskNumber") { + return []byte("0"), nil + } + + if strings.Contains(args[j], "mklink") { + return nil, nil + } + } + return nil, fmt.Errorf("Unexpected cmd %s, args %v", cmd, args) + } + fakeExec := NewFakeExec(execCallback) + + mounter := SafeFormatAndMount{ + Interface: &fakeMounter, + Exec: fakeExec, + } + + tests := []struct { + device string + target string + fstype string + mountOptions []string + expectError bool + }{ + { + "0", + "disk", + "NTFS", + []string{}, + false, + }, + { + "0", + "disk", + "", + []string{}, + false, + }, + { + "invalidDevice", + "disk", + "NTFS", + []string{}, + true, + }, + } + + for _, test := range tests { + base, err := ioutil.TempDir("", test.device) + if err != nil { + t.Fatalf(err.Error()) + } + defer os.RemoveAll(base) + + target := filepath.Join(base, test.target) + err = mounter.FormatAndMount(test.device, target, test.fstype, test.mountOptions) + if test.expectError { + assert.NotNil(t, err, "Expect error during FormatAndMount(%s, %s, %s, %v)", test.device, test.target, test.fstype, test.mountOptions) + } else { + assert.Nil(t, err, "Expect error is nil during FormatAndMount(%s, %s, %s, %v)", test.device, test.target, test.fstype, test.mountOptions) + } + } +} + +func TestNewSMBMapping(t *testing.T) { + tests := []struct { + username string + password string + remotepath string + expectError bool + }{ + { + "", + "password", + `\\remotepath`, + true, + }, + { + "username", + "", + `\\remotepath`, + true, + }, + { + "username", + "password", + "", + true, + }, + } + + for _, test := range tests { + _, err := newSMBMapping(test.username, test.password, test.remotepath) + if test.expectError { + assert.NotNil(t, err, "Expect error during newSMBMapping(%s, %s, %s, %v)", test.username, test.password, test.remotepath) + } else { + assert.Nil(t, err, "Expect error is nil during newSMBMapping(%s, %s, %s, %v)", test.username, test.password, test.remotepath) + } + } +} From e0050ebc9417a3dddc892b55380b011c2b790879 Mon Sep 17 00:00:00 2001 From: Travis Rhoden Date: Thu, 22 Aug 2019 10:42:34 -0600 Subject: [PATCH 4/5] move testfiles back --- pkg/util/mount/{hostutil => }/mount_helper_unix_test.go | 0 pkg/util/mount/{hostutil => }/mount_helper_windows_test.go | 0 pkg/util/mount/{hostutil => }/mount_linux_test.go | 0 pkg/util/mount/{hostutil => }/mount_windows_test.go | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename pkg/util/mount/{hostutil => }/mount_helper_unix_test.go (100%) rename pkg/util/mount/{hostutil => }/mount_helper_windows_test.go (100%) rename pkg/util/mount/{hostutil => }/mount_linux_test.go (100%) rename pkg/util/mount/{hostutil => }/mount_windows_test.go (100%) diff --git a/pkg/util/mount/hostutil/mount_helper_unix_test.go b/pkg/util/mount/mount_helper_unix_test.go similarity index 100% rename from pkg/util/mount/hostutil/mount_helper_unix_test.go rename to pkg/util/mount/mount_helper_unix_test.go diff --git a/pkg/util/mount/hostutil/mount_helper_windows_test.go b/pkg/util/mount/mount_helper_windows_test.go similarity index 100% rename from pkg/util/mount/hostutil/mount_helper_windows_test.go rename to pkg/util/mount/mount_helper_windows_test.go diff --git a/pkg/util/mount/hostutil/mount_linux_test.go b/pkg/util/mount/mount_linux_test.go similarity index 100% rename from pkg/util/mount/hostutil/mount_linux_test.go rename to pkg/util/mount/mount_linux_test.go diff --git a/pkg/util/mount/hostutil/mount_windows_test.go b/pkg/util/mount/mount_windows_test.go similarity index 100% rename from pkg/util/mount/hostutil/mount_windows_test.go rename to pkg/util/mount/mount_windows_test.go From b94ee6bcb1e96a618fe277db83cacf570b855991 Mon Sep 17 00:00:00 2001 From: Travis Rhoden Date: Thu, 22 Aug 2019 10:52:39 -0600 Subject: [PATCH 5/5] Split HostUtil functionality into its own files This patch takes all the HostUtil functionality currently found in mount*.go files and copies it into hostutil*.go files. Care was taken to preserve git history to the fullest extent. As part of doing this, some common functionality was moved into mount_helper files in preperation for HostUtils to stay in k/k and Mount to move out. THe tests for each relevant function were moved to test files to match the appropriate location. --- pkg/util/mount/BUILD | 36 + pkg/util/mount/fake.go | 94 --- pkg/util/mount/fake_hostutil.go | 164 ----- pkg/util/mount/hostutil.go | 259 ------- pkg/util/mount/hostutil_linux.go | 546 --------------- pkg/util/mount/hostutil_linux_test.go | 605 ----------------- pkg/util/mount/hostutil_unsupported.go | 59 -- pkg/util/mount/hostutil_windows.go | 283 +------- pkg/util/mount/hostutil_windows_test.go | 350 ---------- pkg/util/mount/mount.go | 88 --- pkg/util/mount/mount_helper_unix.go | 89 +++ pkg/util/mount/mount_helper_unix_test.go | 707 +------------------- pkg/util/mount/mount_helper_windows.go | 25 + pkg/util/mount/mount_helper_windows_test.go | 365 +--------- pkg/util/mount/mount_linux.go | 346 ---------- pkg/util/mount/mount_linux_test.go | 476 ------------- pkg/util/mount/mount_unsupported.go | 63 -- pkg/util/mount/mount_windows.go | 150 +---- pkg/util/mount/mount_windows_test.go | 91 --- 19 files changed, 162 insertions(+), 4634 deletions(-) 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 index 3b85e672198..c85ff70081f 100644 --- a/pkg/util/mount/fake_hostutil.go +++ b/pkg/util/mount/fake_hostutil.go @@ -19,173 +19,9 @@ package mount import ( "errors" "os" - "path/filepath" "sync" - - "k8s.io/klog" ) -// FakeMounter implements mount.Interface for tests. -type FakeMounter struct { - MountPoints []MountPoint - Log []FakeAction - // Error to return for a path when calling IsLikelyNotMountPoint - MountCheckErrors map[string]error - // Some tests run things in parallel, make sure the mounter does not produce - // any golang's DATA RACE warnings. - mutex sync.Mutex -} - -var _ Interface = &FakeMounter{} - -const ( - // FakeActionMount is the string for specifying mount as FakeAction.Action - FakeActionMount = "mount" - // FakeActionUnmount is the string for specifying unmount as FakeAction.Action - FakeActionUnmount = "unmount" -) - -// FakeAction objects are logged every time a fake mount or unmount is called. -type FakeAction struct { - Action string // "mount" or "unmount" - Target string // applies to both mount and unmount actions - Source string // applies only to "mount" actions - FSType string // applies only to "mount" actions -} - -// ResetLog clears all the log entries in FakeMounter -func (f *FakeMounter) ResetLog() { - f.mutex.Lock() - defer f.mutex.Unlock() - - f.Log = []FakeAction{} -} - -// Mount records the mount event and updates the in-memory mount points for FakeMounter -func (f *FakeMounter) Mount(source string, target string, fstype string, options []string) error { - f.mutex.Lock() - defer f.mutex.Unlock() - - opts := []string{} - - for _, option := range options { - // find 'bind' option - if option == "bind" { - // This is a bind-mount. In order to mimic linux behaviour, we must - // use the original device of the bind-mount as the real source. - // E.g. when mounted /dev/sda like this: - // $ mount /dev/sda /mnt/test - // $ mount -o bind /mnt/test /mnt/bound - // then /proc/mount contains: - // /dev/sda /mnt/test - // /dev/sda /mnt/bound - // (and not /mnt/test /mnt/bound) - // I.e. we must use /dev/sda as source instead of /mnt/test in the - // bind mount. - for _, mnt := range f.MountPoints { - if source == mnt.Path { - source = mnt.Device - break - } - } - } - // reuse MountPoint.Opts field to mark mount as readonly - opts = append(opts, option) - } - - // If target is a symlink, get its absolute path - absTarget, err := filepath.EvalSymlinks(target) - if err != nil { - absTarget = target - } - f.MountPoints = append(f.MountPoints, MountPoint{Device: source, Path: absTarget, Type: fstype, Opts: opts}) - klog.V(5).Infof("Fake mounter: mounted %s to %s", source, absTarget) - f.Log = append(f.Log, FakeAction{Action: FakeActionMount, Target: absTarget, Source: source, FSType: fstype}) - return nil -} - -// Unmount records the unmount event and updates the in-memory mount points for FakeMounter -func (f *FakeMounter) Unmount(target string) error { - f.mutex.Lock() - defer f.mutex.Unlock() - - // If target is a symlink, get its absolute path - absTarget, err := filepath.EvalSymlinks(target) - if err != nil { - absTarget = target - } - - newMountpoints := []MountPoint{} - for _, mp := range f.MountPoints { - if mp.Path == absTarget { - klog.V(5).Infof("Fake mounter: unmounted %s from %s", mp.Device, absTarget) - // Don't copy it to newMountpoints - continue - } - newMountpoints = append(newMountpoints, MountPoint{Device: mp.Device, Path: mp.Path, Type: mp.Type}) - } - f.MountPoints = newMountpoints - f.Log = append(f.Log, FakeAction{Action: FakeActionUnmount, Target: absTarget}) - delete(f.MountCheckErrors, target) - return nil -} - -// List returns all the in-memory mountpoints for FakeMounter -func (f *FakeMounter) List() ([]MountPoint, error) { - f.mutex.Lock() - defer f.mutex.Unlock() - - return f.MountPoints, nil -} - -// IsMountPointMatch tests if dir and mp are the same path -func (f *FakeMounter) IsMountPointMatch(mp MountPoint, dir string) bool { - return mp.Path == dir -} - -// IsLikelyNotMountPoint determines whether a path is a mountpoint by checking -// if the absolute path to file is in the in-memory mountpoints -func (f *FakeMounter) IsLikelyNotMountPoint(file string) (bool, error) { - f.mutex.Lock() - defer f.mutex.Unlock() - - err := f.MountCheckErrors[file] - if err != nil { - return false, err - } - - _, err = os.Stat(file) - if err != nil { - return true, err - } - - // If file is a symlink, get its absolute path - absFile, err := filepath.EvalSymlinks(file) - if err != nil { - absFile = file - } - - for _, mp := range f.MountPoints { - if mp.Path == absFile { - klog.V(5).Infof("isLikelyNotMountPoint for %s: mounted %s, false", file, mp.Path) - return false, nil - } - } - klog.V(5).Infof("isLikelyNotMountPoint for %s: true", file) - return true, nil -} - -// GetMountRefs finds all mount references to the path, returns a -// list of paths. -func (f *FakeMounter) GetMountRefs(pathname string) ([]string, error) { - realpath, err := filepath.EvalSymlinks(pathname) - if err != nil { - // Ignore error in FakeMounter, because we actually didn't create files. - realpath = pathname - } - return getMountRefsByDev(f, realpath) -} - // FakeHostUtil is a fake mount.HostUtils implementation for testing type FakeHostUtil struct { MountPoints []MountPoint diff --git a/pkg/util/mount/hostutil.go b/pkg/util/mount/hostutil.go index 5f0fe1867f4..25d8c17ce2a 100644 --- a/pkg/util/mount/hostutil.go +++ b/pkg/util/mount/hostutil.go @@ -22,17 +22,12 @@ 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. @@ -47,31 +42,6 @@ const ( FileTypeUnknown FileType = "" ) -// Interface defines the set of methods to allow for mount operations on a system. -type Interface interface { - // Mount mounts source to target as fstype with given options. - Mount(source string, target string, fstype string, options []string) error - // Unmount unmounts given target. - Unmount(target string) error - // List returns a list of all mounted filesystems. This can be large. - // On some platforms, reading mounts directly from the OS is not guaranteed - // consistent (i.e. it could change between chunked reads). This is guaranteed - // to be consistent. - List() ([]MountPoint, error) - // IsMountPointMatch determines if the mountpoint matches the dir. - IsMountPointMatch(mp MountPoint, dir string) bool - // IsLikelyNotMountPoint uses heuristics to determine if a directory - // is not a mountpoint. - // It should return ErrNotExist when the directory does not exist. - // IsLikelyNotMountPoint does NOT properly detect all mountpoint types - // most notably linux bind mounts and symbolic link. - IsLikelyNotMountPoint(file string) (bool, error) - // GetMountRefs finds all mount references to the path, returns a - // list of paths. Path could be a mountpoint path, device or a normal - // directory (for bind mount). - 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 @@ -105,239 +75,10 @@ type HostUtils interface { 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 - // stream. - Run(cmd string, args ...string) ([]byte, error) -} - -// Compile-time check to ensure all Mounter implementations satisfy -// 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 - Path string - Type string - Opts []string - Freq int - Pass int -} - -// SafeFormatAndMount probes a device to see if it is formatted. -// Namely it checks to see if a file system is present. If so it -// mounts it otherwise the device is formatted first then mounted. -type SafeFormatAndMount struct { - Interface - Exec -} - -// FormatAndMount formats the given disk, if needed, and mounts it. -// That is if the disk is not formatted and it is not being mounted as -// read-only it will format it first then mount it. Otherwise, if the -// disk is already formatted or it is being mounted as read-only, it -// will be mounted without formatting. -func (mounter *SafeFormatAndMount) FormatAndMount(source string, target string, fstype string, options []string) error { - return mounter.formatAndMount(source, target, fstype, options) -} - -// getMountRefsByDev finds all references to the device provided -// by mountPath; returns a list of paths. -// Note that mountPath should be path after the evaluation of any symblolic links. -func getMountRefsByDev(mounter Interface, mountPath string) ([]string, error) { - mps, err := mounter.List() - if err != nil { - return nil, err - } - - // Finding the device mounted to mountPath. - diskDev := "" - for i := range mps { - if mountPath == mps[i].Path { - diskDev = mps[i].Device - break - } - } - - // Find all references to the device. - var refs []string - for i := range mps { - if mps[i].Device == diskDev || mps[i].Device == mountPath { - if mps[i].Path != mountPath { - refs = append(refs, mps[i].Path) - } - } - } - return refs, nil -} - -// GetDeviceNameFromMount given a mnt point, find the device from /proc/mounts -// returns the device name, reference count, and error code. -func GetDeviceNameFromMount(mounter Interface, mountPath string) (string, int, error) { - mps, err := mounter.List() - if err != nil { - return "", 0, err - } - - // Find the device name. - // FIXME if multiple devices mounted on the same mount path, only the first one is returned. - device := "" - // If mountPath is symlink, need get its target path. - slTarget, err := filepath.EvalSymlinks(mountPath) - if err != nil { - slTarget = mountPath - } - for i := range mps { - if mps[i].Path == slTarget { - device = mps[i].Device - break - } - } - - // Find all references to the device. - refCount := 0 - for i := range mps { - if mps[i].Device == device { - refCount++ - } - } - return device, refCount, nil -} - -// IsNotMountPoint determines if a directory is a mountpoint. -// It should return ErrNotExist when the directory does not exist. -// IsNotMountPoint is more expensive than IsLikelyNotMountPoint. -// IsNotMountPoint detects bind mounts in linux. -// IsNotMountPoint enumerates all the mountpoints using List() and -// the list of mountpoints may be large, then it uses -// IsMountPointMatch to evaluate whether the directory is a mountpoint. -func IsNotMountPoint(mounter Interface, file string) (bool, error) { - // IsLikelyNotMountPoint provides a quick check - // to determine whether file IS A mountpoint. - notMnt, notMntErr := mounter.IsLikelyNotMountPoint(file) - if notMntErr != nil && os.IsPermission(notMntErr) { - // We were not allowed to do the simple stat() check, e.g. on NFS with - // root_squash. Fall back to /proc/mounts check below. - notMnt = true - notMntErr = nil - } - if notMntErr != nil { - return notMnt, notMntErr - } - // identified as mountpoint, so return this fact. - if notMnt == false { - return notMnt, nil - } - - // Resolve any symlinks in file, kernel would do the same and use the resolved path in /proc/mounts. - hu := NewHostUtil() - resolvedFile, err := hu.EvalHostSymlinks(file) - if err != nil { - return true, err - } - - // check all mountpoints since IsLikelyNotMountPoint - // is not reliable for some mountpoint types. - mountPoints, mountPointsErr := mounter.List() - if mountPointsErr != nil { - return notMnt, mountPointsErr - } - for _, mp := range mountPoints { - if mounter.IsMountPointMatch(mp, resolvedFile) { - notMnt = false - break - } - } - return notMnt, nil -} - -// IsBind detects whether a bind mount is being requested and makes the remount options to -// use in case of bind mount, due to the fact that bind mount doesn't respect mount options. -// The list equals: -// options - 'bind' + 'remount' (no duplicate) -func IsBind(options []string) (bool, []string, []string) { - // Because we have an FD opened on the subpath bind mount, the "bind" option - // needs to be included, otherwise the mount target will error as busy if you - // remount as readonly. - // - // As a consequence, all read only bind mounts will no longer change the underlying - // volume mount to be read only. - bindRemountOpts := []string{"bind", "remount"} - bind := false - bindOpts := []string{"bind"} - - // _netdev is a userspace mount option and does not automatically get added when - // bind mount is created and hence we must carry it over. - if checkForNetDev(options) { - bindOpts = append(bindOpts, "_netdev") - } - - for _, option := range options { - switch option { - case "bind": - bind = true - break - case "remount": - break - default: - bindRemountOpts = append(bindRemountOpts, option) - } - } - - return bind, bindOpts, bindRemountOpts -} - -func checkForNetDev(options []string) bool { - for _, option := range options { - if option == "_netdev" { - return true - } - } - return false -} - -// HasMountRefs checks if the given mountPath has mountRefs. -// TODO: this is a workaround for the unmount device issue caused by gci mounter. -// In GCI cluster, if gci mounter is used for mounting, the container started by mounter -// script will cause additional mounts created in the container. Since these mounts are -// irrelevant to the original mounts, they should be not considered when checking the -// mount references. Current solution is to filter out those mount paths that contain -// the string of original mount path. -// Plan to work on better approach to solve this issue. -func HasMountRefs(mountPath string, mountRefs []string) bool { - for _, ref := range mountRefs { - if !strings.Contains(ref, mountPath) { - return true - } - } - return false -} - -// PathWithinBase checks if give path is within given base directory. -func PathWithinBase(fullPath, basePath string) bool { - rel, err := filepath.Rel(basePath, fullPath) - if err != nil { - return false - } - if StartsWithBackstep(rel) { - // Needed to escape the base path. - return false - } - return true -} - -// StartsWithBackstep checks if the given path starts with a backstep segment. -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 diff --git a/pkg/util/mount/hostutil_linux.go b/pkg/util/mount/hostutil_linux.go index 1d5e71961ed..e995a5c8715 100644 --- a/pkg/util/mount/hostutil_linux.go +++ b/pkg/util/mount/hostutil_linux.go @@ -19,439 +19,18 @@ limitations under the License. package mount import ( - "errors" "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 - procMountInfoPath = "/proc/self/mountinfo" - // 'fsck' found errors and corrected them - fsckErrorsCorrected = 1 - // 'fsck' found errors but exited without correcting them - fsckErrorsUncorrected = 4 -) - -// Mounter provides the default implementation of mount.Interface -// for the linux platform. This implementation assumes that the -// kubelet is running in the host's root mount namespace. -type Mounter struct { - mounterPath string - withSystemd bool -} - -// New returns a mount.Interface for the current system. -// It provides options to override the default mounter behavior. -// mounterPath allows using an alternative to `/bin/mount` for mounting. -func New(mounterPath string) Interface { - return &Mounter{ - mounterPath: mounterPath, - withSystemd: detectSystemd(), - } -} - -// Mount mounts source to target as fstype with given options. 'source' and 'fstype' must -// be an empty string in case it's not required, e.g. for remount, or for auto filesystem -// type, where kernel handles fstype for you. The mount 'options' is a list of options, -// currently come from mount(8), e.g. "ro", "remount", "bind", etc. If no more option is -// required, call Mount with an empty string list or nil. -func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { - // Path to mounter binary if containerized mounter is needed. Otherwise, it is set to empty. - // All Linux distros are expected to be shipped with a mount utility that a support bind mounts. - mounterPath := "" - bind, bindOpts, bindRemountOpts := IsBind(options) - if bind { - err := mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindOpts) - if err != nil { - return err - } - return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, bindRemountOpts) - } - // The list of filesystems that require containerized mounter on GCI image cluster - fsTypesNeedMounter := map[string]struct{}{ - "nfs": {}, - "glusterfs": {}, - "ceph": {}, - "cifs": {}, - } - if _, ok := fsTypesNeedMounter[fstype]; ok { - mounterPath = mounter.mounterPath - } - return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options) -} - -// doMount runs the mount command. mounterPath is the path to mounter binary if containerized mounter is used. -func (mounter *Mounter) doMount(mounterPath string, mountCmd string, source string, target string, fstype string, options []string) error { - mountArgs := MakeMountArgs(source, target, fstype, options) - if len(mounterPath) > 0 { - mountArgs = append([]string{mountCmd}, mountArgs...) - mountCmd = mounterPath - } - - if mounter.withSystemd { - // Try to run mount via systemd-run --scope. This will escape the - // service where kubelet runs and any fuse daemons will be started in a - // specific scope. kubelet service than can be restarted without killing - // these fuse daemons. - // - // Complete command line (when mounterPath is not used): - // systemd-run --description=... --scope -- mount -t - // - // Expected flow: - // * systemd-run creates a transient scope (=~ cgroup) and executes its - // argument (/bin/mount) there. - // * mount does its job, forks a fuse daemon if necessary and finishes. - // (systemd-run --scope finishes at this point, returning mount's exit - // code and stdout/stderr - thats one of --scope benefits). - // * systemd keeps the fuse daemon running in the scope (i.e. in its own - // cgroup) until the fuse daemon dies (another --scope benefit). - // Kubelet service can be restarted and the fuse daemon survives. - // * When the fuse daemon dies (e.g. during unmount) systemd removes the - // scope automatically. - // - // systemd-mount is not used because it's too new for older distros - // (CentOS 7, Debian Jessie). - mountCmd, mountArgs = AddSystemdScope("systemd-run", target, mountCmd, mountArgs) - } else { - // No systemd-run on the host (or we failed to check it), assume kubelet - // does not run as a systemd service. - // No code here, mountCmd and mountArgs are already populated. - } - - klog.V(4).Infof("Mounting cmd (%s) with arguments (%s)", mountCmd, mountArgs) - command := exec.Command(mountCmd, mountArgs...) - output, err := command.CombinedOutput() - if err != nil { - args := strings.Join(mountArgs, " ") - klog.Errorf("Mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s\n", err, mountCmd, args, string(output)) - return fmt.Errorf("mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s", - err, mountCmd, args, string(output)) - } - return err -} - -// detectSystemd returns true if OS runs with systemd as init. When not sure -// (permission errors, ...), it returns false. -// There may be different ways how to detect systemd, this one makes sure that -// systemd-runs (needed by Mount()) works. -func detectSystemd() bool { - if _, err := exec.LookPath("systemd-run"); err != nil { - klog.V(2).Infof("Detected OS without systemd") - return false - } - // Try to run systemd-run --scope /bin/true, that should be enough - // to make sure that systemd is really running and not just installed, - // which happens when running in a container with a systemd-based image - // but with different pid 1. - cmd := exec.Command("systemd-run", "--description=Kubernetes systemd probe", "--scope", "true") - output, err := cmd.CombinedOutput() - if err != nil { - klog.V(2).Infof("Cannot run systemd-run, assuming non-systemd OS") - klog.V(4).Infof("systemd-run failed with: %v", err) - klog.V(4).Infof("systemd-run output: %s", string(output)) - return false - } - klog.V(2).Infof("Detected OS with systemd") - return true -} - -// MakeMountArgs makes the arguments to the mount(8) command. -// Implementation is shared with NsEnterMounter -func MakeMountArgs(source, target, fstype string, options []string) []string { - // Build mount command as follows: - // mount [-t $fstype] [-o $options] [$source] $target - mountArgs := []string{} - if len(fstype) > 0 { - mountArgs = append(mountArgs, "-t", fstype) - } - if len(options) > 0 { - mountArgs = append(mountArgs, "-o", strings.Join(options, ",")) - } - if len(source) > 0 { - mountArgs = append(mountArgs, source) - } - mountArgs = append(mountArgs, target) - - return mountArgs -} - -// AddSystemdScope adds "system-run --scope" to given command line -// implementation is shared with NsEnterMounter -func AddSystemdScope(systemdRunPath, mountName, command string, args []string) (string, []string) { - descriptionArg := fmt.Sprintf("--description=Kubernetes transient mount for %s", mountName) - systemdRunArgs := []string{descriptionArg, "--scope", "--", command} - return systemdRunPath, append(systemdRunArgs, args...) -} - -// Unmount unmounts the target. -func (mounter *Mounter) Unmount(target string) error { - klog.V(4).Infof("Unmounting %s", target) - command := exec.Command("umount", target) - output, err := command.CombinedOutput() - if err != nil { - return fmt.Errorf("unmount failed: %v\nUnmounting arguments: %s\nOutput: %s", err, target, string(output)) - } - return nil -} - -// List returns a list of all mounted filesystems. -func (*Mounter) List() ([]MountPoint, error) { - return ListProcMounts(procMountsPath) -} - -// IsMountPointMatch returns true if the path in mp is the same as dir -func (mounter *Mounter) IsMountPointMatch(mp MountPoint, dir string) bool { - deletedDir := fmt.Sprintf("%s\\040(deleted)", dir) - return ((mp.Path == dir) || (mp.Path == deletedDir)) -} - -// IsLikelyNotMountPoint determines if a directory is not a mountpoint. -// It is fast but not necessarily ALWAYS correct. If the path is in fact -// a bind mount from one part of a mount to another it will not be detected. -// It also can not distinguish between mountpoints and symbolic links. -// mkdir /tmp/a /tmp/b; mount --bind /tmp/a /tmp/b; IsLikelyNotMountPoint("/tmp/b") -// will return true. When in fact /tmp/b is a mount point. If this situation -// is of interest to you, don't use this function... -func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { - stat, err := os.Stat(file) - if err != nil { - return true, err - } - rootStat, err := os.Stat(filepath.Dir(strings.TrimSuffix(file, "/"))) - if err != nil { - return true, err - } - // If the directory has a different device as parent, then it is a mountpoint. - if stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev { - return false, nil - } - - return true, nil -} - -// GetMountRefs finds all mount references to pathname, returns a -// list of paths. Path could be a mountpoint path, device or a normal -// directory (for bind mount). -func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { - pathExists, pathErr := PathExists(pathname) - if !pathExists { - return []string{}, nil - } else if IsCorruptedMnt(pathErr) { - klog.Warningf("GetMountRefs found corrupted mount at %s, treating as unmounted path", pathname) - return []string{}, nil - } else if pathErr != nil { - return nil, fmt.Errorf("error checking path %s: %v", pathname, pathErr) - } - realpath, err := filepath.EvalSymlinks(pathname) - if err != nil { - return nil, err - } - return SearchMountPoints(realpath, procMountInfoPath) -} - -// formatAndMount uses unix utils to format and mount the given disk -func (mounter *SafeFormatAndMount) formatAndMount(source string, target string, fstype string, options []string) error { - readOnly := false - for _, option := range options { - if option == "ro" { - readOnly = true - break - } - } - - options = append(options, "defaults") - - if !readOnly { - // Run fsck on the disk to fix repairable issues, only do this for volumes requested as rw. - klog.V(4).Infof("Checking for issues with fsck on disk: %s", source) - args := []string{"-a", source} - out, err := mounter.Exec.Run("fsck", args...) - if err != nil { - ee, isExitError := err.(utilexec.ExitError) - switch { - case err == utilexec.ErrExecutableNotFound: - klog.Warningf("'fsck' not found on system; continuing mount without running 'fsck'.") - case isExitError && ee.ExitStatus() == fsckErrorsCorrected: - klog.Infof("Device %s has errors which were corrected by fsck.", source) - case isExitError && ee.ExitStatus() == fsckErrorsUncorrected: - return fmt.Errorf("'fsck' found errors on device %s but could not correct them: %s", source, string(out)) - case isExitError && ee.ExitStatus() > fsckErrorsUncorrected: - klog.Infof("`fsck` error %s", string(out)) - } - } - } - - // Try to mount the disk - klog.V(4).Infof("Attempting to mount disk: %s %s %s", fstype, source, target) - mountErr := mounter.Interface.Mount(source, target, fstype, options) - if mountErr != nil { - // Mount failed. This indicates either that the disk is unformatted or - // it contains an unexpected filesystem. - existingFormat, err := mounter.GetDiskFormat(source) - if err != nil { - return err - } - if existingFormat == "" { - if readOnly { - // Don't attempt to format if mounting as readonly, return an error to reflect this. - return errors.New("failed to mount unformatted volume as read only") - } - - // Disk is unformatted so format it. - args := []string{source} - // Use 'ext4' as the default - if len(fstype) == 0 { - fstype = "ext4" - } - - if fstype == "ext4" || fstype == "ext3" { - args = []string{ - "-F", // Force flag - "-m0", // Zero blocks reserved for super-user - source, - } - } - klog.Infof("Disk %q appears to be unformatted, attempting to format as type: %q with options: %v", source, fstype, args) - _, err := mounter.Exec.Run("mkfs."+fstype, args...) - if err == nil { - // the disk has been formatted successfully try to mount it again. - klog.Infof("Disk successfully formatted (mkfs): %s - %s %s", fstype, source, target) - return mounter.Interface.Mount(source, target, fstype, options) - } - klog.Errorf("format of disk %q failed: type:(%q) target:(%q) options:(%q)error:(%v)", source, fstype, target, options, err) - return err - } - // Disk is already formatted and failed to mount - if len(fstype) == 0 || fstype == existingFormat { - // This is mount error - return mountErr - } - // Block device is formatted with unexpected filesystem, let the user know - return fmt.Errorf("failed to mount the volume as %q, it already contains %s. Mount error: %v", fstype, existingFormat, mountErr) - } - return mountErr -} - -// GetDiskFormat uses 'blkid' to see if the given disk is unformatted -func (mounter *SafeFormatAndMount) GetDiskFormat(disk string) (string, error) { - args := []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", disk} - klog.V(4).Infof("Attempting to determine if disk %q is formatted using blkid with args: (%v)", disk, args) - dataOut, err := mounter.Exec.Run("blkid", args...) - output := string(dataOut) - klog.V(4).Infof("Output: %q, err: %v", output, err) - - if err != nil { - if exit, ok := err.(utilexec.ExitError); ok { - if exit.ExitStatus() == 2 { - // Disk device is unformatted. - // For `blkid`, if the specified token (TYPE/PTTYPE, etc) was - // not found, or no (specified) devices could be identified, an - // exit code of 2 is returned. - return "", nil - } - } - klog.Errorf("Could not determine if disk %q is formatted (%v)", disk, err) - return "", err - } - - var fstype, pttype string - - lines := strings.Split(output, "\n") - for _, l := range lines { - if len(l) <= 0 { - // Ignore empty line. - continue - } - cs := strings.Split(l, "=") - if len(cs) != 2 { - return "", fmt.Errorf("blkid returns invalid output: %s", output) - } - // TYPE is filesystem type, and PTTYPE is partition table type, according - // to https://www.kernel.org/pub/linux/utils/util-linux/v2.21/libblkid-docs/. - if cs[0] == "TYPE" { - fstype = cs[1] - } else if cs[0] == "PTTYPE" { - pttype = cs[1] - } - } - - if len(pttype) > 0 { - klog.V(4).Infof("Disk %s detected partition table type: %s", disk, pttype) - // Returns a special non-empty string as filesystem type, then kubelet - // will not format it. - return "unknown data, probably partitions", nil - } - - return fstype, nil -} - -// ListProcMounts is shared with NsEnterMounter -func ListProcMounts(mountFilePath string) ([]MountPoint, error) { - content, err := utilio.ConsistentRead(mountFilePath, maxListTries) - if err != nil { - return nil, err - } - return parseProcMounts(content) -} - -func parseProcMounts(content []byte) ([]MountPoint, error) { - out := []MountPoint{} - lines := strings.Split(string(content), "\n") - for _, line := range lines { - if line == "" { - // the last split() item is empty string following the last \n - continue - } - fields := strings.Fields(line) - if len(fields) != expectedNumFieldsPerLine { - return nil, fmt.Errorf("wrong number of fields (expected %d, got %d): %s", expectedNumFieldsPerLine, len(fields), line) - } - - mp := MountPoint{ - Device: fields[0], - Path: fields[1], - Type: fields[2], - Opts: strings.Split(fields[3], ","), - } - - freq, err := strconv.Atoi(fields[4]) - if err != nil { - return nil, err - } - mp.Freq = freq - - pass, err := strconv.Atoi(fields[5]) - if err != nil { - return nil, err - } - mp.Pass = pass - - out = append(out, mp) - } - return out, nil -} - type hostUtil struct { } @@ -607,83 +186,6 @@ func isShared(mount string, mountInfoPath string) (bool, error) { 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 { @@ -790,51 +292,3 @@ func GetModeLinux(pathname string) (os.FileMode, error) { } return info.Mode(), nil } - -// SearchMountPoints finds all mount references to the source, returns a list of -// mountpoints. -// This function assumes source cannot be device. -// Some filesystems may share a source name, e.g. tmpfs. And for bind mounting, -// it's possible to mount a non-root path of a filesystem, so we need to use -// root path and major:minor to represent mount source uniquely. -// This implementation is shared between Linux and NsEnterMounter -func SearchMountPoints(hostSource, mountInfoPath string) ([]string, error) { - mis, err := parseMountInfo(mountInfoPath) - if err != nil { - return nil, err - } - - mountID := 0 - rootPath := "" - majorMinor := "" - - // Finding the underlying root path and major:minor if possible. - // We need search in backward order because it's possible for later mounts - // to overlap earlier mounts. - for i := len(mis) - 1; i >= 0; i-- { - if hostSource == mis[i].mountPoint || PathWithinBase(hostSource, mis[i].mountPoint) { - // If it's a mount point or path under a mount point. - mountID = mis[i].id - rootPath = filepath.Join(mis[i].root, strings.TrimPrefix(hostSource, mis[i].mountPoint)) - majorMinor = mis[i].majorMinor - break - } - } - - if rootPath == "" || majorMinor == "" { - return nil, fmt.Errorf("failed to get root path and major:minor for %s", hostSource) - } - - var refs []string - for i := range mis { - if mis[i].id == mountID { - // Ignore mount entry for mount source itself. - continue - } - if mis[i].root == rootPath && mis[i].majorMinor == majorMinor { - refs = append(refs, mis[i].mountPoint) - } - } - - return refs, nil -} diff --git a/pkg/util/mount/hostutil_linux_test.go b/pkg/util/mount/hostutil_linux_test.go index e8ff8251d8f..aec45afeb84 100644 --- a/pkg/util/mount/hostutil_linux_test.go +++ b/pkg/util/mount/hostutil_linux_test.go @@ -24,205 +24,12 @@ import ( "net" "os" "path/filepath" - "reflect" "strings" "testing" "k8s.io/utils/exec" ) -func TestReadProcMountsFrom(t *testing.T) { - successCase := - `/dev/0 /path/to/0 type0 flags 0 0 -/dev/1 /path/to/1 type1 flags 1 1 -/dev/2 /path/to/2 type2 flags,1,2=3 2 2 -` - // NOTE: readProcMountsFrom has been updated to using fnv.New32a() - mounts, err := parseProcMounts([]byte(successCase)) - if err != nil { - t.Errorf("expected success, got %v", err) - } - if len(mounts) != 3 { - t.Fatalf("expected 3 mounts, got %d", len(mounts)) - } - mp := MountPoint{"/dev/0", "/path/to/0", "type0", []string{"flags"}, 0, 0} - if !mountPointsEqual(&mounts[0], &mp) { - t.Errorf("got unexpected MountPoint[0]: %#v", mounts[0]) - } - mp = MountPoint{"/dev/1", "/path/to/1", "type1", []string{"flags"}, 1, 1} - if !mountPointsEqual(&mounts[1], &mp) { - t.Errorf("got unexpected MountPoint[1]: %#v", mounts[1]) - } - mp = MountPoint{"/dev/2", "/path/to/2", "type2", []string{"flags", "1", "2=3"}, 2, 2} - if !mountPointsEqual(&mounts[2], &mp) { - t.Errorf("got unexpected MountPoint[2]: %#v", mounts[2]) - } - - errorCases := []string{ - "/dev/0 /path/to/mount\n", - "/dev/1 /path/to/mount type flags a 0\n", - "/dev/2 /path/to/mount type flags 0 b\n", - } - for _, ec := range errorCases { - _, err := parseProcMounts([]byte(ec)) - if err == nil { - t.Errorf("expected error") - } - } -} - -func mountPointsEqual(a, b *MountPoint) bool { - if a.Device != b.Device || a.Path != b.Path || a.Type != b.Type || !reflect.DeepEqual(a.Opts, b.Opts) || a.Pass != b.Pass || a.Freq != b.Freq { - return false - } - return true -} - -func TestGetMountRefs(t *testing.T) { - fm := &FakeMounter{ - MountPoints: []MountPoint{ - {Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"}, - {Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2"}, - }, - } - - tests := []struct { - mountPath string - expectedRefs []string - }{ - { - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod", - []string{ - "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd", - }, - }, - { - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1", - []string{ - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2", - "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2", - }, - }, - { - "/var/fake/directory/that/doesnt/exist", - []string{}, - }, - } - - for i, test := range tests { - if refs, err := fm.GetMountRefs(test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { - t.Errorf("%d. getMountRefs(%q) = %v, %v; expected %v, nil", i, test.mountPath, refs, err, test.expectedRefs) - } - } -} - -func setEquivalent(set1, set2 []string) bool { - map1 := make(map[string]bool) - map2 := make(map[string]bool) - for _, s := range set1 { - map1[s] = true - } - for _, s := range set2 { - map2[s] = true - } - - for s := range map1 { - if !map2[s] { - return false - } - } - for s := range map2 { - if !map1[s] { - return false - } - } - return true -} - -func TestGetDeviceNameFromMount(t *testing.T) { - fm := &FakeMounter{ - MountPoints: []MountPoint{ - {Device: "/dev/disk/by-path/prefix-lun-1", - Path: "/mnt/111"}, - {Device: "/dev/disk/by-path/prefix-lun-1", - Path: "/mnt/222"}, - }, - } - - tests := []struct { - mountPath string - expectedDevice string - expectedRefs int - }{ - { - "/mnt/222", - "/dev/disk/by-path/prefix-lun-1", - 2, - }, - } - - for i, test := range tests { - if device, refs, err := GetDeviceNameFromMount(fm, test.mountPath); err != nil || test.expectedRefs != refs || test.expectedDevice != device { - t.Errorf("%d. GetDeviceNameFromMount(%s) = (%s, %d), %v; expected (%s,%d), nil", i, test.mountPath, device, refs, err, test.expectedDevice, test.expectedRefs) - } - } -} - -func TestGetMountRefsByDev(t *testing.T) { - fm := &FakeMounter{ - MountPoints: []MountPoint{ - {Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"}, - {Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2"}, - }, - } - - tests := []struct { - mountPath string - expectedRefs []string - }{ - { - "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd", - []string{ - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod", - }, - }, - { - "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2", - []string{ - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1", - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2", - }, - }, - } - - for i, test := range tests { - - if refs, err := getMountRefsByDev(fm, test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { - t.Errorf("%d. getMountRefsByDev(%q) = %v, %v; expected %v, nil", i, test.mountPath, refs, err, test.expectedRefs) - } - } -} - -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 @@ -332,268 +139,6 @@ func TestIsSharedFailure(t *testing.T) { } } -func TestPathWithinBase(t *testing.T) { - tests := []struct { - name string - fullPath string - basePath string - expected bool - }{ - { - name: "good subpath", - fullPath: "/a/b/c", - basePath: "/a", - expected: true, - }, - { - name: "good subpath 2", - fullPath: "/a/b/c", - basePath: "/a/b", - expected: true, - }, - { - name: "good subpath end slash", - fullPath: "/a/b/c/", - basePath: "/a/b", - expected: true, - }, - { - name: "good subpath backticks", - fullPath: "/a/b/../c", - basePath: "/a", - expected: true, - }, - { - name: "good subpath equal", - fullPath: "/a/b/c", - basePath: "/a/b/c", - expected: true, - }, - { - name: "good subpath equal 2", - fullPath: "/a/b/c/", - basePath: "/a/b/c", - expected: true, - }, - { - name: "good subpath root", - fullPath: "/a", - basePath: "/", - expected: true, - }, - { - name: "bad subpath parent", - fullPath: "/a/b/c", - basePath: "/a/b/c/d", - expected: false, - }, - { - name: "bad subpath outside", - fullPath: "/b/c", - basePath: "/a/b/c", - expected: false, - }, - { - name: "bad subpath prefix", - fullPath: "/a/b/cd", - basePath: "/a/b/c", - expected: false, - }, - { - name: "bad subpath backticks", - fullPath: "/a/../b", - basePath: "/a", - expected: false, - }, - { - name: "configmap subpath", - fullPath: "/var/lib/kubelet/pods/uuid/volumes/kubernetes.io~configmap/config/..timestamp/file.txt", - basePath: "/var/lib/kubelet/pods/uuid/volumes/kubernetes.io~configmap/config", - expected: true, - }, - } - for _, test := range tests { - if PathWithinBase(test.fullPath, test.basePath) != test.expected { - t.Errorf("test %q failed: expected %v", test.name, test.expected) - } - - } -} - -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 @@ -766,153 +311,3 @@ func isOperationNotPermittedError(err error) bool { } return false } - -func TestSearchMountPoints(t *testing.T) { - base := ` -19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw -20 25 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw -21 25 0:6 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=4058156k,nr_inodes=1014539,mode=755 -22 21 0:14 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 -23 25 0:19 / /run rw,nosuid,noexec,relatime shared:5 - tmpfs tmpfs rw,size=815692k,mode=755 -25 0 252:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered -26 19 0:12 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:8 - securityfs securityfs rw -27 21 0:21 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw -28 23 0:22 / /run/lock rw,nosuid,nodev,noexec,relatime shared:6 - tmpfs tmpfs rw,size=5120k -29 19 0:23 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755 -30 29 0:24 / /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 19 0:25 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:11 - pstore pstore rw -32 29 0:26 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,devices -33 29 0:27 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,freezer -34 29 0:28 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,pids -35 29 0:29 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,blkio -36 29 0:30 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,memory -37 29 0:31 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,perf_event -38 29 0:32 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,hugetlb -39 29 0:33 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,cpu,cpuacct -40 29 0:34 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,cpuset -41 29 0:35 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,net_cls,net_prio -58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordere -` - - testcases := []struct { - name string - source string - mountInfos string - expectedRefs []string - expectedErr error - }{ - { - "dir", - "/mnt/disks/vol1", - base, - nil, - nil, - }, - { - "dir-used", - "/mnt/disks/vol1", - base + ` -56 25 252:0 /mnt/disks/vol1 /var/lib/kubelet/pods/1890aef5-5a60-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered -57 25 0:45 / /mnt/disks/vol rw,relatime shared:36 - tmpfs tmpfs rw -`, - []string{"/var/lib/kubelet/pods/1890aef5-5a60-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, - nil, - }, - { - "tmpfs-vol", - "/mnt/disks/vol1", - base + `120 25 0:76 / /mnt/disks/vol1 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k -`, - nil, - nil, - }, - { - "tmpfs-vol-used-by-two-pods", - "/mnt/disks/vol1", - base + `120 25 0:76 / /mnt/disks/vol1 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k -196 25 0:76 / /var/lib/kubelet/pods/ade3ac21-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k -228 25 0:76 / /var/lib/kubelet/pods/ac60532d-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k -`, - []string{ - "/var/lib/kubelet/pods/ade3ac21-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585", - "/var/lib/kubelet/pods/ac60532d-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585", - }, - nil, - }, - { - "tmpfs-subdir-used-indirectly-via-bindmount-dir-by-one-pod", - "/mnt/vol1/foo", - base + `177 25 0:46 / /mnt/data rw,relatime shared:37 - tmpfs data rw -190 25 0:46 /vol1 /mnt/vol1 rw,relatime shared:37 - tmpfs data rw -191 25 0:46 /vol2 /mnt/vol2 rw,relatime shared:37 - tmpfs data rw -62 25 0:46 /vol1/foo /var/lib/kubelet/pods/e25f2f01-5b06-11e8-8694-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:37 - tmpfs data rw -`, - []string{"/var/lib/kubelet/pods/e25f2f01-5b06-11e8-8694-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, - nil, - }, - { - "dir-bindmounted", - "/mnt/disks/vol2", - base + `342 25 252:0 /mnt/disks/vol2 /mnt/disks/vol2 rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered -`, - nil, - nil, - }, - { - "dir-bindmounted-used-by-one-pod", - "/mnt/disks/vol2", - base + `342 25 252:0 /mnt/disks/vol2 /mnt/disks/vol2 rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered -77 25 252:0 /mnt/disks/vol2 /var/lib/kubelet/pods/f30dc360-5a5d-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-1fb30a1c rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered -`, - []string{"/var/lib/kubelet/pods/f30dc360-5a5d-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-1fb30a1c"}, - nil, - }, - { - "blockfs", - "/mnt/disks/blkvol1", - base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -`, - nil, - nil, - }, - { - "blockfs-used-by-one-pod", - "/mnt/disks/blkvol1", - base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -62 25 7:1 / /var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -`, - []string{"/var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, - nil, - }, - { - "blockfs-used-by-two-pods", - "/mnt/disks/blkvol1", - base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -62 25 7:1 / /var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -95 25 7:1 / /var/lib/kubelet/pods/4854a48b-5a64-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -`, - []string{"/var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test", - "/var/lib/kubelet/pods/4854a48b-5a64-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, - nil, - }, - } - tmpFile, err := ioutil.TempFile("", "test-get-filetype") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpFile.Name()) - defer tmpFile.Close() - for _, v := range testcases { - tmpFile.Truncate(0) - tmpFile.Seek(0, 0) - tmpFile.WriteString(v.mountInfos) - tmpFile.Sync() - refs, err := SearchMountPoints(v.source, tmpFile.Name()) - if !reflect.DeepEqual(refs, v.expectedRefs) { - t.Errorf("test %q: expected Refs: %#v, got %#v", v.name, v.expectedRefs, refs) - } - if !reflect.DeepEqual(err, v.expectedErr) { - t.Errorf("test %q: expected err: %v, got %v", v.name, v.expectedErr, err) - } - } -} diff --git a/pkg/util/mount/hostutil_unsupported.go b/pkg/util/mount/hostutil_unsupported.go index 7cdf75ba5ee..d4b05137156 100644 --- a/pkg/util/mount/hostutil_unsupported.go +++ b/pkg/util/mount/hostutil_unsupported.go @@ -19,68 +19,9 @@ limitations under the License. package mount import ( - "errors" "os" ) -// Mounter implements mount.Interface for unsupported platforms -type Mounter struct { - mounterPath string -} - -var errUnsupported = errors.New("util/mount on this platform is not supported") - -// New returns a mount.Interface for the current system. -// It provides options to override the default mounter behavior. -// mounterPath allows using an alternative to `/bin/mount` for mounting. -func New(mounterPath string) Interface { - return &Mounter{ - mounterPath: mounterPath, - } -} - -// Mount always returns an error on unsupported platforms -func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { - return errUnsupported -} - -// Unmount always returns an error on unsupported platforms -func (mounter *Mounter) Unmount(target string) error { - return errUnsupported -} - -// List always returns an error on unsupported platforms -func (mounter *Mounter) List() ([]MountPoint, error) { - return []MountPoint{}, errUnsupported -} - -// IsMountPointMatch returns true if the path in mp is the same as dir -func (mounter *Mounter) IsMountPointMatch(mp MountPoint, dir string) bool { - return (mp.Path == dir) -} - -// IsLikelyNotMountPoint always returns an error on unsupported platforms -func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { - return true, errUnsupported -} - -// GetMountRefs always returns an error on unsupported platforms -func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { - return nil, errUnsupported -} - -func (mounter *SafeFormatAndMount) formatAndMount(source string, target string, fstype string, options []string) error { - return mounter.Interface.Mount(source, target, fstype, options) -} - -func (mounter *SafeFormatAndMount) diskLooksUnformatted(disk string) (bool, error) { - return true, errUnsupported -} - -func getDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) (string, error) { - return "", errUnsupported -} - type hostUtil struct{} // NewHostUtil returns a struct that implements the HostUtils interface on diff --git a/pkg/util/mount/hostutil_windows.go b/pkg/util/mount/hostutil_windows.go index 0dde2e8b23e..20f340a9cde 100644 --- a/pkg/util/mount/hostutil_windows.go +++ b/pkg/util/mount/hostutil_windows.go @@ -21,294 +21,15 @@ package mount 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 -// for the windows platform. This implementation assumes that the -// kubelet is running in the host's root mount namespace. -type Mounter struct { - mounterPath string -} - -// New returns a mount.Interface for the current system. -// It provides options to override the default mounter behavior. -// mounterPath allows using an alternative to `/bin/mount` for mounting. -func New(mounterPath string) Interface { - return &Mounter{ - mounterPath: mounterPath, - } -} - -// acquire lock for smb mount -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) - - if source == "tmpfs" { - klog.V(3).Infof("mounting source (%q), target (%q), with options (%q)", source, target, options) - return os.MkdirAll(target, 0755) - } - - parentDir := filepath.Dir(target) - if err := os.MkdirAll(parentDir, 0755); err != nil { - return err - } - - klog.V(4).Infof("mount options(%q) source:%q, target:%q, fstype:%q, begin to mount", - options, source, target, fstype) - bindSource := source - - // 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) - } else { - if len(options) < 2 { - klog.Warningf("mount options(%q) command number(%d) less than 2, source:%q, target:%q, skip mounting", - options, len(options), source, target) - return nil - } - - // currently only cifs mount is supported - if strings.ToLower(fstype) != "cifs" { - return fmt.Errorf("only cifs mount is supported now, fstype: %q, mounting source (%q), target (%q), with options (%q)", fstype, source, target, options) - } - - // lock smb mount for the same source - getSMBMountMutex.LockKey(source) - defer getSMBMountMutex.UnlockKey(source) - - if output, err := newSMBMapping(options[0], options[1], source); err != nil { - if isSMBMappingExist(source) { - klog.V(2).Infof("SMB Mapping(%s) already exists, now begin to remove and remount", source) - if output, err := removeSMBMapping(source); err != nil { - return fmt.Errorf("Remove-SmbGlobalMapping failed: %v, output: %q", err, output) - } - if output, err := newSMBMapping(options[0], options[1], source); err != nil { - return fmt.Errorf("New-SmbGlobalMapping remount failed: %v, output: %q", err, output) - } - } else { - return fmt.Errorf("New-SmbGlobalMapping failed: %v, output: %q", err, output) - } - } - } - - if output, err := exec.Command("cmd", "/c", "mklink", "/D", target, bindSource).CombinedOutput(); err != nil { - klog.Errorf("mklink failed: %v, source(%q) target(%q) output: %q", err, bindSource, target, string(output)) - return err - } - - return nil -} - -// do the SMB mount with username, password, remotepath -// return (output, error) -func newSMBMapping(username, password, remotepath string) (string, error) { - if username == "" || password == "" || remotepath == "" { - return "", fmt.Errorf("invalid parameter(username: %s, password: %s, remoteapth: %s)", username, password, remotepath) - } - - // use PowerShell Environment Variables to store user input string to prevent command line injection - // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1 - cmdLine := `$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force` + - `;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord` + - `;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential` - cmd := exec.Command("powershell", "/c", cmdLine) - cmd.Env = append(os.Environ(), - fmt.Sprintf("smbuser=%s", username), - fmt.Sprintf("smbpassword=%s", password), - fmt.Sprintf("smbremotepath=%s", remotepath)) - - output, err := cmd.CombinedOutput() - return string(output), err -} - -// check whether remotepath is already mounted -func isSMBMappingExist(remotepath string) bool { - cmd := exec.Command("powershell", "/c", `Get-SmbGlobalMapping -RemotePath $Env:smbremotepath`) - cmd.Env = append(os.Environ(), fmt.Sprintf("smbremotepath=%s", remotepath)) - _, err := cmd.CombinedOutput() - return err == nil -} - -// remove SMB mapping -func removeSMBMapping(remotepath string) (string, error) { - cmd := exec.Command("powershell", "/c", `Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force`) - cmd.Env = append(os.Environ(), fmt.Sprintf("smbremotepath=%s", remotepath)) - output, err := cmd.CombinedOutput() - return string(output), err -} - -// Unmount unmounts the target. -func (mounter *Mounter) Unmount(target string) error { - klog.V(4).Infof("azureMount: Unmount target (%q)", 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 - } - return nil -} - -// List returns a list of all mounted filesystems. todo -func (mounter *Mounter) List() ([]MountPoint, error) { - return []MountPoint{}, nil -} - -// IsMountPointMatch determines if the mountpoint matches the dir -func (mounter *Mounter) IsMountPointMatch(mp MountPoint, dir string) bool { - return mp.Path == dir -} - -// IsLikelyNotMountPoint determines if a directory is not a mountpoint. -func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { - stat, err := os.Lstat(file) - if err != nil { - return true, err - } - // If current file is a symlink, then it is a mountpoint. - if stat.Mode()&os.ModeSymlink != 0 { - target, err := os.Readlink(file) - if err != nil { - return true, fmt.Errorf("readlink error: %v", err) - } - hu := NewHostUtil() - exists, err := hu.PathExists(target) - if err != nil { - return true, err - } - return !exists, nil - } - - return true, nil -} - -// 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) - pathExists, pathErr := PathExists(windowsPath) - if !pathExists { - return []string{}, nil - } else if IsCorruptedMnt(pathErr) { - klog.Warningf("GetMountRefs found corrupted mount at %s, treating as unmounted path", windowsPath) - return []string{}, nil - } else if pathErr != nil { - return nil, fmt.Errorf("error checking path %s: %v", windowsPath, pathErr) - } - return []string{pathname}, nil -} - -func (mounter *SafeFormatAndMount) formatAndMount(source string, target string, fstype string, options []string) error { - // Try to mount the disk - klog.V(4).Infof("Attempting to formatAndMount disk: %s %s %s", fstype, source, target) - - if err := ValidateDiskNumber(source); err != nil { - klog.Errorf("diskMount: formatAndMount failed, err: %v", err) - return err - } - - if len(fstype) == 0 { - // Use 'NTFS' as the default - fstype = "NTFS" - } - - // format disk if it is unformatted(raw) - cmd := fmt.Sprintf("Get-Disk -Number %s | Where partitionstyle -eq 'raw' | Initialize-Disk -PartitionStyle MBR -PassThru"+ - " | New-Partition -AssignDriveLetter -UseMaximumSize | Format-Volume -FileSystem %s -Confirm:$false", source, fstype) - if output, err := mounter.Exec.Run("powershell", "/c", cmd); err != nil { - return fmt.Errorf("diskMount: format disk failed, error: %v, output: %q", err, string(output)) - } - klog.V(4).Infof("diskMount: Disk successfully formatted, disk: %q, fstype: %q", source, fstype) - - driveLetter, err := getDriveLetterByDiskNumber(source, mounter.Exec) - if err != nil { - return err - } - driverPath := driveLetter + ":" - 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)) - return err - } - 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) - output, err := exec.Run("powershell", "/c", cmd) - if err != nil { - return "", fmt.Errorf("azureMount: Get Drive Letter failed: %v, output: %q", err, string(output)) - } - if len(string(output)) < 1 { - return "", fmt.Errorf("azureMount: Get Drive Letter failed, output is empty") - } - return string(output)[:1], nil -} - -// getAllParentLinks walks all symbolic links and return all the parent targets recursively -func getAllParentLinks(path string) ([]string, error) { - const maxIter = 255 - links := []string{} - for { - links = append(links, path) - if len(links) > maxIter { - return links, fmt.Errorf("unexpected length of parent links: %v", links) - } - - fi, err := os.Lstat(path) - if err != nil { - return links, fmt.Errorf("Lstat: %v", err) - } - if fi.Mode()&os.ModeSymlink == 0 { - break - } - - path, err = os.Readlink(path) - if err != nil { - return links, fmt.Errorf("Readlink error: %v", err) - } - } - - return links, nil -} - type hostUtil struct{} // NewHostUtil returns a struct that implements the HostUtils interface on @@ -334,10 +55,10 @@ func getDeviceNameFromMount(mounter Interface, mountPath, pluginMountDir string) if len(refs) == 0 { return "", fmt.Errorf("directory %s is not mounted", mountPath) } - basemountPath := normalizeWindowsPath(pluginMountDir) + basemountPath := NormalizeWindowsPath(pluginMountDir) for _, ref := range refs { if strings.Contains(ref, basemountPath) { - volumeID, err := filepath.Rel(normalizeWindowsPath(basemountPath), ref) + 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 diff --git a/pkg/util/mount/hostutil_windows_test.go b/pkg/util/mount/hostutil_windows_test.go index 9af846ddd9b..1a17c05d29e 100644 --- a/pkg/util/mount/hostutil_windows_test.go +++ b/pkg/util/mount/hostutil_windows_test.go @@ -19,161 +19,11 @@ limitations under the License. package mount import ( - "fmt" "io/ioutil" "os" - "os/exec" - "path/filepath" - "strings" "testing" - - "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)) - } - return nil -} - -func removeLink(link string) error { - if output, err := exec.Command("cmd", "/c", "rmdir", link).CombinedOutput(); err != nil { - return fmt.Errorf("rmdir failed: %v, output: %q", err, string(output)) - } - return nil -} - -func setEquivalent(set1, set2 []string) bool { - map1 := make(map[string]bool) - map2 := make(map[string]bool) - for _, s := range set1 { - map1[s] = true - } - for _, s := range set2 { - map2[s] = true - } - - for s := range map1 { - if !map2[s] { - return false - } - } - for s := range map2 { - if !map1[s] { - return false - } - } - return true -} - -// this func must run in admin mode, otherwise it will fail -func TestGetMountRefs(t *testing.T) { - tests := []struct { - mountPath string - expectedRefs []string - }{ - { - mountPath: `c:\windows`, - expectedRefs: []string{`c:\windows`}, - }, - { - mountPath: `c:\doesnotexist`, - expectedRefs: []string{}, - }, - } - - mounter := Mounter{"fake/path"} - - for _, test := range tests { - if refs, err := mounter.GetMountRefs(test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { - t.Errorf("getMountRefs(%q) = %v, error: %v; expected %v", test.mountPath, refs, err, test.expectedRefs) - } - } -} - -func TestPathWithinBase(t *testing.T) { - tests := []struct { - fullPath string - basePath string - expectedResult bool - }{ - { - fullPath: `c:\tmp\a\b\c`, - basePath: `c:\tmp`, - expectedResult: true, - }, - { - fullPath: `c:\tmp1`, - basePath: `c:\tmp2`, - expectedResult: false, - }, - { - fullPath: `c:\tmp`, - basePath: `c:\tmp`, - expectedResult: true, - }, - { - fullPath: `c:\tmp`, - basePath: `c:\tmp\a\b\c`, - expectedResult: false, - }, - { - fullPath: `c:\kubelet\pods\uuid\volumes\kubernetes.io~configmap\config\..timestamp\file.txt`, - basePath: `c:\kubelet\pods\uuid\volumes\kubernetes.io~configmap\config`, - expectedResult: true, - }, - } - - for _, test := range tests { - result := PathWithinBase(test.fullPath, test.basePath) - assert.Equal(t, result, test.expectedResult, "Expect result not equal with PathWithinBase(%s, %s) return: %q, expected: %q", - test.fullPath, test.basePath, result, test.expectedResult) - } -} - func TestGetFileType(t *testing.T) { hu := NewHostUtil() @@ -222,203 +72,3 @@ func TestGetFileType(t *testing.T) { } } } - -func TestIsLikelyNotMountPoint(t *testing.T) { - mounter := Mounter{"fake/path"} - - tests := []struct { - fileName string - targetLinkName string - setUp func(base, fileName, targetLinkName string) error - expectedResult bool - expectError bool - }{ - { - "Dir", - "", - func(base, fileName, targetLinkName string) error { - return os.Mkdir(filepath.Join(base, fileName), 0750) - }, - true, - false, - }, - { - "InvalidDir", - "", - func(base, fileName, targetLinkName string) error { - return nil - }, - true, - true, - }, - { - "ValidSymLink", - "targetSymLink", - func(base, fileName, targetLinkName string) error { - targeLinkPath := filepath.Join(base, targetLinkName) - if err := os.Mkdir(targeLinkPath, 0750); err != nil { - return err - } - - filePath := filepath.Join(base, fileName) - if err := makeLink(filePath, targeLinkPath); err != nil { - return err - } - return nil - }, - false, - false, - }, - { - "InvalidSymLink", - "targetSymLink2", - func(base, fileName, targetLinkName string) error { - targeLinkPath := filepath.Join(base, targetLinkName) - if err := os.Mkdir(targeLinkPath, 0750); err != nil { - return err - } - - filePath := filepath.Join(base, fileName) - if err := makeLink(filePath, targeLinkPath); err != nil { - return err - } - return removeLink(targeLinkPath) - }, - true, - false, - }, - } - - for _, test := range tests { - base, err := ioutil.TempDir("", test.fileName) - if err != nil { - t.Fatalf(err.Error()) - } - - defer os.RemoveAll(base) - - if err := test.setUp(base, test.fileName, test.targetLinkName); err != nil { - t.Fatalf("unexpected error in setUp(%s, %s): %v", test.fileName, test.targetLinkName, err) - } - - filePath := filepath.Join(base, test.fileName) - result, err := mounter.IsLikelyNotMountPoint(filePath) - assert.Equal(t, result, test.expectedResult, "Expect result not equal with IsLikelyNotMountPoint(%s) return: %q, expected: %q", - filePath, result, test.expectedResult) - - if test.expectError { - assert.NotNil(t, err, "Expect error during IsLikelyNotMountPoint(%s)", filePath) - } else { - assert.Nil(t, err, "Expect error is nil during IsLikelyNotMountPoint(%s)", filePath) - } - } -} - -func TestFormatAndMount(t *testing.T) { - fakeMounter := ErrorMounter{&FakeMounter{}, 0, nil} - execCallback := func(cmd string, args ...string) ([]byte, error) { - for j := range args { - if strings.Contains(args[j], "Get-Disk -Number") { - return []byte("0"), nil - } - - if strings.Contains(args[j], "Get-Partition -DiskNumber") { - return []byte("0"), nil - } - - if strings.Contains(args[j], "mklink") { - return nil, nil - } - } - return nil, fmt.Errorf("Unexpected cmd %s, args %v", cmd, args) - } - fakeExec := NewFakeExec(execCallback) - - mounter := SafeFormatAndMount{ - Interface: &fakeMounter, - Exec: fakeExec, - } - - tests := []struct { - device string - target string - fstype string - mountOptions []string - expectError bool - }{ - { - "0", - "disk", - "NTFS", - []string{}, - false, - }, - { - "0", - "disk", - "", - []string{}, - false, - }, - { - "invalidDevice", - "disk", - "NTFS", - []string{}, - true, - }, - } - - for _, test := range tests { - base, err := ioutil.TempDir("", test.device) - if err != nil { - t.Fatalf(err.Error()) - } - defer os.RemoveAll(base) - - target := filepath.Join(base, test.target) - err = mounter.FormatAndMount(test.device, target, test.fstype, test.mountOptions) - if test.expectError { - assert.NotNil(t, err, "Expect error during FormatAndMount(%s, %s, %s, %v)", test.device, test.target, test.fstype, test.mountOptions) - } else { - assert.Nil(t, err, "Expect error is nil during FormatAndMount(%s, %s, %s, %v)", test.device, test.target, test.fstype, test.mountOptions) - } - } -} - -func TestNewSMBMapping(t *testing.T) { - tests := []struct { - username string - password string - remotepath string - expectError bool - }{ - { - "", - "password", - `\\remotepath`, - true, - }, - { - "username", - "", - `\\remotepath`, - true, - }, - { - "username", - "password", - "", - true, - }, - } - - for _, test := range tests { - _, err := newSMBMapping(test.username, test.password, test.remotepath) - if test.expectError { - assert.NotNil(t, err, "Expect error during newSMBMapping(%s, %s, %s, %v)", test.username, test.password, test.remotepath) - } else { - assert.Nil(t, err, "Expect error is nil during newSMBMapping(%s, %s, %s, %v)", test.username, test.password, test.remotepath) - } - } -} diff --git a/pkg/util/mount/mount.go b/pkg/util/mount/mount.go index 5f0fe1867f4..a73f44313f5 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 @@ -337,37 +283,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 index e8ff8251d8f..5c1ebdb4a22 100644 --- a/pkg/util/mount/mount_helper_unix_test.go +++ b/pkg/util/mount/mount_helper_unix_test.go @@ -1,7 +1,7 @@ -// +build linux +// +build !windows /* -Copyright 2014 The Kubernetes Authors. +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. @@ -19,196 +19,13 @@ 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) { - successCase := - `/dev/0 /path/to/0 type0 flags 0 0 -/dev/1 /path/to/1 type1 flags 1 1 -/dev/2 /path/to/2 type2 flags,1,2=3 2 2 -` - // NOTE: readProcMountsFrom has been updated to using fnv.New32a() - mounts, err := parseProcMounts([]byte(successCase)) - if err != nil { - t.Errorf("expected success, got %v", err) - } - if len(mounts) != 3 { - t.Fatalf("expected 3 mounts, got %d", len(mounts)) - } - mp := MountPoint{"/dev/0", "/path/to/0", "type0", []string{"flags"}, 0, 0} - if !mountPointsEqual(&mounts[0], &mp) { - t.Errorf("got unexpected MountPoint[0]: %#v", mounts[0]) - } - mp = MountPoint{"/dev/1", "/path/to/1", "type1", []string{"flags"}, 1, 1} - if !mountPointsEqual(&mounts[1], &mp) { - t.Errorf("got unexpected MountPoint[1]: %#v", mounts[1]) - } - mp = MountPoint{"/dev/2", "/path/to/2", "type2", []string{"flags", "1", "2=3"}, 2, 2} - if !mountPointsEqual(&mounts[2], &mp) { - t.Errorf("got unexpected MountPoint[2]: %#v", mounts[2]) - } - - errorCases := []string{ - "/dev/0 /path/to/mount\n", - "/dev/1 /path/to/mount type flags a 0\n", - "/dev/2 /path/to/mount type flags 0 b\n", - } - for _, ec := range errorCases { - _, err := parseProcMounts([]byte(ec)) - if err == nil { - t.Errorf("expected error") - } - } -} - -func mountPointsEqual(a, b *MountPoint) bool { - if a.Device != b.Device || a.Path != b.Path || a.Type != b.Type || !reflect.DeepEqual(a.Opts, b.Opts) || a.Pass != b.Pass || a.Freq != b.Freq { - return false - } - return true -} - -func TestGetMountRefs(t *testing.T) { - fm := &FakeMounter{ - MountPoints: []MountPoint{ - {Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"}, - {Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2"}, - }, - } - - tests := []struct { - mountPath string - expectedRefs []string - }{ - { - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod", - []string{ - "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd", - }, - }, - { - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1", - []string{ - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2", - "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2", - }, - }, - { - "/var/fake/directory/that/doesnt/exist", - []string{}, - }, - } - - for i, test := range tests { - if refs, err := fm.GetMountRefs(test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { - t.Errorf("%d. getMountRefs(%q) = %v, %v; expected %v, nil", i, test.mountPath, refs, err, test.expectedRefs) - } - } -} - -func setEquivalent(set1, set2 []string) bool { - map1 := make(map[string]bool) - map2 := make(map[string]bool) - for _, s := range set1 { - map1[s] = true - } - for _, s := range set2 { - map2[s] = true - } - - for s := range map1 { - if !map2[s] { - return false - } - } - for s := range map2 { - if !map1[s] { - return false - } - } - return true -} - -func TestGetDeviceNameFromMount(t *testing.T) { - fm := &FakeMounter{ - MountPoints: []MountPoint{ - {Device: "/dev/disk/by-path/prefix-lun-1", - Path: "/mnt/111"}, - {Device: "/dev/disk/by-path/prefix-lun-1", - Path: "/mnt/222"}, - }, - } - - tests := []struct { - mountPath string - expectedDevice string - expectedRefs int - }{ - { - "/mnt/222", - "/dev/disk/by-path/prefix-lun-1", - 2, - }, - } - - for i, test := range tests { - if device, refs, err := GetDeviceNameFromMount(fm, test.mountPath); err != nil || test.expectedRefs != refs || test.expectedDevice != device { - t.Errorf("%d. GetDeviceNameFromMount(%s) = (%s, %d), %v; expected (%s,%d), nil", i, test.mountPath, device, refs, err, test.expectedDevice, test.expectedRefs) - } - } -} - -func TestGetMountRefsByDev(t *testing.T) { - fm := &FakeMounter{ - MountPoints: []MountPoint{ - {Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"}, - {Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1"}, - {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2"}, - }, - } - - tests := []struct { - mountPath string - expectedRefs []string - }{ - { - "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd", - []string{ - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod", - }, - }, - { - "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2", - []string{ - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1", - "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2", - }, - }, - } - - for i, test := range tests { - - if refs, err := getMountRefsByDev(fm, test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { - t.Errorf("%d. getMountRefsByDev(%q) = %v, %v; expected %v, nil", i, test.mountPath, refs, err, test.expectedRefs) - } - } -} - func writeFile(content string) (string, string, error) { tempDir, err := ioutil.TempDir("", "mounter_shared_test") if err != nil { @@ -223,203 +40,6 @@ func writeFile(content string) (string, string, error) { 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 - fullPath string - basePath string - expected bool - }{ - { - name: "good subpath", - fullPath: "/a/b/c", - basePath: "/a", - expected: true, - }, - { - name: "good subpath 2", - fullPath: "/a/b/c", - basePath: "/a/b", - expected: true, - }, - { - name: "good subpath end slash", - fullPath: "/a/b/c/", - basePath: "/a/b", - expected: true, - }, - { - name: "good subpath backticks", - fullPath: "/a/b/../c", - basePath: "/a", - expected: true, - }, - { - name: "good subpath equal", - fullPath: "/a/b/c", - basePath: "/a/b/c", - expected: true, - }, - { - name: "good subpath equal 2", - fullPath: "/a/b/c/", - basePath: "/a/b/c", - expected: true, - }, - { - name: "good subpath root", - fullPath: "/a", - basePath: "/", - expected: true, - }, - { - name: "bad subpath parent", - fullPath: "/a/b/c", - basePath: "/a/b/c/d", - expected: false, - }, - { - name: "bad subpath outside", - fullPath: "/b/c", - basePath: "/a/b/c", - expected: false, - }, - { - name: "bad subpath prefix", - fullPath: "/a/b/cd", - basePath: "/a/b/c", - expected: false, - }, - { - name: "bad subpath backticks", - fullPath: "/a/../b", - basePath: "/a", - expected: false, - }, - { - name: "configmap subpath", - fullPath: "/var/lib/kubelet/pods/uuid/volumes/kubernetes.io~configmap/config/..timestamp/file.txt", - basePath: "/var/lib/kubelet/pods/uuid/volumes/kubernetes.io~configmap/config", - expected: true, - }, - } - for _, test := range tests { - if PathWithinBase(test.fullPath, test.basePath) != test.expected { - t.Errorf("test %q failed: expected %v", test.name, test.expected) - } - - } -} - func TestParseMountInfo(t *testing.T) { info := `62 0 253:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered @@ -593,326 +213,3 @@ func TestParseMountInfo(t *testing.T) { } } } - -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 -20 25 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw -21 25 0:6 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=4058156k,nr_inodes=1014539,mode=755 -22 21 0:14 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 -23 25 0:19 / /run rw,nosuid,noexec,relatime shared:5 - tmpfs tmpfs rw,size=815692k,mode=755 -25 0 252:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered -26 19 0:12 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:8 - securityfs securityfs rw -27 21 0:21 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw -28 23 0:22 / /run/lock rw,nosuid,nodev,noexec,relatime shared:6 - tmpfs tmpfs rw,size=5120k -29 19 0:23 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755 -30 29 0:24 / /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 19 0:25 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:11 - pstore pstore rw -32 29 0:26 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,devices -33 29 0:27 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,freezer -34 29 0:28 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,pids -35 29 0:29 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,blkio -36 29 0:30 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,memory -37 29 0:31 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,perf_event -38 29 0:32 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,hugetlb -39 29 0:33 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,cpu,cpuacct -40 29 0:34 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,cpuset -41 29 0:35 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,net_cls,net_prio -58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordere -` - - testcases := []struct { - name string - source string - mountInfos string - expectedRefs []string - expectedErr error - }{ - { - "dir", - "/mnt/disks/vol1", - base, - nil, - nil, - }, - { - "dir-used", - "/mnt/disks/vol1", - base + ` -56 25 252:0 /mnt/disks/vol1 /var/lib/kubelet/pods/1890aef5-5a60-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered -57 25 0:45 / /mnt/disks/vol rw,relatime shared:36 - tmpfs tmpfs rw -`, - []string{"/var/lib/kubelet/pods/1890aef5-5a60-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, - nil, - }, - { - "tmpfs-vol", - "/mnt/disks/vol1", - base + `120 25 0:76 / /mnt/disks/vol1 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k -`, - nil, - nil, - }, - { - "tmpfs-vol-used-by-two-pods", - "/mnt/disks/vol1", - base + `120 25 0:76 / /mnt/disks/vol1 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k -196 25 0:76 / /var/lib/kubelet/pods/ade3ac21-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k -228 25 0:76 / /var/lib/kubelet/pods/ac60532d-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k -`, - []string{ - "/var/lib/kubelet/pods/ade3ac21-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585", - "/var/lib/kubelet/pods/ac60532d-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585", - }, - nil, - }, - { - "tmpfs-subdir-used-indirectly-via-bindmount-dir-by-one-pod", - "/mnt/vol1/foo", - base + `177 25 0:46 / /mnt/data rw,relatime shared:37 - tmpfs data rw -190 25 0:46 /vol1 /mnt/vol1 rw,relatime shared:37 - tmpfs data rw -191 25 0:46 /vol2 /mnt/vol2 rw,relatime shared:37 - tmpfs data rw -62 25 0:46 /vol1/foo /var/lib/kubelet/pods/e25f2f01-5b06-11e8-8694-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:37 - tmpfs data rw -`, - []string{"/var/lib/kubelet/pods/e25f2f01-5b06-11e8-8694-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, - nil, - }, - { - "dir-bindmounted", - "/mnt/disks/vol2", - base + `342 25 252:0 /mnt/disks/vol2 /mnt/disks/vol2 rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered -`, - nil, - nil, - }, - { - "dir-bindmounted-used-by-one-pod", - "/mnt/disks/vol2", - base + `342 25 252:0 /mnt/disks/vol2 /mnt/disks/vol2 rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered -77 25 252:0 /mnt/disks/vol2 /var/lib/kubelet/pods/f30dc360-5a5d-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-1fb30a1c rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered -`, - []string{"/var/lib/kubelet/pods/f30dc360-5a5d-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-1fb30a1c"}, - nil, - }, - { - "blockfs", - "/mnt/disks/blkvol1", - base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -`, - nil, - nil, - }, - { - "blockfs-used-by-one-pod", - "/mnt/disks/blkvol1", - base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -62 25 7:1 / /var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -`, - []string{"/var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, - nil, - }, - { - "blockfs-used-by-two-pods", - "/mnt/disks/blkvol1", - base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -62 25 7:1 / /var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -95 25 7:1 / /var/lib/kubelet/pods/4854a48b-5a64-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered -`, - []string{"/var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test", - "/var/lib/kubelet/pods/4854a48b-5a64-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, - nil, - }, - } - tmpFile, err := ioutil.TempFile("", "test-get-filetype") - if err != nil { - t.Fatal(err) - } - defer os.Remove(tmpFile.Name()) - defer tmpFile.Close() - for _, v := range testcases { - tmpFile.Truncate(0) - tmpFile.Seek(0, 0) - tmpFile.WriteString(v.mountInfos) - tmpFile.Sync() - refs, err := SearchMountPoints(v.source, tmpFile.Name()) - if !reflect.DeepEqual(refs, v.expectedRefs) { - t.Errorf("test %q: expected Refs: %#v, got %#v", v.name, v.expectedRefs, refs) - } - if !reflect.DeepEqual(err, v.expectedErr) { - t.Errorf("test %q: expected err: %v, got %v", v.name, v.expectedErr, err) - } - } -} 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 index 9af846ddd9b..7f5e6eb1827 100644 --- a/pkg/util/mount/mount_helper_windows_test.go +++ b/pkg/util/mount/mount_helper_windows_test.go @@ -19,32 +19,24 @@ limitations under the License. package mount import ( - "fmt" - "io/ioutil" - "os" - "os/exec" - "path/filepath" - "strings" "testing" - - "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) + 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) + 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) + normalizedPath = NormalizeWindowsPath(path) if normalizedPath != `c:\` { t.Errorf("normizeWindowsPath test failed, normalizedPath : %q", normalizedPath) } @@ -71,354 +63,3 @@ func TestValidateDiskNumber(t *testing.T) { 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)) - } - return nil -} - -func removeLink(link string) error { - if output, err := exec.Command("cmd", "/c", "rmdir", link).CombinedOutput(); err != nil { - return fmt.Errorf("rmdir failed: %v, output: %q", err, string(output)) - } - return nil -} - -func setEquivalent(set1, set2 []string) bool { - map1 := make(map[string]bool) - map2 := make(map[string]bool) - for _, s := range set1 { - map1[s] = true - } - for _, s := range set2 { - map2[s] = true - } - - for s := range map1 { - if !map2[s] { - return false - } - } - for s := range map2 { - if !map1[s] { - return false - } - } - return true -} - -// this func must run in admin mode, otherwise it will fail -func TestGetMountRefs(t *testing.T) { - tests := []struct { - mountPath string - expectedRefs []string - }{ - { - mountPath: `c:\windows`, - expectedRefs: []string{`c:\windows`}, - }, - { - mountPath: `c:\doesnotexist`, - expectedRefs: []string{}, - }, - } - - mounter := Mounter{"fake/path"} - - for _, test := range tests { - if refs, err := mounter.GetMountRefs(test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { - t.Errorf("getMountRefs(%q) = %v, error: %v; expected %v", test.mountPath, refs, err, test.expectedRefs) - } - } -} - -func TestPathWithinBase(t *testing.T) { - tests := []struct { - fullPath string - basePath string - expectedResult bool - }{ - { - fullPath: `c:\tmp\a\b\c`, - basePath: `c:\tmp`, - expectedResult: true, - }, - { - fullPath: `c:\tmp1`, - basePath: `c:\tmp2`, - expectedResult: false, - }, - { - fullPath: `c:\tmp`, - basePath: `c:\tmp`, - expectedResult: true, - }, - { - fullPath: `c:\tmp`, - basePath: `c:\tmp\a\b\c`, - expectedResult: false, - }, - { - fullPath: `c:\kubelet\pods\uuid\volumes\kubernetes.io~configmap\config\..timestamp\file.txt`, - basePath: `c:\kubelet\pods\uuid\volumes\kubernetes.io~configmap\config`, - expectedResult: true, - }, - } - - for _, test := range tests { - result := PathWithinBase(test.fullPath, test.basePath) - assert.Equal(t, result, test.expectedResult, "Expect result not equal with PathWithinBase(%s, %s) return: %q, expected: %q", - test.fullPath, test.basePath, result, test.expectedResult) - } -} - -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"} - - tests := []struct { - fileName string - targetLinkName string - setUp func(base, fileName, targetLinkName string) error - expectedResult bool - expectError bool - }{ - { - "Dir", - "", - func(base, fileName, targetLinkName string) error { - return os.Mkdir(filepath.Join(base, fileName), 0750) - }, - true, - false, - }, - { - "InvalidDir", - "", - func(base, fileName, targetLinkName string) error { - return nil - }, - true, - true, - }, - { - "ValidSymLink", - "targetSymLink", - func(base, fileName, targetLinkName string) error { - targeLinkPath := filepath.Join(base, targetLinkName) - if err := os.Mkdir(targeLinkPath, 0750); err != nil { - return err - } - - filePath := filepath.Join(base, fileName) - if err := makeLink(filePath, targeLinkPath); err != nil { - return err - } - return nil - }, - false, - false, - }, - { - "InvalidSymLink", - "targetSymLink2", - func(base, fileName, targetLinkName string) error { - targeLinkPath := filepath.Join(base, targetLinkName) - if err := os.Mkdir(targeLinkPath, 0750); err != nil { - return err - } - - filePath := filepath.Join(base, fileName) - if err := makeLink(filePath, targeLinkPath); err != nil { - return err - } - return removeLink(targeLinkPath) - }, - true, - false, - }, - } - - for _, test := range tests { - base, err := ioutil.TempDir("", test.fileName) - if err != nil { - t.Fatalf(err.Error()) - } - - defer os.RemoveAll(base) - - if err := test.setUp(base, test.fileName, test.targetLinkName); err != nil { - t.Fatalf("unexpected error in setUp(%s, %s): %v", test.fileName, test.targetLinkName, err) - } - - filePath := filepath.Join(base, test.fileName) - result, err := mounter.IsLikelyNotMountPoint(filePath) - assert.Equal(t, result, test.expectedResult, "Expect result not equal with IsLikelyNotMountPoint(%s) return: %q, expected: %q", - filePath, result, test.expectedResult) - - if test.expectError { - assert.NotNil(t, err, "Expect error during IsLikelyNotMountPoint(%s)", filePath) - } else { - assert.Nil(t, err, "Expect error is nil during IsLikelyNotMountPoint(%s)", filePath) - } - } -} - -func TestFormatAndMount(t *testing.T) { - fakeMounter := ErrorMounter{&FakeMounter{}, 0, nil} - execCallback := func(cmd string, args ...string) ([]byte, error) { - for j := range args { - if strings.Contains(args[j], "Get-Disk -Number") { - return []byte("0"), nil - } - - if strings.Contains(args[j], "Get-Partition -DiskNumber") { - return []byte("0"), nil - } - - if strings.Contains(args[j], "mklink") { - return nil, nil - } - } - return nil, fmt.Errorf("Unexpected cmd %s, args %v", cmd, args) - } - fakeExec := NewFakeExec(execCallback) - - mounter := SafeFormatAndMount{ - Interface: &fakeMounter, - Exec: fakeExec, - } - - tests := []struct { - device string - target string - fstype string - mountOptions []string - expectError bool - }{ - { - "0", - "disk", - "NTFS", - []string{}, - false, - }, - { - "0", - "disk", - "", - []string{}, - false, - }, - { - "invalidDevice", - "disk", - "NTFS", - []string{}, - true, - }, - } - - for _, test := range tests { - base, err := ioutil.TempDir("", test.device) - if err != nil { - t.Fatalf(err.Error()) - } - defer os.RemoveAll(base) - - target := filepath.Join(base, test.target) - err = mounter.FormatAndMount(test.device, target, test.fstype, test.mountOptions) - if test.expectError { - assert.NotNil(t, err, "Expect error during FormatAndMount(%s, %s, %s, %v)", test.device, test.target, test.fstype, test.mountOptions) - } else { - assert.Nil(t, err, "Expect error is nil during FormatAndMount(%s, %s, %s, %v)", test.device, test.target, test.fstype, test.mountOptions) - } - } -} - -func TestNewSMBMapping(t *testing.T) { - tests := []struct { - username string - password string - remotepath string - expectError bool - }{ - { - "", - "password", - `\\remotepath`, - true, - }, - { - "username", - "", - `\\remotepath`, - true, - }, - { - "username", - "password", - "", - true, - }, - } - - for _, test := range tests { - _, err := newSMBMapping(test.username, test.password, test.remotepath) - if test.expectError { - assert.NotNil(t, err, "Expect error during newSMBMapping(%s, %s, %s, %v)", test.username, test.password, test.remotepath) - } else { - assert.Nil(t, err, "Expect error is nil during newSMBMapping(%s, %s, %s, %v)", test.username, test.password, test.remotepath) - } - } -} 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"}