github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/storage/provider/loop.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  	"fmt"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/juju/errors"
    14  	"github.com/juju/names/v5"
    15  
    16  	"github.com/juju/juju/environs/context"
    17  	"github.com/juju/juju/storage"
    18  )
    19  
    20  const (
    21  	// Loop provider types.
    22  	LoopProviderType     = storage.ProviderType("loop")
    23  	HostLoopProviderType = storage.ProviderType("hostloop")
    24  )
    25  
    26  // loopProviders create volume sources which use loop devices.
    27  type loopProvider struct {
    28  	// run is a function used for running commands on the local machine.
    29  	run runCommandFunc
    30  }
    31  
    32  var _ storage.Provider = (*loopProvider)(nil)
    33  
    34  func (*loopProvider) ValidateForK8s(map[string]any) error {
    35  	return errors.NotValidf("storage provider type %q", LoopProviderType)
    36  }
    37  
    38  // ValidateConfig is defined on the Provider interface.
    39  func (*loopProvider) ValidateConfig(*storage.Config) error {
    40  	// Loop provider has no configuration.
    41  	return nil
    42  }
    43  
    44  // validateFullConfig validates a fully-constructed storage config,
    45  // combining the user-specified config and any internally specified
    46  // config.
    47  func (lp *loopProvider) validateFullConfig(cfg *storage.Config) error {
    48  	if err := lp.ValidateConfig(cfg); err != nil {
    49  		return err
    50  	}
    51  	storageDir, ok := cfg.ValueString(storage.ConfigStorageDir)
    52  	if !ok || storageDir == "" {
    53  		return errors.New("storage directory not specified")
    54  	}
    55  	return nil
    56  }
    57  
    58  // VolumeSource is defined on the Provider interface.
    59  func (lp *loopProvider) VolumeSource(sourceConfig *storage.Config) (storage.VolumeSource, error) {
    60  	if err := lp.validateFullConfig(sourceConfig); err != nil {
    61  		return nil, err
    62  	}
    63  	// storageDir is validated by validateFullConfig.
    64  	storageDir, _ := sourceConfig.ValueString(storage.ConfigStorageDir)
    65  	return &loopVolumeSource{
    66  		&osDirFuncs{run: lp.run},
    67  		lp.run,
    68  		storageDir,
    69  	}, nil
    70  }
    71  
    72  // FilesystemSource is defined on the Provider interface.
    73  func (lp *loopProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) {
    74  	return nil, errors.NotSupportedf("filesystems")
    75  }
    76  
    77  // Supports is defined on the Provider interface.
    78  func (*loopProvider) Supports(k storage.StorageKind) bool {
    79  	return k == storage.StorageKindBlock
    80  }
    81  
    82  // Scope is defined on the Provider interface.
    83  func (*loopProvider) Scope() storage.Scope {
    84  	return storage.ScopeMachine
    85  }
    86  
    87  // Dynamic is defined on the Provider interface.
    88  func (*loopProvider) Dynamic() bool {
    89  	return true
    90  }
    91  
    92  // Releasable is defined on the Provider interface.
    93  func (*loopProvider) Releasable() bool {
    94  	return false
    95  }
    96  
    97  // DefaultPools is defined on the Provider interface.
    98  func (*loopProvider) DefaultPools() []*storage.Config {
    99  	return nil
   100  }
   101  
   102  // loopVolumeSource provides common functionality to handle
   103  // loop devices for rootfs and host loop volume sources.
   104  type loopVolumeSource struct {
   105  	dirFuncs   dirFuncs
   106  	run        runCommandFunc
   107  	storageDir string
   108  }
   109  
   110  var _ storage.VolumeSource = (*loopVolumeSource)(nil)
   111  
   112  // CreateVolumes is defined on the VolumeSource interface.
   113  func (lvs *loopVolumeSource) CreateVolumes(ctx context.ProviderCallContext, args []storage.VolumeParams) ([]storage.CreateVolumesResult, error) {
   114  	results := make([]storage.CreateVolumesResult, len(args))
   115  	for i, arg := range args {
   116  		volume, err := lvs.createVolume(arg)
   117  		if err != nil {
   118  			results[i].Error = errors.Annotate(err, "creating volume")
   119  		}
   120  		results[i].Volume = &volume
   121  	}
   122  	return results, nil
   123  }
   124  
   125  func (lvs *loopVolumeSource) createVolume(params storage.VolumeParams) (storage.Volume, error) {
   126  	volumeId := params.Tag.String()
   127  	loopFilePath := lvs.volumeFilePath(params.Tag)
   128  	if err := ensureDir(lvs.dirFuncs, filepath.Dir(loopFilePath)); err != nil {
   129  		return storage.Volume{}, errors.Trace(err)
   130  	}
   131  	if err := createBlockFile(lvs.run, loopFilePath, params.Size); err != nil {
   132  		return storage.Volume{}, errors.Annotate(err, "could not create block file")
   133  	}
   134  	return storage.Volume{
   135  		params.Tag,
   136  		storage.VolumeInfo{
   137  			VolumeId: volumeId,
   138  			Size:     params.Size,
   139  		},
   140  	}, nil
   141  }
   142  
   143  func (lvs *loopVolumeSource) volumeFilePath(tag names.VolumeTag) string {
   144  	return filepath.Join(lvs.storageDir, tag.String())
   145  }
   146  
   147  // ListVolumes is defined on the VolumeSource interface.
   148  func (lvs *loopVolumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) {
   149  	// TODO(axw) implement this when we need it.
   150  	return nil, errors.NotImplementedf("ListVolumes")
   151  }
   152  
   153  // DescribeVolumes is defined on the VolumeSource interface.
   154  func (lvs *loopVolumeSource) DescribeVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) {
   155  	// TODO(axw) implement this when we need it.
   156  	return nil, errors.NotImplementedf("DescribeVolumes")
   157  }
   158  
   159  // DestroyVolumes is defined on the VolumeSource interface.
   160  func (lvs *loopVolumeSource) DestroyVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) {
   161  	results := make([]error, len(volumeIds))
   162  	for i, volumeId := range volumeIds {
   163  		if err := lvs.destroyVolume(volumeId); err != nil {
   164  			results[i] = errors.Annotatef(err, "destroying %q", volumeId)
   165  		}
   166  	}
   167  	return results, nil
   168  }
   169  
   170  // ReleaseVolumes is defined on the VolumeSource interface.
   171  func (lvs *loopVolumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) {
   172  	return make([]error, len(volumeIds)), nil
   173  }
   174  
   175  func (lvs *loopVolumeSource) destroyVolume(volumeId string) error {
   176  	tag, err := names.ParseVolumeTag(volumeId)
   177  	if err != nil {
   178  		return errors.Errorf("invalid loop volume ID %q", volumeId)
   179  	}
   180  	loopFilePath := lvs.volumeFilePath(tag)
   181  	err = os.Remove(loopFilePath)
   182  	if err != nil && !os.IsNotExist(err) {
   183  		return errors.Annotate(err, "removing loop backing file")
   184  	}
   185  	return nil
   186  }
   187  
   188  // ValidateVolumeParams is defined on the VolumeSource interface.
   189  func (lvs *loopVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error {
   190  	// ValdiateVolumeParams may be called on a machine other than the
   191  	// machine where the loop device will be created, so we cannot check
   192  	// available size until we get to CreateVolumes.
   193  	return nil
   194  }
   195  
   196  // AttachVolumes is defined on the VolumeSource interface.
   197  func (lvs *loopVolumeSource) AttachVolumes(ctx context.ProviderCallContext, args []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) {
   198  	results := make([]storage.AttachVolumesResult, len(args))
   199  	for i, arg := range args {
   200  		attachment, err := lvs.attachVolume(arg)
   201  		if err != nil {
   202  			results[i].Error = errors.Annotatef(err, "attaching volume %v", arg.Volume.Id())
   203  			continue
   204  		}
   205  		results[i].VolumeAttachment = attachment
   206  	}
   207  	return results, nil
   208  }
   209  
   210  func (lvs *loopVolumeSource) attachVolume(arg storage.VolumeAttachmentParams) (*storage.VolumeAttachment, error) {
   211  	loopFilePath := lvs.volumeFilePath(arg.Volume)
   212  	deviceName, err := attachLoopDevice(lvs.run, loopFilePath, arg.ReadOnly)
   213  	if err != nil {
   214  		os.Remove(loopFilePath)
   215  		return nil, errors.Annotate(err, "attaching loop device")
   216  	}
   217  	return &storage.VolumeAttachment{
   218  		arg.Volume,
   219  		arg.Machine,
   220  		storage.VolumeAttachmentInfo{
   221  			DeviceName: deviceName,
   222  			ReadOnly:   arg.ReadOnly,
   223  		},
   224  	}, nil
   225  }
   226  
   227  // DetachVolumes is defined on the VolumeSource interface.
   228  func (lvs *loopVolumeSource) DetachVolumes(ctx context.ProviderCallContext, args []storage.VolumeAttachmentParams) ([]error, error) {
   229  	results := make([]error, len(args))
   230  	for i, arg := range args {
   231  		if err := lvs.detachVolume(arg.Volume); err != nil {
   232  			results[i] = errors.Annotatef(err, "detaching volume %s", arg.Volume.Id())
   233  		}
   234  	}
   235  	return results, nil
   236  }
   237  
   238  func (lvs *loopVolumeSource) detachVolume(tag names.VolumeTag) error {
   239  	loopFilePath := lvs.volumeFilePath(tag)
   240  	deviceNames, err := associatedLoopDevices(lvs.run, loopFilePath)
   241  	if err != nil {
   242  		return errors.Annotate(err, "locating loop device")
   243  	}
   244  	if len(deviceNames) > 1 {
   245  		logger.Errorf("expected 1 loop device, got %d", len(deviceNames))
   246  	}
   247  	for _, deviceName := range deviceNames {
   248  		if err := detachLoopDevice(lvs.run, deviceName); err != nil {
   249  			return errors.Trace(err)
   250  		}
   251  	}
   252  	return nil
   253  }
   254  
   255  // createBlockFile creates a file at the specified path, with the
   256  // given size in mebibytes.
   257  func createBlockFile(run runCommandFunc, filePath string, sizeInMiB uint64) error {
   258  	// fallocate will reserve the space without actually writing to it.
   259  	_, err := run("fallocate", "-l", fmt.Sprintf("%dMiB", sizeInMiB), filePath)
   260  	if err != nil {
   261  		return errors.Annotatef(err, "allocating loop backing file %q", filePath)
   262  	}
   263  	return nil
   264  }
   265  
   266  // attachLoopDevice attaches a loop device to the file with the
   267  // specified path, and returns the loop device's name (e.g. "loop0").
   268  // losetup will create additional loop devices as necessary.
   269  func attachLoopDevice(run runCommandFunc, filePath string, readOnly bool) (loopDeviceName string, _ error) {
   270  	devices, err := associatedLoopDevices(run, filePath)
   271  	if err != nil {
   272  		return "", err
   273  	}
   274  	if len(devices) > 0 {
   275  		// Already attached.
   276  		logger.Debugf("%s already attached to %s", filePath, devices)
   277  		return devices[0], nil
   278  	}
   279  	// -f automatically finds the first available loop-device.
   280  	// -r sets up a read-only loop-device.
   281  	// --show returns the loop device chosen on stdout.
   282  	args := []string{"-f", "--show"}
   283  	if readOnly {
   284  		args = append(args, "-r")
   285  	}
   286  	args = append(args, filePath)
   287  	stdout, err := run("losetup", args...)
   288  	if err != nil {
   289  		return "", errors.Annotatef(err, "attaching loop device to %q", filePath)
   290  	}
   291  	stdout = strings.TrimSpace(stdout)
   292  	loopDeviceName = stdout[len("/dev/"):]
   293  	return loopDeviceName, nil
   294  }
   295  
   296  // detachLoopDevice detaches the loop device with the specified name.
   297  func detachLoopDevice(run runCommandFunc, deviceName string) error {
   298  	_, err := run("losetup", "-d", path.Join("/dev", deviceName))
   299  	if err != nil {
   300  		return errors.Annotatef(err, "detaching loop device %q", deviceName)
   301  	}
   302  	return err
   303  }
   304  
   305  // associatedLoopDevices returns the device names of the loop devices
   306  // associated with the specified file path.
   307  func associatedLoopDevices(run runCommandFunc, filePath string) ([]string, error) {
   308  	stdout, err := run("losetup", "-j", filePath)
   309  	if err != nil {
   310  		return nil, errors.Trace(err)
   311  	}
   312  	stdout = strings.TrimSpace(stdout)
   313  	if stdout == "" {
   314  		return nil, nil
   315  	}
   316  	// The output will be zero or more lines with the format:
   317  	//    "/dev/loop0: [0021]:7504142 (/tmp/test.dat)"
   318  	lines := strings.Split(stdout, "\n")
   319  	deviceNames := make([]string, len(lines))
   320  	for i, line := range lines {
   321  		pos := strings.IndexRune(line, ':')
   322  		if pos == -1 {
   323  			return nil, errors.Errorf("unexpected output %q", line)
   324  		}
   325  		deviceName := line[:pos][len("/dev/"):]
   326  		deviceNames[i] = deviceName
   327  	}
   328  	return deviceNames, nil
   329  }