github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/storage/provider/managedfs.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provider
     5  
     6  import (
     7  	"bufio"
     8  	"io/ioutil"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"strings"
    13  	"unicode"
    14  
    15  	"github.com/juju/errors"
    16  	"gopkg.in/juju/names.v2"
    17  
    18  	"github.com/juju/juju/environs/context"
    19  	"github.com/juju/juju/storage"
    20  )
    21  
    22  const (
    23  	// defaultFilesystemType is the default filesystem type
    24  	// to create for volume-backed managed filesystems.
    25  	defaultFilesystemType = "ext4"
    26  )
    27  
    28  // managedFilesystemSource is an implementation of storage.FilesystemSource
    29  // that manages filesystems on volumes attached to the host machine.
    30  //
    31  // managedFilesystemSource is expected to be called from a single goroutine.
    32  type managedFilesystemSource struct {
    33  	run                runCommandFunc
    34  	dirFuncs           dirFuncs
    35  	volumeBlockDevices map[names.VolumeTag]storage.BlockDevice
    36  	filesystems        map[names.FilesystemTag]storage.Filesystem
    37  }
    38  
    39  // NewManagedFilesystemSource returns a storage.FilesystemSource that manages
    40  // filesystems on block devices on the host machine.
    41  //
    42  // The parameters are maps that the caller will update with information about
    43  // block devices and filesystems created by the source. The caller must not
    44  // update the maps during calls to the source's methods.
    45  func NewManagedFilesystemSource(
    46  	volumeBlockDevices map[names.VolumeTag]storage.BlockDevice,
    47  	filesystems map[names.FilesystemTag]storage.Filesystem,
    48  ) storage.FilesystemSource {
    49  	return &managedFilesystemSource{
    50  		logAndExec,
    51  		&osDirFuncs{logAndExec},
    52  		volumeBlockDevices, filesystems,
    53  	}
    54  }
    55  
    56  // ValidateFilesystemParams is defined on storage.FilesystemSource.
    57  func (s *managedFilesystemSource) ValidateFilesystemParams(arg storage.FilesystemParams) error {
    58  	// NOTE(axw) the parameters may be for destroying a filesystem, which
    59  	// may be called when the backing volume is detached from the machine.
    60  	// We must not perform any validation here that would fail if the
    61  	// volume is detached.
    62  	return nil
    63  }
    64  
    65  func (s *managedFilesystemSource) backingVolumeBlockDevice(v names.VolumeTag) (storage.BlockDevice, error) {
    66  	blockDevice, ok := s.volumeBlockDevices[v]
    67  	if !ok {
    68  		return storage.BlockDevice{}, errors.Errorf(
    69  			"backing-volume %s is not yet attached", v.Id(),
    70  		)
    71  	}
    72  	return blockDevice, nil
    73  }
    74  
    75  // CreateFilesystems is defined on storage.FilesystemSource.
    76  func (s *managedFilesystemSource) CreateFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemParams) ([]storage.CreateFilesystemsResult, error) {
    77  	results := make([]storage.CreateFilesystemsResult, len(args))
    78  	for i, arg := range args {
    79  		filesystem, err := s.createFilesystem(arg)
    80  		if err != nil {
    81  			results[i].Error = err
    82  			continue
    83  		}
    84  		results[i].Filesystem = filesystem
    85  	}
    86  	return results, nil
    87  }
    88  
    89  func (s *managedFilesystemSource) createFilesystem(arg storage.FilesystemParams) (*storage.Filesystem, error) {
    90  	blockDevice, err := s.backingVolumeBlockDevice(arg.Volume)
    91  	if err != nil {
    92  		return nil, errors.Trace(err)
    93  	}
    94  	devicePath := devicePath(blockDevice)
    95  	if isDiskDevice(devicePath) {
    96  		if err := destroyPartitions(s.run, devicePath); err != nil {
    97  			return nil, errors.Trace(err)
    98  		}
    99  		if err := createPartition(s.run, devicePath); err != nil {
   100  			return nil, errors.Trace(err)
   101  		}
   102  		devicePath = partitionDevicePath(devicePath)
   103  	}
   104  	if err := createFilesystem(s.run, devicePath); err != nil {
   105  		return nil, errors.Trace(err)
   106  	}
   107  	return &storage.Filesystem{
   108  		arg.Tag,
   109  		arg.Volume,
   110  		storage.FilesystemInfo{
   111  			arg.Tag.String(),
   112  			blockDevice.Size,
   113  		},
   114  	}, nil
   115  }
   116  
   117  // DestroyFilesystems is defined on storage.FilesystemSource.
   118  func (s *managedFilesystemSource) DestroyFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) {
   119  	// DestroyFilesystems is a no-op; there is nothing to destroy,
   120  	// since the filesystem is just data on a volume. The volume
   121  	// is destroyed separately.
   122  	return make([]error, len(filesystemIds)), nil
   123  }
   124  
   125  // ReleaseFilesystems is defined on storage.FilesystemSource.
   126  func (s *managedFilesystemSource) ReleaseFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) {
   127  	return make([]error, len(filesystemIds)), nil
   128  }
   129  
   130  // AttachFilesystems is defined on storage.FilesystemSource.
   131  func (s *managedFilesystemSource) AttachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]storage.AttachFilesystemsResult, error) {
   132  	results := make([]storage.AttachFilesystemsResult, len(args))
   133  	for i, arg := range args {
   134  		attachment, err := s.attachFilesystem(arg)
   135  		if err != nil {
   136  			results[i].Error = err
   137  			continue
   138  		}
   139  		results[i].FilesystemAttachment = attachment
   140  	}
   141  	return results, nil
   142  }
   143  
   144  func (s *managedFilesystemSource) attachFilesystem(arg storage.FilesystemAttachmentParams) (*storage.FilesystemAttachment, error) {
   145  	filesystem, ok := s.filesystems[arg.Filesystem]
   146  	if !ok {
   147  		return nil, errors.Errorf("filesystem %v is not yet provisioned", arg.Filesystem.Id())
   148  	}
   149  	blockDevice, err := s.backingVolumeBlockDevice(filesystem.Volume)
   150  	if err != nil {
   151  		return nil, errors.Trace(err)
   152  	}
   153  	devicePath := devicePath(blockDevice)
   154  	if isDiskDevice(devicePath) {
   155  		devicePath = partitionDevicePath(devicePath)
   156  	}
   157  	if err := mountFilesystem(s.run, s.dirFuncs, devicePath, arg.Path, arg.ReadOnly); err != nil {
   158  		return nil, errors.Trace(err)
   159  	}
   160  	return &storage.FilesystemAttachment{
   161  		arg.Filesystem,
   162  		arg.Machine,
   163  		storage.FilesystemAttachmentInfo{
   164  			arg.Path,
   165  			arg.ReadOnly,
   166  		},
   167  	}, nil
   168  }
   169  
   170  // DetachFilesystems is defined on storage.FilesystemSource.
   171  func (s *managedFilesystemSource) DetachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]error, error) {
   172  	results := make([]error, len(args))
   173  	for i, arg := range args {
   174  		if err := maybeUnmount(s.run, s.dirFuncs, arg.Path); err != nil {
   175  			results[i] = err
   176  		}
   177  	}
   178  	return results, nil
   179  }
   180  
   181  func destroyPartitions(run runCommandFunc, devicePath string) error {
   182  	logger.Debugf("destroying partitions on %q", devicePath)
   183  	if _, err := run("sgdisk", "--zap-all", devicePath); err != nil {
   184  		return errors.Annotate(err, "sgdisk failed")
   185  	}
   186  	return nil
   187  }
   188  
   189  // createPartition creates a single partition (1) on the disk with the
   190  // specified device path.
   191  func createPartition(run runCommandFunc, devicePath string) error {
   192  	logger.Debugf("creating partition on %q", devicePath)
   193  	if _, err := run("sgdisk", "-n", "1:0:-1", devicePath); err != nil {
   194  		return errors.Annotate(err, "sgdisk failed")
   195  	}
   196  	return nil
   197  }
   198  
   199  func createFilesystem(run runCommandFunc, devicePath string) error {
   200  	logger.Debugf("attempting to create filesystem on %q", devicePath)
   201  	mkfscmd := "mkfs." + defaultFilesystemType
   202  	_, err := run(mkfscmd, devicePath)
   203  	if err != nil {
   204  		return errors.Annotatef(err, "%s failed", mkfscmd)
   205  	}
   206  	logger.Infof("created filesystem on %q", devicePath)
   207  	return nil
   208  }
   209  
   210  func mountFilesystem(run runCommandFunc, dirFuncs dirFuncs, devicePath, mountPoint string, readOnly bool) error {
   211  	logger.Debugf("attempting to mount filesystem on %q at %q", devicePath, mountPoint)
   212  	if err := dirFuncs.mkDirAll(mountPoint, 0755); err != nil {
   213  		return errors.Annotate(err, "creating mount point")
   214  	}
   215  	mounted, mountSource, err := isMounted(dirFuncs, mountPoint)
   216  	if err != nil {
   217  		return errors.Trace(err)
   218  	}
   219  	if mounted {
   220  		logger.Debugf("filesystem on %q already mounted at %q", mountSource, mountPoint)
   221  		return nil
   222  	}
   223  	var args []string
   224  	if readOnly {
   225  		args = append(args, "-o", "ro")
   226  	}
   227  	args = append(args, devicePath, mountPoint)
   228  	if _, err := run("mount", args...); err != nil {
   229  		return errors.Annotate(err, "mount failed")
   230  	}
   231  	logger.Infof("mounted filesystem on %q at %q", devicePath, mountPoint)
   232  
   233  	// Look for the mtab entry resulting from the mount and copy it to fstab.
   234  	// This ensures the mount is available available after a reboot.
   235  	etcDir := dirFuncs.etcDir()
   236  	mtabEntry, err := extractMtabEntry(etcDir, devicePath, mountPoint)
   237  	if err != nil {
   238  		return errors.Annotate(err, "parsing /etc/mtab")
   239  	}
   240  	if mtabEntry == "" {
   241  		return nil
   242  	}
   243  	return addFstabEntry(etcDir, devicePath, mountPoint, mtabEntry)
   244  }
   245  
   246  // extractMtabEntry returns any /etc/mtab entry for the specified
   247  // device path and mount point, or "" if none exists.
   248  func extractMtabEntry(etcDir string, devicePath, mountPoint string) (string, error) {
   249  	f, err := os.Open(filepath.Join(etcDir, "mtab"))
   250  	if os.IsNotExist(err) {
   251  		return "", nil
   252  	}
   253  	if err != nil {
   254  		return "", errors.Trace(err)
   255  	}
   256  	defer f.Close()
   257  	scanner := bufio.NewScanner(f)
   258  
   259  	for scanner.Scan() {
   260  		line := scanner.Text()
   261  		fields := strings.Fields(line)
   262  		if len(fields) >= 2 && fields[0] == devicePath && fields[1] == mountPoint {
   263  			return line, nil
   264  		}
   265  	}
   266  
   267  	if err := scanner.Err(); err != nil {
   268  		return "", errors.Trace(err)
   269  	}
   270  	return "", nil
   271  }
   272  
   273  // addFstabEntry creates an entry in /etc/fstab for the specified
   274  // device path and mount point so long as there's no existing entry already.
   275  func addFstabEntry(etcDir string, devicePath, mountPoint, entry string) error {
   276  	f, err := os.OpenFile(filepath.Join(etcDir, "fstab"), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
   277  	if err != nil {
   278  		return errors.Annotate(err, "opening /etc/fstab")
   279  	}
   280  	defer f.Close()
   281  
   282  	// Ensure there's no entry there already
   283  	scanner := bufio.NewScanner(f)
   284  	for scanner.Scan() {
   285  		line := scanner.Text()
   286  		fields := strings.Fields(line)
   287  		if len(fields) >= 2 && fields[0] == devicePath && fields[1] == mountPoint {
   288  			return nil
   289  		}
   290  	}
   291  	if err := scanner.Err(); err != nil {
   292  		return errors.Trace(err)
   293  	}
   294  
   295  	// The entry will be written at the end of the fstab file.
   296  	if _, err = f.WriteString("\n" + entry + "\n"); err != nil {
   297  		return errors.Annotate(err, "writing /etc/fstab")
   298  	}
   299  	return nil
   300  }
   301  
   302  func maybeUnmount(run runCommandFunc, dirFuncs dirFuncs, mountPoint string) error {
   303  	mounted, _, err := isMounted(dirFuncs, mountPoint)
   304  	if err != nil {
   305  		return errors.Trace(err)
   306  	}
   307  	if !mounted {
   308  		return nil
   309  	}
   310  	logger.Debugf("attempting to unmount filesystem at %q", mountPoint)
   311  	if err := removeFstabEntry(dirFuncs.etcDir(), mountPoint); err != nil {
   312  		return errors.Annotate(err, "updating /etc/fstab failed")
   313  	}
   314  	if _, err := run("umount", mountPoint); err != nil {
   315  		return errors.Annotate(err, "umount failed")
   316  	}
   317  	logger.Infof("unmounted filesystem at %q", mountPoint)
   318  	return nil
   319  }
   320  
   321  // removeFstabEntry removes any existing /etc/fstab entry for
   322  // the specified mount point.
   323  func removeFstabEntry(etcDir string, mountPoint string) error {
   324  	fstab := filepath.Join(etcDir, "fstab")
   325  	f, err := os.Open(fstab)
   326  	if os.IsNotExist(err) {
   327  		return nil
   328  	}
   329  	if err != nil {
   330  		return errors.Trace(err)
   331  	}
   332  	defer f.Close()
   333  	scanner := bufio.NewScanner(f)
   334  
   335  	// Use a tempfile in /etc and rename when done.
   336  	newFsTab, err := ioutil.TempFile(etcDir, "juju-fstab-")
   337  	if err != nil {
   338  		return errors.Trace(err)
   339  	}
   340  	defer func() {
   341  		newFsTab.Close()
   342  		os.Remove(newFsTab.Name())
   343  	}()
   344  	if err := os.Chmod(newFsTab.Name(), 0644); err != nil {
   345  		return errors.Trace(err)
   346  	}
   347  
   348  	for scanner.Scan() {
   349  		line := scanner.Text()
   350  		fields := strings.Fields(line)
   351  		if len(fields) < 2 || fields[1] != mountPoint {
   352  			_, err := newFsTab.WriteString(line + "\n")
   353  			if err != nil {
   354  				return errors.Trace(err)
   355  			}
   356  		}
   357  	}
   358  	if err := scanner.Err(); err != nil {
   359  		return errors.Trace(err)
   360  	}
   361  
   362  	return os.Rename(newFsTab.Name(), fstab)
   363  }
   364  
   365  func isMounted(dirFuncs dirFuncs, mountPoint string) (bool, string, error) {
   366  	mountPointParent := filepath.Dir(mountPoint)
   367  	parentSource, err := dirFuncs.mountPointSource(mountPointParent)
   368  	if err != nil {
   369  		return false, "", errors.Trace(err)
   370  	}
   371  	source, err := dirFuncs.mountPointSource(mountPoint)
   372  	if err != nil {
   373  		return false, "", errors.Trace(err)
   374  	}
   375  	if source != parentSource {
   376  		// Already mounted.
   377  		return true, source, nil
   378  	}
   379  	return false, "", nil
   380  }
   381  
   382  // devicePath returns the device path for the given block device.
   383  func devicePath(dev storage.BlockDevice) string {
   384  	return path.Join("/dev", dev.DeviceName)
   385  }
   386  
   387  // partitionDevicePath returns the device path for the first (and only)
   388  // partition of the disk with the specified device path.
   389  func partitionDevicePath(devicePath string) string {
   390  	return devicePath + "1"
   391  }
   392  
   393  // isDiskDevice reports whether or not the device is a full disk, as opposed
   394  // to a partition or a loop device. We create a partition on disks to contain
   395  // filesystems.
   396  func isDiskDevice(devicePath string) bool {
   397  	var last rune
   398  	for _, r := range devicePath {
   399  		last = r
   400  	}
   401  	return !unicode.IsDigit(last)
   402  }