copy mount files to hostutils
This commit is contained in:
280
pkg/util/mount/hostutils/fake_hostutil.go
Normal file
280
pkg/util/mount/hostutils/fake_hostutil.go
Normal file
@@ -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")
|
||||||
|
}
|
373
pkg/util/mount/hostutils/mount.go
Normal file
373
pkg/util/mount/hostutils/mount.go
Normal file
@@ -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")
|
||||||
|
}
|
840
pkg/util/mount/hostutils/mount_linux.go
Normal file
840
pkg/util/mount/hostutils/mount_linux.go
Normal file
@@ -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/<pid>/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 <type> <what> <where>
|
||||||
|
//
|
||||||
|
// 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/<pid>/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 <path> <path>' and '--make-rshared <path>' 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
|
||||||
|
}
|
918
pkg/util/mount/hostutils/mount_linux_test.go
Normal file
918
pkg/util/mount/hostutils/mount_linux_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
pkg/util/mount/hostutils/mount_unsupported.go
Normal file
144
pkg/util/mount/hostutils/mount_unsupported.go
Normal file
@@ -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
|
||||||
|
}
|
424
pkg/util/mount/hostutils/mount_windows.go
Normal file
424
pkg/util/mount/hostutils/mount_windows.go
Normal file
@@ -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
|
||||||
|
}
|
424
pkg/util/mount/hostutils/mount_windows_test.go
Normal file
424
pkg/util/mount/hostutils/mount_windows_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user