github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/maas/volumes.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package maas
     5  
     6  import (
     7  	"strings"
     8  	"unicode"
     9  
    10  	"github.com/dustin/go-humanize"
    11  	"github.com/juju/collections/set"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/gomaasapi/v2"
    14  	"github.com/juju/names/v5"
    15  	"github.com/juju/schema"
    16  
    17  	"github.com/juju/juju/core/constraints"
    18  	"github.com/juju/juju/provider/common"
    19  	"github.com/juju/juju/storage"
    20  )
    21  
    22  const (
    23  	// maasStorageProviderType is the name of the storage provider
    24  	// used to specify storage when acquiring MAAS nodes.
    25  	maasStorageProviderType = storage.ProviderType("maas")
    26  
    27  	// rootDiskLabel is the label recognised by MAAS as being for
    28  	// the root disk.
    29  	rootDiskLabel = "root"
    30  
    31  	// tagsAttribute is the name of the pool attribute used
    32  	// to specify tag values for requested volumes.
    33  	tagsAttribute = "tags"
    34  )
    35  
    36  // StorageProviderTypes implements storage.ProviderRegistry.
    37  func (*maasEnviron) StorageProviderTypes() ([]storage.ProviderType, error) {
    38  	return []storage.ProviderType{maasStorageProviderType}, nil
    39  }
    40  
    41  // StorageProvider implements storage.ProviderRegistry.
    42  func (*maasEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) {
    43  	if t == maasStorageProviderType {
    44  		return maasStorageProvider{}, nil
    45  	}
    46  	return nil, errors.NotFoundf("storage provider %q", t)
    47  }
    48  
    49  // maasStorageProvider allows volumes to be specified when a node is acquired.
    50  type maasStorageProvider struct{}
    51  
    52  var storageConfigFields = schema.Fields{
    53  	tagsAttribute: schema.OneOf(
    54  		schema.List(schema.String()),
    55  		schema.String(),
    56  	),
    57  }
    58  
    59  var storageConfigChecker = schema.FieldMap(
    60  	storageConfigFields,
    61  	schema.Defaults{
    62  		tagsAttribute: schema.Omit,
    63  	},
    64  )
    65  
    66  type storageConfig struct {
    67  	tags []string
    68  }
    69  
    70  func newStorageConfig(attrs map[string]interface{}) (*storageConfig, error) {
    71  	out, err := storageConfigChecker.Coerce(attrs, nil)
    72  	if err != nil {
    73  		return nil, errors.Annotate(err, "validating MAAS storage config")
    74  	}
    75  	coerced := out.(map[string]interface{})
    76  	var tags []string
    77  	switch v := coerced[tagsAttribute].(type) {
    78  	case []string:
    79  		tags = v
    80  	case string:
    81  		fields := strings.Split(v, ",")
    82  		for _, f := range fields {
    83  			f = strings.TrimSpace(f)
    84  			if len(f) == 0 {
    85  				continue
    86  			}
    87  			if i := strings.IndexFunc(f, unicode.IsSpace); i >= 0 {
    88  				return nil, errors.Errorf("tags may not contain whitespace: %q", f)
    89  			}
    90  			tags = append(tags, f)
    91  		}
    92  	}
    93  	return &storageConfig{tags: tags}, nil
    94  }
    95  
    96  func (maasStorageProvider) ValidateForK8s(map[string]any) error {
    97  	// no validation required
    98  	return nil
    99  }
   100  
   101  // ValidateConfig is defined on the Provider interface.
   102  func (maasStorageProvider) ValidateConfig(cfg *storage.Config) error {
   103  	_, err := newStorageConfig(cfg.Attrs())
   104  	return errors.Trace(err)
   105  }
   106  
   107  // Supports is defined on the Provider interface.
   108  func (maasStorageProvider) Supports(k storage.StorageKind) bool {
   109  	return k == storage.StorageKindBlock
   110  }
   111  
   112  // Scope is defined on the Provider interface.
   113  func (maasStorageProvider) Scope() storage.Scope {
   114  	return storage.ScopeEnviron
   115  }
   116  
   117  // Dynamic is defined on the Provider interface.
   118  func (maasStorageProvider) Dynamic() bool {
   119  	return false
   120  }
   121  
   122  // Releasable is defined on the Provider interface.
   123  func (maasStorageProvider) Releasable() bool {
   124  	return false
   125  }
   126  
   127  // DefaultPools is defined on the Provider interface.
   128  func (maasStorageProvider) DefaultPools() []*storage.Config {
   129  	return nil
   130  }
   131  
   132  // VolumeSource is defined on the Provider interface.
   133  func (maasStorageProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) {
   134  	// Dynamic volumes not supported.
   135  	return nil, errors.NotSupportedf("volumes")
   136  }
   137  
   138  // FilesystemSource is defined on the Provider interface.
   139  func (maasStorageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) {
   140  	return nil, errors.NotSupportedf("filesystems")
   141  }
   142  
   143  type volumeInfo struct {
   144  	name     string
   145  	sizeInGB uint64
   146  	tags     []string
   147  }
   148  
   149  // mibToGB converts the value in MiB to GB.
   150  // Juju works in MiB, MAAS expects GB.
   151  func mibToGb(m uint64) uint64 {
   152  	return common.MiBToGiB(m) * (humanize.GiByte / humanize.GByte)
   153  }
   154  
   155  // buildMAASVolumeParameters creates the MAAS volume information to include
   156  // in a request to acquire a MAAS node, based on the supplied storage parameters.
   157  func buildMAASVolumeParameters(args []storage.VolumeParams, cons constraints.Value) ([]volumeInfo, error) {
   158  	if len(args) == 0 && cons.RootDisk == nil {
   159  		return nil, nil
   160  	}
   161  	volumes := make([]volumeInfo, len(args)+1)
   162  	rootVolume := volumeInfo{name: rootDiskLabel}
   163  	if cons.RootDisk != nil {
   164  		rootVolume.sizeInGB = mibToGb(*cons.RootDisk)
   165  	}
   166  	volumes[0] = rootVolume
   167  	for i, v := range args {
   168  		cfg, err := newStorageConfig(v.Attributes)
   169  		if err != nil {
   170  			return nil, errors.Trace(err)
   171  		}
   172  		info := volumeInfo{
   173  			name:     v.Tag.Id(),
   174  			sizeInGB: mibToGb(v.Size),
   175  			tags:     cfg.tags,
   176  		}
   177  		volumes[i+1] = info
   178  	}
   179  	return volumes, nil
   180  }
   181  
   182  func (mi *maasInstance) volumes(
   183  	mTag names.MachineTag, requestedVolumes []names.VolumeTag,
   184  ) (
   185  	[]storage.Volume, []storage.VolumeAttachment, error,
   186  ) {
   187  	if mi.constraintMatches.Storage == nil {
   188  		return nil, nil, errors.NotFoundf("constraint storage mapping")
   189  	}
   190  
   191  	var volumes []storage.Volume
   192  	var attachments []storage.VolumeAttachment
   193  
   194  	// Set up a collection of volumes tags which
   195  	// we specifically asked for when the node was acquired.
   196  	validVolumes := set.NewStrings()
   197  	for _, v := range requestedVolumes {
   198  		validVolumes.Add(v.Id())
   199  	}
   200  
   201  	for label, devices := range mi.constraintMatches.Storage {
   202  		// We don't explicitly allow the root volume to be specified yet.
   203  		if label == rootDiskLabel {
   204  			continue
   205  		}
   206  
   207  		// We only care about the volumes we specifically asked for.
   208  		if !validVolumes.Contains(label) {
   209  			continue
   210  		}
   211  
   212  		// There should be exactly one block device per label.
   213  		if len(devices) == 0 {
   214  			continue
   215  		} else if len(devices) > 1 {
   216  			// This should never happen, as we only request one block
   217  			// device per label. If it does happen, we'll just report
   218  			// the first block device and log this warning.
   219  			logger.Warningf(
   220  				"expected 1 block device for label %s, received %d",
   221  				label, len(devices),
   222  			)
   223  		}
   224  
   225  		device := devices[0]
   226  		volumeTag := names.NewVolumeTag(label)
   227  		vol := storage.Volume{
   228  			volumeTag,
   229  			storage.VolumeInfo{
   230  				VolumeId:   volumeTag.String(),
   231  				Size:       device.Size() / humanize.MiByte,
   232  				Persistent: false,
   233  			},
   234  		}
   235  		attachment := storage.VolumeAttachment{
   236  			volumeTag,
   237  			mTag,
   238  			storage.VolumeAttachmentInfo{
   239  				ReadOnly: false,
   240  			},
   241  		}
   242  
   243  		const devDiskByIdPrefix = "/dev/disk/by-id/"
   244  		const devPrefix = "/dev/"
   245  
   246  		if blockDev, ok := device.(gomaasapi.BlockDevice); ok {
   247  			// Handle a block device specifically that way the path used
   248  			// by Juju will always be a persistent path.
   249  			idPath := blockDev.IDPath()
   250  			if idPath == devPrefix+blockDev.Name() {
   251  				// On vMAAS (i.e. with virtio), the device name
   252  				// will be stable, and is what is used to form
   253  				// id_path.
   254  				deviceName := idPath[len(devPrefix):]
   255  				attachment.DeviceName = deviceName
   256  			} else if strings.HasPrefix(idPath, devDiskByIdPrefix) {
   257  				const wwnPrefix = "wwn-"
   258  				id := idPath[len(devDiskByIdPrefix):]
   259  				if strings.HasPrefix(id, wwnPrefix) {
   260  					vol.WWN = id[len(wwnPrefix):]
   261  				} else {
   262  					vol.HardwareId = id
   263  				}
   264  			} else {
   265  				// It's neither /dev/<name> nor /dev/disk/by-id/<hardware-id>,
   266  				// so set it as the device link and hope for
   267  				// the best. At worst, the path won't exist
   268  				// and the storage will remain pending.
   269  				attachment.DeviceLink = idPath
   270  			}
   271  		} else {
   272  			// Handle all other storage devices using the path MAAS provided.
   273  			// In the case of partitions the path is always stable because its
   274  			// based on the GUID of the partition using the dname path.
   275  			attachment.DeviceLink = device.Path()
   276  		}
   277  
   278  		volumes = append(volumes, vol)
   279  		attachments = append(attachments, attachment)
   280  	}
   281  	return volumes, attachments, nil
   282  }