github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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  	"fmt"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"strings"
    13  	"unicode"
    14  
    15  	"github.com/juju/collections/set"
    16  	"github.com/juju/errors"
    17  	"github.com/juju/names/v5"
    18  
    19  	"github.com/juju/juju/environs/context"
    20  	"github.com/juju/juju/storage"
    21  )
    22  
    23  const (
    24  	// defaultFilesystemType is the default filesystem type
    25  	// to create for volume-backed managed filesystems.
    26  	defaultFilesystemType = "ext4"
    27  )
    28  
    29  // managedFilesystemSource is an implementation of storage.FilesystemSource
    30  // that manages filesystems on volumes attached to the host machine.
    31  //
    32  // managedFilesystemSource is expected to be called from a single goroutine.
    33  type managedFilesystemSource struct {
    34  	run                runCommandFunc
    35  	dirFuncs           dirFuncs
    36  	volumeBlockDevices map[names.VolumeTag]storage.BlockDevice
    37  	filesystems        map[names.FilesystemTag]storage.Filesystem
    38  }
    39  
    40  // NewManagedFilesystemSource returns a storage.FilesystemSource that manages
    41  // filesystems on block devices on the host machine.
    42  //
    43  // The parameters are maps that the caller will update with information about
    44  // block devices and filesystems created by the source. The caller must not
    45  // update the maps during calls to the source's methods.
    46  func NewManagedFilesystemSource(
    47  	volumeBlockDevices map[names.VolumeTag]storage.BlockDevice,
    48  	filesystems map[names.FilesystemTag]storage.Filesystem,
    49  ) storage.FilesystemSource {
    50  	return &managedFilesystemSource{
    51  		logAndExec,
    52  		&osDirFuncs{run: logAndExec},
    53  		volumeBlockDevices, filesystems,
    54  	}
    55  }
    56  
    57  // ValidateFilesystemParams is defined on storage.FilesystemSource.
    58  func (s *managedFilesystemSource) ValidateFilesystemParams(arg storage.FilesystemParams) error {
    59  	// NOTE(axw) the parameters may be for destroying a filesystem, which
    60  	// may be called when the backing volume is detached from the machine.
    61  	// We must not perform any validation here that would fail if the
    62  	// volume is detached.
    63  	return nil
    64  }
    65  
    66  func (s *managedFilesystemSource) backingVolumeBlockDevice(v names.VolumeTag) (storage.BlockDevice, error) {
    67  	blockDevice, ok := s.volumeBlockDevices[v]
    68  	if !ok {
    69  		return storage.BlockDevice{}, errors.Errorf(
    70  			"backing-volume %s is not yet attached", v.Id(),
    71  		)
    72  	}
    73  	return blockDevice, nil
    74  }
    75  
    76  // CreateFilesystems is defined on storage.FilesystemSource.
    77  func (s *managedFilesystemSource) CreateFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemParams) ([]storage.CreateFilesystemsResult, error) {
    78  	results := make([]storage.CreateFilesystemsResult, len(args))
    79  	for i, arg := range args {
    80  		filesystem, err := s.createFilesystem(arg)
    81  		if err != nil {
    82  			results[i].Error = err
    83  			continue
    84  		}
    85  		results[i].Filesystem = filesystem
    86  	}
    87  	return results, nil
    88  }
    89  
    90  func (s *managedFilesystemSource) createFilesystem(arg storage.FilesystemParams) (*storage.Filesystem, error) {
    91  	blockDevice, err := s.backingVolumeBlockDevice(arg.Volume)
    92  	if err != nil {
    93  		return nil, errors.Trace(err)
    94  	}
    95  	devicePath := devicePath(blockDevice)
    96  	if isDiskDevice(devicePath) {
    97  		if err := destroyPartitions(s.run, devicePath); err != nil {
    98  			return nil, errors.Trace(err)
    99  		}
   100  		if err := createPartition(s.run, devicePath); err != nil {
   101  			return nil, errors.Trace(err)
   102  		}
   103  		devicePath = partitionDevicePath(devicePath)
   104  	}
   105  	if err := createFilesystem(s.run, devicePath); err != nil {
   106  		return nil, errors.Trace(err)
   107  	}
   108  	return &storage.Filesystem{
   109  		arg.Tag,
   110  		arg.Volume,
   111  		storage.FilesystemInfo{
   112  			arg.Tag.String(),
   113  			blockDevice.Size,
   114  		},
   115  	}, nil
   116  }
   117  
   118  // DestroyFilesystems is defined on storage.FilesystemSource.
   119  func (s *managedFilesystemSource) DestroyFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) {
   120  	// DestroyFilesystems is a no-op; there is nothing to destroy,
   121  	// since the filesystem is just data on a volume. The volume
   122  	// is destroyed separately.
   123  	return make([]error, len(filesystemIds)), nil
   124  }
   125  
   126  // ReleaseFilesystems is defined on storage.FilesystemSource.
   127  func (s *managedFilesystemSource) ReleaseFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) {
   128  	return make([]error, len(filesystemIds)), nil
   129  }
   130  
   131  // AttachFilesystems is defined on storage.FilesystemSource.
   132  func (s *managedFilesystemSource) AttachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]storage.AttachFilesystemsResult, error) {
   133  	results := make([]storage.AttachFilesystemsResult, len(args))
   134  	for i, arg := range args {
   135  		attachment, err := s.attachFilesystem(arg)
   136  		if err != nil {
   137  			results[i].Error = err
   138  			continue
   139  		}
   140  		results[i].FilesystemAttachment = attachment
   141  	}
   142  	return results, nil
   143  }
   144  
   145  func (s *managedFilesystemSource) attachFilesystem(arg storage.FilesystemAttachmentParams) (*storage.FilesystemAttachment, error) {
   146  	filesystem, ok := s.filesystems[arg.Filesystem]
   147  	if !ok {
   148  		return nil, errors.Errorf("filesystem %v is not yet provisioned", arg.Filesystem.Id())
   149  	}
   150  	blockDevice, err := s.backingVolumeBlockDevice(filesystem.Volume)
   151  	if err != nil {
   152  		return nil, errors.Trace(err)
   153  	}
   154  	devicePath := devicePath(blockDevice)
   155  	if isDiskDevice(devicePath) {
   156  		devicePath = partitionDevicePath(devicePath)
   157  	}
   158  	if err := mountFilesystem(s.run, s.dirFuncs, devicePath, blockDevice.UUID, arg.Path, arg.ReadOnly); err != nil {
   159  		return nil, errors.Trace(err)
   160  	}
   161  	return &storage.FilesystemAttachment{
   162  		arg.Filesystem,
   163  		arg.Machine,
   164  		storage.FilesystemAttachmentInfo{
   165  			arg.Path,
   166  			arg.ReadOnly,
   167  		},
   168  	}, nil
   169  }
   170  
   171  // DetachFilesystems is defined on storage.FilesystemSource.
   172  func (s *managedFilesystemSource) DetachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]error, error) {
   173  	results := make([]error, len(args))
   174  	for i, arg := range args {
   175  		if err := maybeUnmount(s.run, s.dirFuncs, arg.Path); err != nil {
   176  			results[i] = err
   177  		}
   178  	}
   179  	return results, nil
   180  }
   181  
   182  func destroyPartitions(run runCommandFunc, devicePath string) error {
   183  	logger.Debugf("destroying partitions on %q", devicePath)
   184  	if _, err := run("sgdisk", "--zap-all", devicePath); err != nil {
   185  		return errors.Annotate(err, "sgdisk failed")
   186  	}
   187  	return nil
   188  }
   189  
   190  // createPartition creates a single partition (1) on the disk with the
   191  // specified device path.
   192  func createPartition(run runCommandFunc, devicePath string) error {
   193  	logger.Debugf("creating partition on %q", devicePath)
   194  	if _, err := run("sgdisk", "-n", "1:0:-1", devicePath); err != nil {
   195  		return errors.Annotate(err, "sgdisk failed")
   196  	}
   197  	return nil
   198  }
   199  
   200  func createFilesystem(run runCommandFunc, devicePath string) error {
   201  	logger.Debugf("attempting to create filesystem on %q", devicePath)
   202  	mkfscmd := "mkfs." + defaultFilesystemType
   203  	_, err := run(mkfscmd, devicePath)
   204  	if err != nil {
   205  		return errors.Annotatef(err, "%s failed", mkfscmd)
   206  	}
   207  	logger.Infof("created filesystem on %q", devicePath)
   208  	return nil
   209  }
   210  
   211  func mountFilesystem(run runCommandFunc, dirFuncs dirFuncs, devicePath, UUID, mountPoint string, readOnly bool) error {
   212  	logger.Debugf("attempting to mount filesystem on %q at %q", devicePath, mountPoint)
   213  	if err := dirFuncs.mkDirAll(mountPoint, 0755); err != nil {
   214  		return errors.Annotate(err, "creating mount point")
   215  	}
   216  	mounted, mountSource, err := isMounted(dirFuncs, mountPoint)
   217  	if err != nil {
   218  		return errors.Trace(err)
   219  	}
   220  	if mounted {
   221  		logger.Debugf("filesystem on %q already mounted at %q", mountSource, mountPoint)
   222  	} else {
   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.Debugf("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 ensureFstabEntry(etcDir, devicePath, UUID, 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  // ensureFstabEntry 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 ensureFstabEntry(etcDir, devicePath, UUID, mountPoint, entry string) error {
   276  	f, err := os.Open(filepath.Join(etcDir, "fstab"))
   277  	if err != nil && !os.IsNotExist(err) {
   278  		return errors.Annotate(err, "opening /etc/fstab")
   279  	}
   280  	if err == nil {
   281  		defer f.Close()
   282  	}
   283  
   284  	newFsTab, err := os.CreateTemp(etcDir, "juju-fstab-")
   285  	if err != nil {
   286  		return errors.Trace(err)
   287  	}
   288  	defer func() {
   289  		_ = newFsTab.Close()
   290  		_ = os.Remove(newFsTab.Name())
   291  	}()
   292  	if err := os.Chmod(newFsTab.Name(), 0644); err != nil {
   293  		return errors.Trace(err)
   294  	}
   295  
   296  	// Add nofail if not there already
   297  	resultFields := strings.Fields(entry)
   298  	options := set.NewStrings()
   299  	if len(resultFields) >= 4 {
   300  		options = set.NewStrings(strings.Split(resultFields[3], ",")...)
   301  	}
   302  	if !options.Contains("nofail") {
   303  		options.Add("nofail")
   304  		opts := strings.Join(options.SortedValues(), ",")
   305  		if len(resultFields) >= 4 {
   306  			resultFields[3] = opts
   307  		} else {
   308  			resultFields = append(resultFields, opts)
   309  		}
   310  	}
   311  
   312  	uuidField := "UUID=" + UUID
   313  	addNewEntry := true
   314  	// Scan all the fstab lines, searching for one
   315  	// which describes the entry we want to create.
   316  	scanner := bufio.NewScanner(f)
   317  	for f != nil && scanner.Scan() {
   318  		line := scanner.Text()
   319  		fields := strings.Fields(line)
   320  		if len(fields) < 2 || fields[1] != mountPoint {
   321  			goto writeLine
   322  		}
   323  		// Is the line the UUID based mount entry we want.
   324  		if fields[0] == uuidField {
   325  			addNewEntry = false
   326  			goto writeLine
   327  		}
   328  		// Is the line for some other entry.
   329  		if fields[0] != devicePath {
   330  			goto writeLine
   331  		}
   332  		// We have a match, if UUID is not yet known, retain the line.
   333  		if UUID == "" {
   334  			addNewEntry = false
   335  			goto writeLine
   336  		}
   337  		continue
   338  	writeLine:
   339  		_, err := newFsTab.WriteString(line + "\n")
   340  		if err != nil {
   341  			return errors.Trace(err)
   342  		}
   343  	}
   344  	if err := scanner.Err(); err != nil {
   345  		return errors.Trace(err)
   346  	}
   347  
   348  	if addNewEntry {
   349  		if UUID != "" {
   350  			if len(resultFields) >= 2 { // just being defensive, check should never fail.
   351  				_, err := newFsTab.WriteString(fmt.Sprintf("# %s was on %s during installation\n", resultFields[1], resultFields[0]))
   352  				if err != nil {
   353  					return errors.Trace(err)
   354  				}
   355  			}
   356  			resultFields[0] = uuidField
   357  		}
   358  		_, err := newFsTab.WriteString(strings.Join(resultFields, " ") + "\n")
   359  		if err != nil {
   360  			return errors.Trace(err)
   361  		}
   362  
   363  	}
   364  	return os.Rename(newFsTab.Name(), filepath.Join(etcDir, "fstab"))
   365  }
   366  
   367  func maybeUnmount(run runCommandFunc, dirFuncs dirFuncs, mountPoint string) error {
   368  	mounted, _, err := isMounted(dirFuncs, mountPoint)
   369  	if err != nil {
   370  		return errors.Trace(err)
   371  	}
   372  	if !mounted {
   373  		return nil
   374  	}
   375  	logger.Debugf("attempting to unmount filesystem at %q", mountPoint)
   376  	if err := removeFstabEntry(dirFuncs.etcDir(), mountPoint); err != nil {
   377  		return errors.Annotate(err, "updating /etc/fstab failed")
   378  	}
   379  	if _, err := run("umount", mountPoint); err != nil {
   380  		return errors.Annotate(err, "umount failed")
   381  	}
   382  	logger.Infof("unmounted filesystem at %q", mountPoint)
   383  	return nil
   384  }
   385  
   386  // removeFstabEntry removes any existing /etc/fstab entry for
   387  // the specified mount point.
   388  func removeFstabEntry(etcDir string, mountPoint string) error {
   389  	fstab := filepath.Join(etcDir, "fstab")
   390  	f, err := os.Open(fstab)
   391  	if os.IsNotExist(err) {
   392  		return nil
   393  	}
   394  	if err != nil {
   395  		return errors.Trace(err)
   396  	}
   397  	defer f.Close()
   398  	scanner := bufio.NewScanner(f)
   399  
   400  	// Use a tempfile in /etc and rename when done.
   401  	newFsTab, err := os.CreateTemp(etcDir, "juju-fstab-")
   402  	if err != nil {
   403  		return errors.Trace(err)
   404  	}
   405  	defer func() {
   406  		_ = newFsTab.Close()
   407  		_ = os.Remove(newFsTab.Name())
   408  	}()
   409  	if err := os.Chmod(newFsTab.Name(), 0644); err != nil {
   410  		return errors.Trace(err)
   411  	}
   412  
   413  	for scanner.Scan() {
   414  		line := scanner.Text()
   415  		fields := strings.Fields(line)
   416  		if len(fields) < 2 || fields[1] != mountPoint {
   417  			_, err := newFsTab.WriteString(line + "\n")
   418  			if err != nil {
   419  				return errors.Trace(err)
   420  			}
   421  		}
   422  	}
   423  	if err := scanner.Err(); err != nil {
   424  		return errors.Trace(err)
   425  	}
   426  
   427  	return os.Rename(newFsTab.Name(), fstab)
   428  }
   429  
   430  func isMounted(dirFuncs dirFuncs, mountPoint string) (bool, string, error) {
   431  	mountPointParent := filepath.Dir(mountPoint)
   432  	parentSource, err := dirFuncs.mountPointSource(mountPointParent)
   433  	if err != nil {
   434  		return false, "", errors.Trace(err)
   435  	}
   436  	source, err := dirFuncs.mountPointSource(mountPoint)
   437  	if err != nil {
   438  		return false, "", errors.Trace(err)
   439  	}
   440  	if source != parentSource {
   441  		// Already mounted.
   442  		return true, source, nil
   443  	}
   444  	return false, "", nil
   445  }
   446  
   447  // devicePath returns the device path for the given block device.
   448  func devicePath(dev storage.BlockDevice) string {
   449  	return path.Join("/dev", dev.DeviceName)
   450  }
   451  
   452  // partitionDevicePath returns the device path for the first (and only)
   453  // partition of the disk with the specified device path.
   454  func partitionDevicePath(devicePath string) string {
   455  	return devicePath + "1"
   456  }
   457  
   458  // isDiskDevice reports whether or not the device is a full disk, as opposed
   459  // to a partition or a loop device. We create a partition on disks to contain
   460  // filesystems.
   461  func isDiskDevice(devicePath string) bool {
   462  	var last rune
   463  	for _, r := range devicePath {
   464  		last = r
   465  	}
   466  	return !unicode.IsDigit(last)
   467  }