github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/fs/fs.go (about)

     1  /*
     2  Copyright 2019 Mirantis
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package fs
    18  
    19  import (
    20  	"bufio"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"os"
    26  	"os/user"
    27  	"path/filepath"
    28  	"strconv"
    29  	"strings"
    30  	"sync"
    31  
    32  	"github.com/golang/glog"
    33  )
    34  
    35  const (
    36  	emulatorUserName     = "libvirt-qemu"
    37  	defaultMountInfoPath = "/proc/self/mountinfo"
    38  )
    39  
    40  // DelimitedReader is an interface for reading a delimeter-separated
    41  // data from files. It can be used for reading /sys and /proc
    42  // information, for example.
    43  type DelimitedReader interface {
    44  	// ReadString returns next part of data up to (and including it)
    45  	// the delimeter byte.
    46  	ReadString(delim byte) (string, error)
    47  	// Close closes the reader.
    48  	Close() error
    49  }
    50  
    51  // FileSystem defines a filesystem interface interface
    52  type FileSystem interface {
    53  	// Mount mounts the specified source under the target path.
    54  	// For bind mounts, bind must be true. The bind mounts will
    55  	// happen in the host mount namespace (that of PID 1).
    56  	Mount(source string, target string, fstype string, bind bool) error
    57  	// Unmount unmounts the specified target directory. If detach
    58  	// is true, MNT_DETACH option is used (disconnect the
    59  	// filesystem for the new accesses even if it's busy).
    60  	Unmount(target string, detach bool) error
    61  	// IsPathAnNs verifies if the path is a mountpoint with nsfs filesystem type.
    62  	IsPathAnNs(string) bool
    63  	// ChownForEmulator makes a file or directory owned by the emulator user.
    64  	ChownForEmulator(filePath string, recursive bool) error
    65  	// GetDelimitedReader returns a DelimitedReader for the specified path.
    66  	GetDelimitedReader(path string) (DelimitedReader, error)
    67  	// WriteFile creates a new file with the specified path or truncates
    68  	// the existing one, setting the specified permissions and writing
    69  	// the data to it. Returns an error if any occured
    70  	// during the operation.
    71  	WriteFile(path string, data []byte, perm os.FileMode) error
    72  }
    73  
    74  type nullFileSystem struct{}
    75  
    76  // NullFileSystem is a fs that's used for testing and does nothing
    77  // instead of mounting/unmounting.
    78  var NullFileSystem FileSystem = nullFileSystem{}
    79  
    80  func (fs nullFileSystem) Mount(source string, target string, fstype string, bind bool) error {
    81  	return nil
    82  }
    83  
    84  func (fs nullFileSystem) Unmount(target string, detach bool) error {
    85  	return nil
    86  }
    87  
    88  func (fs nullFileSystem) IsPathAnNs(path string) bool {
    89  	return false
    90  }
    91  
    92  func (fs nullFileSystem) ChownForEmulator(filePath string, recursive bool) error {
    93  	return nil
    94  }
    95  
    96  func (fs nullFileSystem) GetDelimitedReader(path string) (DelimitedReader, error) {
    97  	return nil, errors.New("not implemented")
    98  }
    99  
   100  func (fs nullFileSystem) WriteFile(path string, data []byte, perm os.FileMode) error {
   101  	return nil
   102  }
   103  
   104  type mountEntry struct {
   105  	source, fsType string
   106  }
   107  
   108  type realFileSystem struct {
   109  	sync.Mutex
   110  	mountInfo       map[string]mountEntry
   111  	gotEmulatorUser bool
   112  	uid, gid        int
   113  	mountInfoPath   string
   114  }
   115  
   116  // RealFileSystem provides access to the real filesystem.
   117  var RealFileSystem FileSystem = &realFileSystem{}
   118  
   119  func (fs *realFileSystem) ensureMountInfo() error {
   120  	fs.Lock()
   121  	defer fs.Unlock()
   122  	if fs.mountInfo != nil {
   123  		return nil
   124  	}
   125  
   126  	mountInfoPath := fs.mountInfoPath
   127  	if mountInfoPath == "" {
   128  		mountInfoPath = defaultMountInfoPath
   129  	}
   130  
   131  	reader, err := fs.GetDelimitedReader(mountInfoPath)
   132  	if err != nil {
   133  		return err
   134  	}
   135  	defer reader.Close()
   136  
   137  	fs.mountInfo = make(map[string]mountEntry)
   138  LineReader:
   139  	for {
   140  		line, err := reader.ReadString('\n')
   141  		switch err {
   142  		case io.EOF:
   143  			break LineReader
   144  		case nil:
   145  			// strip eol
   146  			line = strings.Trim(line, "\n")
   147  
   148  			// split and parse the entries acording to section 3.5 in
   149  			// https://www.kernel.org/doc/Documentation/filesystems/proc.txt
   150  			// TODO: whitespaces and control chars in names are encoded as
   151  			// octal values (e.g. for "x x": "x\040x") what should be expanded
   152  			// in both mount point source and target
   153  			parts := strings.Split(line, " ")
   154  			if len(parts) < 10 {
   155  				glog.Errorf("bad mountinfo entry: %q", line)
   156  			} else {
   157  				fs.mountInfo[parts[4]] = mountEntry{source: parts[9], fsType: parts[8]}
   158  			}
   159  		default:
   160  			return err
   161  		}
   162  	}
   163  	return nil
   164  }
   165  
   166  func (fs *realFileSystem) getMountInfo(path string) (mountEntry, bool, error) {
   167  	if err := fs.ensureMountInfo(); err != nil {
   168  		return mountEntry{}, false, err
   169  	}
   170  
   171  	fs.Lock()
   172  	defer fs.Unlock()
   173  	entry, ok := fs.mountInfo[path]
   174  	return entry, ok, nil
   175  }
   176  
   177  func (fs *realFileSystem) IsPathAnNs(path string) bool {
   178  	_, err := os.Stat(path)
   179  	if err != nil {
   180  		if !os.IsNotExist(err) {
   181  			glog.Errorf("Can't check if %q exists: %v", path, err)
   182  		}
   183  		return false
   184  	}
   185  	realpath, err := filepath.EvalSymlinks(path)
   186  	if err != nil {
   187  		glog.Errorf("Can't get the real path of %q: %v", path, err)
   188  		return false
   189  	}
   190  
   191  	entry, isMountPoint, err := fs.getMountInfo(realpath)
   192  	if err != nil {
   193  		glog.Errorf("Can't check if %q is a namespace: error getting mount info: %v", path, err)
   194  		return false
   195  	}
   196  
   197  	return isMountPoint && (entry.fsType == "nsfs" || entry.fsType == "proc")
   198  }
   199  
   200  func (fs *realFileSystem) getEmulatorUidGid() (int, int, error) {
   201  	fs.Lock()
   202  	defer fs.Unlock()
   203  	if !fs.gotEmulatorUser {
   204  		u, err := user.Lookup(emulatorUserName)
   205  		if err != nil {
   206  			return 0, 0, fmt.Errorf("can't find user %q: %v", emulatorUserName, err)
   207  		}
   208  		fs.uid, err = strconv.Atoi(u.Uid)
   209  		if err != nil {
   210  			return 0, 0, fmt.Errorf("bad uid %q for user %q: %v", u.Uid, emulatorUserName, err)
   211  		}
   212  		fs.gid, err = strconv.Atoi(u.Gid)
   213  		if err != nil {
   214  			return 0, 0, fmt.Errorf("bad gid %q for user %q: %v", u.Gid, emulatorUserName, err)
   215  		}
   216  	}
   217  	return fs.uid, fs.gid, nil
   218  }
   219  
   220  func (fs *realFileSystem) ChownForEmulator(filePath string, recursive bool) error {
   221  	// don't hold the mutex for the duration of chown
   222  	uid, gid, err := fs.getEmulatorUidGid()
   223  	if err != nil {
   224  		return err
   225  	}
   226  
   227  	chown := os.Chown
   228  	if recursive {
   229  		chown = chownR
   230  	}
   231  	if err := chown(filePath, uid, gid); err != nil {
   232  		return fmt.Errorf("can't set the owner of %q: %v", filePath, err)
   233  	}
   234  	return nil
   235  }
   236  
   237  func (fs *realFileSystem) GetDelimitedReader(path string) (DelimitedReader, error) {
   238  	f, err := os.Open(path)
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	return &delimitedReader{f, bufio.NewReader(f)}, nil
   243  }
   244  
   245  func (fs *realFileSystem) WriteFile(path string, data []byte, perm os.FileMode) error {
   246  	return ioutil.WriteFile(path, data, perm)
   247  }
   248  
   249  type delimitedReader struct {
   250  	*os.File
   251  	*bufio.Reader
   252  }
   253  
   254  var _ DelimitedReader = &delimitedReader{}
   255  
   256  // chownR makes a file or directory owned by the emulator user recursively.
   257  func chownR(path string, uid, gid int) error {
   258  	return filepath.Walk(path, func(name string, info os.FileInfo, err error) error {
   259  		if err == nil {
   260  			err = os.Chown(name, uid, gid)
   261  			if err != nil {
   262  				glog.Warningf("Failed to change the owner of %q: %v", name, err)
   263  			}
   264  		}
   265  		return err
   266  	})
   267  }