github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/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  	"path"
     8  	"path/filepath"
     9  	"unicode"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/names"
    13  
    14  	"github.com/juju/juju/storage"
    15  )
    16  
    17  const (
    18  	// defaultFilesystemType is the default filesystem type
    19  	// to create for volume-backed managed filesystems.
    20  	defaultFilesystemType = "ext4"
    21  )
    22  
    23  // managedFilesystemSource is an implementation of storage.FilesystemSource
    24  // that manages filesystems on volumes attached to the host machine.
    25  //
    26  // managedFilesystemSource is expected to be called from a single goroutine.
    27  type managedFilesystemSource struct {
    28  	run                runCommandFunc
    29  	dirFuncs           dirFuncs
    30  	volumeBlockDevices map[names.VolumeTag]storage.BlockDevice
    31  	filesystems        map[names.FilesystemTag]storage.Filesystem
    32  }
    33  
    34  // NewManagedFilesystemSource returns a storage.FilesystemSource that manages
    35  // filesystems on block devices on the host machine.
    36  //
    37  // The parameters are maps that the caller will update with information about
    38  // block devices and filesystems created by the source. The caller must not
    39  // update the maps during calls to the source's methods.
    40  func NewManagedFilesystemSource(
    41  	volumeBlockDevices map[names.VolumeTag]storage.BlockDevice,
    42  	filesystems map[names.FilesystemTag]storage.Filesystem,
    43  ) storage.FilesystemSource {
    44  	return &managedFilesystemSource{
    45  		logAndExec,
    46  		&osDirFuncs{logAndExec},
    47  		volumeBlockDevices, filesystems,
    48  	}
    49  }
    50  
    51  // ValidateFilesystemParams is defined on storage.FilesystemSource.
    52  func (s *managedFilesystemSource) ValidateFilesystemParams(arg storage.FilesystemParams) error {
    53  	if _, err := s.backingVolumeBlockDevice(arg.Volume); err != nil {
    54  		return errors.Trace(err)
    55  	}
    56  	return nil
    57  }
    58  
    59  func (s *managedFilesystemSource) backingVolumeBlockDevice(v names.VolumeTag) (storage.BlockDevice, error) {
    60  	blockDevice, ok := s.volumeBlockDevices[v]
    61  	if !ok {
    62  		return storage.BlockDevice{}, errors.Errorf(
    63  			"backing-volume %s is not yet attached", v.Id(),
    64  		)
    65  	}
    66  	return blockDevice, nil
    67  }
    68  
    69  // CreateFilesystems is defined on storage.FilesystemSource.
    70  func (s *managedFilesystemSource) CreateFilesystems(args []storage.FilesystemParams) ([]storage.CreateFilesystemsResult, error) {
    71  	results := make([]storage.CreateFilesystemsResult, len(args))
    72  	for i, arg := range args {
    73  		filesystem, err := s.createFilesystem(arg)
    74  		if err != nil {
    75  			results[i].Error = err
    76  			continue
    77  		}
    78  		results[i].Filesystem = filesystem
    79  	}
    80  	return results, nil
    81  }
    82  
    83  func (s *managedFilesystemSource) createFilesystem(arg storage.FilesystemParams) (*storage.Filesystem, error) {
    84  	blockDevice, err := s.backingVolumeBlockDevice(arg.Volume)
    85  	if err != nil {
    86  		return nil, errors.Trace(err)
    87  	}
    88  	devicePath := devicePath(blockDevice)
    89  	if isDiskDevice(devicePath) {
    90  		if err := destroyPartitions(s.run, devicePath); err != nil {
    91  			return nil, errors.Trace(err)
    92  		}
    93  		if err := createPartition(s.run, devicePath); err != nil {
    94  			return nil, errors.Trace(err)
    95  		}
    96  		devicePath = partitionDevicePath(devicePath)
    97  	}
    98  	if err := createFilesystem(s.run, devicePath); err != nil {
    99  		return nil, errors.Trace(err)
   100  	}
   101  	return &storage.Filesystem{
   102  		arg.Tag,
   103  		arg.Volume,
   104  		storage.FilesystemInfo{
   105  			arg.Tag.String(),
   106  			blockDevice.Size,
   107  		},
   108  	}, nil
   109  }
   110  
   111  // DestroyFilesystems is defined on storage.FilesystemSource.
   112  func (s *managedFilesystemSource) DestroyFilesystems(filesystemIds []string) ([]error, error) {
   113  	// DestroyFilesystems is a no-op; there is nothing to destroy,
   114  	// since the filesystem is just data on a volume. The volume
   115  	// is destroyed separately.
   116  	return make([]error, len(filesystemIds)), nil
   117  }
   118  
   119  // AttachFilesystems is defined on storage.FilesystemSource.
   120  func (s *managedFilesystemSource) AttachFilesystems(args []storage.FilesystemAttachmentParams) ([]storage.AttachFilesystemsResult, error) {
   121  	results := make([]storage.AttachFilesystemsResult, len(args))
   122  	for i, arg := range args {
   123  		attachment, err := s.attachFilesystem(arg)
   124  		if err != nil {
   125  			results[i].Error = err
   126  			continue
   127  		}
   128  		results[i].FilesystemAttachment = attachment
   129  	}
   130  	return results, nil
   131  }
   132  
   133  func (s *managedFilesystemSource) attachFilesystem(arg storage.FilesystemAttachmentParams) (*storage.FilesystemAttachment, error) {
   134  	filesystem, ok := s.filesystems[arg.Filesystem]
   135  	if !ok {
   136  		return nil, errors.Errorf("filesystem %v is not yet provisioned", arg.Filesystem.Id())
   137  	}
   138  	blockDevice, err := s.backingVolumeBlockDevice(filesystem.Volume)
   139  	if err != nil {
   140  		return nil, errors.Trace(err)
   141  	}
   142  	devicePath := devicePath(blockDevice)
   143  	if isDiskDevice(devicePath) {
   144  		devicePath = partitionDevicePath(devicePath)
   145  	}
   146  	if err := mountFilesystem(s.run, s.dirFuncs, devicePath, arg.Path, arg.ReadOnly); err != nil {
   147  		return nil, errors.Trace(err)
   148  	}
   149  	return &storage.FilesystemAttachment{
   150  		arg.Filesystem,
   151  		arg.Machine,
   152  		storage.FilesystemAttachmentInfo{
   153  			arg.Path,
   154  			arg.ReadOnly,
   155  		},
   156  	}, nil
   157  }
   158  
   159  // DetachFilesystems is defined on storage.FilesystemSource.
   160  func (s *managedFilesystemSource) DetachFilesystems(args []storage.FilesystemAttachmentParams) ([]error, error) {
   161  	results := make([]error, len(args))
   162  	for i, arg := range args {
   163  		if err := maybeUnmount(s.run, s.dirFuncs, arg.Path); err != nil {
   164  			results[i] = err
   165  		}
   166  	}
   167  	return results, nil
   168  }
   169  
   170  func destroyPartitions(run runCommandFunc, devicePath string) error {
   171  	logger.Debugf("destroying partitions on %q", devicePath)
   172  	if _, err := run("sgdisk", "--zap-all", devicePath); err != nil {
   173  		return errors.Annotate(err, "sgdisk failed")
   174  	}
   175  	return nil
   176  }
   177  
   178  // createPartition creates a single partition (1) on the disk with the
   179  // specified device path.
   180  func createPartition(run runCommandFunc, devicePath string) error {
   181  	logger.Debugf("creating partition on %q", devicePath)
   182  	if _, err := run("sgdisk", "-n", "1:0:-1", devicePath); err != nil {
   183  		return errors.Annotate(err, "sgdisk failed")
   184  	}
   185  	return nil
   186  }
   187  
   188  func createFilesystem(run runCommandFunc, devicePath string) error {
   189  	logger.Debugf("attempting to create filesystem on %q", devicePath)
   190  	mkfscmd := "mkfs." + defaultFilesystemType
   191  	_, err := run(mkfscmd, devicePath)
   192  	if err != nil {
   193  		return errors.Annotatef(err, "%s failed", mkfscmd)
   194  	}
   195  	logger.Infof("created filesystem on %q", devicePath)
   196  	return nil
   197  }
   198  
   199  func mountFilesystem(run runCommandFunc, dirFuncs dirFuncs, devicePath, mountPoint string, readOnly bool) error {
   200  	logger.Debugf("attempting to mount filesystem on %q at %q", devicePath, mountPoint)
   201  	if err := dirFuncs.mkDirAll(mountPoint, 0755); err != nil {
   202  		return errors.Annotate(err, "creating mount point")
   203  	}
   204  	mounted, mountSource, err := isMounted(dirFuncs, mountPoint)
   205  	if err != nil {
   206  		return errors.Trace(err)
   207  	}
   208  	if mounted {
   209  		logger.Debugf("filesystem on %q already mounted at %q", mountSource, mountPoint)
   210  		return nil
   211  	}
   212  	var args []string
   213  	if readOnly {
   214  		args = append(args, "-o", "ro")
   215  	}
   216  	args = append(args, devicePath, mountPoint)
   217  	if _, err := run("mount", args...); err != nil {
   218  		return errors.Annotate(err, "mount failed")
   219  	}
   220  	logger.Infof("mounted filesystem on %q at %q", devicePath, mountPoint)
   221  	return nil
   222  }
   223  
   224  func maybeUnmount(run runCommandFunc, dirFuncs dirFuncs, mountPoint string) error {
   225  	mounted, _, err := isMounted(dirFuncs, mountPoint)
   226  	if err != nil {
   227  		return errors.Trace(err)
   228  	}
   229  	if !mounted {
   230  		return nil
   231  	}
   232  	logger.Debugf("attempting to unmount filesystem at %q", mountPoint)
   233  	if _, err := run("umount", mountPoint); err != nil {
   234  		return errors.Annotate(err, "umount failed")
   235  	}
   236  	logger.Infof("unmounted filesystem at %q", mountPoint)
   237  	return nil
   238  }
   239  
   240  func isMounted(dirFuncs dirFuncs, mountPoint string) (bool, string, error) {
   241  	mountPointParent := filepath.Dir(mountPoint)
   242  	parentSource, err := dirFuncs.mountPointSource(mountPointParent)
   243  	if err != nil {
   244  		return false, "", errors.Trace(err)
   245  	}
   246  	source, err := dirFuncs.mountPointSource(mountPoint)
   247  	if err != nil {
   248  		return false, "", errors.Trace(err)
   249  	}
   250  	if source != parentSource {
   251  		// Already mounted.
   252  		return true, source, nil
   253  	}
   254  	return false, "", nil
   255  }
   256  
   257  // devicePath returns the device path for the given block device.
   258  func devicePath(dev storage.BlockDevice) string {
   259  	return path.Join("/dev", dev.DeviceName)
   260  }
   261  
   262  // partitionDevicePath returns the device path for the first (and only)
   263  // partition of the disk with the specified device path.
   264  func partitionDevicePath(devicePath string) string {
   265  	return devicePath + "1"
   266  }
   267  
   268  // isDiskDevice reports whether or not the device is a full disk, as opposed
   269  // to a partition or a loop device. We create a partition on disks to contain
   270  // filesystems.
   271  func isDiskDevice(devicePath string) bool {
   272  	var last rune
   273  	for _, r := range devicePath {
   274  		last = r
   275  	}
   276  	return !unicode.IsDigit(last)
   277  }