github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/storage/provider/rootfs.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  	"os"
     8  	"path/filepath"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/names/v5"
    12  
    13  	"github.com/juju/juju/environs/context"
    14  	"github.com/juju/juju/storage"
    15  )
    16  
    17  const (
    18  	RootfsProviderType = storage.ProviderType("rootfs")
    19  )
    20  
    21  // rootfsProviders create storage sources which provide access to filesystems.
    22  type rootfsProvider struct {
    23  	// run is a function type used for running commands on the local machine.
    24  	run runCommandFunc
    25  }
    26  
    27  var (
    28  	_ storage.Provider = (*rootfsProvider)(nil)
    29  )
    30  
    31  func (p *rootfsProvider) ValidateForK8s(attributes map[string]any) error {
    32  	if attributes == nil {
    33  		return nil
    34  	}
    35  	// check the configuration
    36  	return checkK8sConfig(attributes)
    37  }
    38  
    39  // ValidateConfig is defined on the Provider interface.
    40  func (p *rootfsProvider) ValidateConfig(cfg *storage.Config) error {
    41  	// Rootfs provider has no configuration.
    42  	return nil
    43  }
    44  
    45  // validateFullConfig validates a fully-constructed storage config,
    46  // combining the user-specified config and any internally specified
    47  // config.
    48  func (p *rootfsProvider) validateFullConfig(cfg *storage.Config) error {
    49  	if err := p.ValidateConfig(cfg); err != nil {
    50  		return err
    51  	}
    52  	storageDir, ok := cfg.ValueString(storage.ConfigStorageDir)
    53  	if !ok || storageDir == "" {
    54  		return errors.New("storage directory not specified")
    55  	}
    56  	return nil
    57  }
    58  
    59  // VolumeSource is defined on the Provider interface.
    60  func (p *rootfsProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) {
    61  	return nil, errors.NotSupportedf("volumes")
    62  }
    63  
    64  // FilesystemSource is defined on the Provider interface.
    65  func (p *rootfsProvider) FilesystemSource(sourceConfig *storage.Config) (storage.FilesystemSource, error) {
    66  	if err := p.validateFullConfig(sourceConfig); err != nil {
    67  		return nil, err
    68  	}
    69  	// storageDir is validated by validateFullConfig.
    70  	storageDir, _ := sourceConfig.ValueString(storage.ConfigStorageDir)
    71  	return &rootfsFilesystemSource{
    72  		&osDirFuncs{run: p.run},
    73  		p.run,
    74  		storageDir,
    75  	}, nil
    76  }
    77  
    78  // Supports is defined on the Provider interface.
    79  func (*rootfsProvider) Supports(k storage.StorageKind) bool {
    80  	return k == storage.StorageKindFilesystem
    81  }
    82  
    83  // Scope is defined on the Provider interface.
    84  func (*rootfsProvider) Scope() storage.Scope {
    85  	return storage.ScopeMachine
    86  }
    87  
    88  // Dynamic is defined on the Provider interface.
    89  func (*rootfsProvider) Dynamic() bool {
    90  	return true
    91  }
    92  
    93  // Releasable is defined on the Provider interface.
    94  func (*rootfsProvider) Releasable() bool {
    95  	return false
    96  }
    97  
    98  // DefaultPools is defined on the Provider interface.
    99  func (*rootfsProvider) DefaultPools() []*storage.Config {
   100  	return nil
   101  }
   102  
   103  type rootfsFilesystemSource struct {
   104  	dirFuncs   dirFuncs
   105  	run        runCommandFunc
   106  	storageDir string
   107  }
   108  
   109  // ensureDir ensures the specified path is a directory, or
   110  // if it does not exist, that a directory can be created there.
   111  func ensureDir(d dirFuncs, path string) error {
   112  	// If path already exists, we check that it is empty.
   113  	// It is up to the storage provisioner to ensure that any
   114  	// shared storage constraints and attachments with the same
   115  	// path are validated etc. So the check here is more a sanity check.
   116  	fi, err := d.lstat(path)
   117  	if err == nil {
   118  		if !fi.IsDir() {
   119  			return errors.Errorf("path %q must be a directory", path)
   120  		}
   121  		return nil
   122  	}
   123  	if !os.IsNotExist(err) {
   124  		return errors.Trace(err)
   125  	}
   126  	if err := d.mkDirAll(path, 0755); err != nil {
   127  		return errors.Annotate(err, "could not create directory")
   128  	}
   129  	return nil
   130  }
   131  
   132  // ensureEmptyDir ensures the specified directory is empty.
   133  func ensureEmptyDir(d dirFuncs, path string) error {
   134  	fileCount, err := d.fileCount(path)
   135  	if err != nil {
   136  		return errors.Annotate(err, "could not read directory")
   137  	}
   138  	if fileCount > 0 {
   139  		return errors.Errorf("%q is not empty", path)
   140  	}
   141  	return nil
   142  }
   143  
   144  var _ storage.FilesystemSource = (*rootfsFilesystemSource)(nil)
   145  
   146  // ValidateFilesystemParams is defined on the FilesystemSource interface.
   147  func (s *rootfsFilesystemSource) ValidateFilesystemParams(params storage.FilesystemParams) error {
   148  	// ValidateFilesystemParams may be called on a machine other than the
   149  	// machine where the filesystem will be mounted, so we cannot check
   150  	// available size until we get to CreateFilesystem.
   151  	return nil
   152  }
   153  
   154  // CreateFilesystems is defined on the FilesystemSource interface.
   155  func (s *rootfsFilesystemSource) CreateFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemParams) ([]storage.CreateFilesystemsResult, error) {
   156  	results := make([]storage.CreateFilesystemsResult, len(args))
   157  	for i, arg := range args {
   158  		filesystem, err := s.createFilesystem(arg)
   159  		if err != nil {
   160  			results[i].Error = err
   161  			continue
   162  		}
   163  		results[i].Filesystem = filesystem
   164  	}
   165  	return results, nil
   166  }
   167  
   168  func (s *rootfsFilesystemSource) createFilesystem(params storage.FilesystemParams) (*storage.Filesystem, error) {
   169  	if err := s.ValidateFilesystemParams(params); err != nil {
   170  		return nil, errors.Trace(err)
   171  	}
   172  	path := filepath.Join(s.storageDir, params.Tag.Id())
   173  	if err := ensureDir(s.dirFuncs, path); err != nil {
   174  		return nil, errors.Trace(err)
   175  	}
   176  	if err := ensureEmptyDir(s.dirFuncs, path); err != nil {
   177  		return nil, errors.Trace(err)
   178  	}
   179  	sizeInMiB, err := s.dirFuncs.calculateSize(s.storageDir)
   180  	if err != nil {
   181  		os.Remove(path)
   182  		return nil, errors.Trace(err)
   183  	}
   184  	if sizeInMiB < params.Size {
   185  		os.Remove(path)
   186  		return nil, errors.Errorf("filesystem is not big enough (%dM < %dM)", sizeInMiB, params.Size)
   187  	}
   188  	return &storage.Filesystem{
   189  		params.Tag,
   190  		names.VolumeTag{},
   191  		storage.FilesystemInfo{
   192  			FilesystemId: params.Tag.Id(),
   193  			Size:         sizeInMiB,
   194  		},
   195  	}, nil
   196  }
   197  
   198  // DestroyFilesystems is defined on the FilesystemSource interface.
   199  func (s *rootfsFilesystemSource) DestroyFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) {
   200  	// DestroyFilesystems is a no-op; we leave the storage directory
   201  	// in tact for post-mortems and such.
   202  	return make([]error, len(filesystemIds)), nil
   203  }
   204  
   205  // ReleaseFilesystems is defined on the FilesystemSource interface.
   206  func (s *rootfsFilesystemSource) ReleaseFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) {
   207  	return make([]error, len(filesystemIds)), nil
   208  }
   209  
   210  // AttachFilesystems is defined on the FilesystemSource interface.
   211  func (s *rootfsFilesystemSource) AttachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]storage.AttachFilesystemsResult, error) {
   212  	results := make([]storage.AttachFilesystemsResult, len(args))
   213  	for i, arg := range args {
   214  		attachment, err := s.attachFilesystem(arg)
   215  		if err != nil {
   216  			results[i].Error = err
   217  			continue
   218  		}
   219  		results[i].FilesystemAttachment = attachment
   220  	}
   221  	return results, nil
   222  }
   223  
   224  func (s *rootfsFilesystemSource) attachFilesystem(arg storage.FilesystemAttachmentParams) (*storage.FilesystemAttachment, error) {
   225  	mountPoint := arg.Path
   226  	if mountPoint == "" {
   227  		return nil, errNoMountPoint
   228  	}
   229  	// The filesystem is created at <storage-dir>/<storage-id>.
   230  	// If it is different to the attachment path, bind mount.
   231  	if err := s.mount(arg.Filesystem, mountPoint); err != nil {
   232  		return nil, err
   233  	}
   234  	return &storage.FilesystemAttachment{
   235  		arg.Filesystem,
   236  		arg.Machine,
   237  		storage.FilesystemAttachmentInfo{
   238  			Path: mountPoint,
   239  		},
   240  	}, nil
   241  }
   242  
   243  func (s *rootfsFilesystemSource) mount(tag names.FilesystemTag, target string) error {
   244  	fsPath := filepath.Join(s.storageDir, tag.Id())
   245  	if target == fsPath {
   246  		return nil
   247  	}
   248  	logger.Debugf("mounting filesystem %q at %q", fsPath, target)
   249  
   250  	if err := ensureDir(s.dirFuncs, target); err != nil {
   251  		return errors.Trace(err)
   252  	}
   253  
   254  	mounted, err := s.tryBindMount(fsPath, target)
   255  	if err != nil {
   256  		return errors.Trace(err)
   257  	}
   258  	if mounted {
   259  		return nil
   260  	}
   261  	// We couldn't bind-mount over the designated directory;
   262  	// carry on and check if it's on the same filesystem. If
   263  	// it is, and it's empty, then claim it as our own.
   264  
   265  	if err := s.validateSameMountPoints(fsPath, target); err != nil {
   266  		return err
   267  	}
   268  
   269  	// The first time we try to take the existing directory, we'll
   270  	// ensure that it's empty and create a file to "claim" it.
   271  	// Future attachments will simply ensure that the claim file
   272  	// exists.
   273  	targetClaimPath := filepath.Join(fsPath, "juju-target-claimed")
   274  	_, err = s.dirFuncs.lstat(targetClaimPath)
   275  	if err == nil {
   276  		return nil
   277  	} else if !os.IsNotExist(err) {
   278  		return errors.Trace(err)
   279  	}
   280  	if err := ensureEmptyDir(s.dirFuncs, target); err != nil {
   281  		return errors.Trace(err)
   282  	}
   283  	if err := s.dirFuncs.mkDirAll(targetClaimPath, 0755); err != nil {
   284  		return errors.Annotate(err, "writing claim file")
   285  	}
   286  	return nil
   287  }
   288  
   289  func (s *rootfsFilesystemSource) tryBindMount(source, target string) (bool, error) {
   290  	targetSource, err := s.dirFuncs.mountPointSource(target)
   291  	if err != nil {
   292  		return false, errors.Annotate(err, "getting target mount-point source")
   293  	}
   294  	if targetSource == source {
   295  		// Already bind mounted.
   296  		return true, nil
   297  	}
   298  	if err := s.dirFuncs.bindMount(source, target); err != nil {
   299  		logger.Debugf("cannot bind-mount: %v", err)
   300  	} else {
   301  		return true, nil
   302  	}
   303  	return false, nil
   304  }
   305  
   306  func (s *rootfsFilesystemSource) validateSameMountPoints(source, target string) error {
   307  	sourceMountPoint, err := s.dirFuncs.mountPoint(source)
   308  	if err != nil {
   309  		return errors.Trace(err)
   310  	}
   311  	targetMountPoint, err := s.dirFuncs.mountPoint(target)
   312  	if err != nil {
   313  		return errors.Trace(err)
   314  	}
   315  	if sourceMountPoint != targetMountPoint {
   316  		return errors.Errorf(
   317  			"%q (%q) and %q (%q) are on different filesystems",
   318  			source, sourceMountPoint, target, targetMountPoint,
   319  		)
   320  	}
   321  	return nil
   322  }
   323  
   324  // DetachFilesystems is defined on the FilesystemSource interface.
   325  func (s *rootfsFilesystemSource) DetachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]error, error) {
   326  	results := make([]error, len(args))
   327  	for i, arg := range args {
   328  		if err := maybeUnmount(s.run, s.dirFuncs, arg.Path); err != nil {
   329  			results[i] = err
   330  		}
   331  	}
   332  	return results, nil
   333  }