github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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  	"strconv"
     8  	"strings"
     9  	"unicode"
    10  
    11  	"github.com/dustin/go-humanize"
    12  	"github.com/juju/collections/set"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/gomaasapi"
    15  	"github.com/juju/schema"
    16  	"gopkg.in/juju/names.v2"
    17  
    18  	"github.com/juju/juju/core/constraints"
    19  	"github.com/juju/juju/provider/common"
    20  	"github.com/juju/juju/storage"
    21  )
    22  
    23  const (
    24  	// maasStorageProviderType is the name of the storage provider
    25  	// used to specify storage when acquiring MAAS nodes.
    26  	maasStorageProviderType = storage.ProviderType("maas")
    27  
    28  	// rootDiskLabel is the label recognised by MAAS as being for
    29  	// the root disk.
    30  	rootDiskLabel = "root"
    31  
    32  	// tagsAttribute is the name of the pool attribute used
    33  	// to specify tag values for requested volumes.
    34  	tagsAttribute = "tags"
    35  )
    36  
    37  // StorageProviderTypes implements storage.ProviderRegistry.
    38  func (*maasEnviron) StorageProviderTypes() ([]storage.ProviderType, error) {
    39  	return []storage.ProviderType{maasStorageProviderType}, nil
    40  }
    41  
    42  // StorageProvider implements storage.ProviderRegistry.
    43  func (*maasEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) {
    44  	if t == maasStorageProviderType {
    45  		return maasStorageProvider{}, nil
    46  	}
    47  	return nil, errors.NotFoundf("storage provider %q", t)
    48  }
    49  
    50  // maasStorageProvider allows volumes to be specified when a node is acquired.
    51  type maasStorageProvider struct{}
    52  
    53  var storageConfigFields = schema.Fields{
    54  	tagsAttribute: schema.OneOf(
    55  		schema.List(schema.String()),
    56  		schema.String(),
    57  	),
    58  }
    59  
    60  var storageConfigChecker = schema.FieldMap(
    61  	storageConfigFields,
    62  	schema.Defaults{
    63  		tagsAttribute: schema.Omit,
    64  	},
    65  )
    66  
    67  type storageConfig struct {
    68  	tags []string
    69  }
    70  
    71  func newStorageConfig(attrs map[string]interface{}) (*storageConfig, error) {
    72  	out, err := storageConfigChecker.Coerce(attrs, nil)
    73  	if err != nil {
    74  		return nil, errors.Annotate(err, "validating MAAS storage config")
    75  	}
    76  	coerced := out.(map[string]interface{})
    77  	var tags []string
    78  	switch v := coerced[tagsAttribute].(type) {
    79  	case []string:
    80  		tags = v
    81  	case string:
    82  		fields := strings.Split(v, ",")
    83  		for _, f := range fields {
    84  			f = strings.TrimSpace(f)
    85  			if len(f) == 0 {
    86  				continue
    87  			}
    88  			if i := strings.IndexFunc(f, unicode.IsSpace); i >= 0 {
    89  				return nil, errors.Errorf("tags may not contain whitespace: %q", f)
    90  			}
    91  			tags = append(tags, f)
    92  		}
    93  	}
    94  	return &storageConfig{tags: tags}, nil
    95  }
    96  
    97  // ValidateConfig is defined on the Provider interface.
    98  func (maasStorageProvider) ValidateConfig(cfg *storage.Config) error {
    99  	_, err := newStorageConfig(cfg.Attrs())
   100  	return errors.Trace(err)
   101  }
   102  
   103  // Supports is defined on the Provider interface.
   104  func (maasStorageProvider) Supports(k storage.StorageKind) bool {
   105  	return k == storage.StorageKindBlock
   106  }
   107  
   108  // Scope is defined on the Provider interface.
   109  func (maasStorageProvider) Scope() storage.Scope {
   110  	return storage.ScopeEnviron
   111  }
   112  
   113  // Dynamic is defined on the Provider interface.
   114  func (maasStorageProvider) Dynamic() bool {
   115  	return false
   116  }
   117  
   118  // Releasable is defined on the Provider interface.
   119  func (maasStorageProvider) Releasable() bool {
   120  	return false
   121  }
   122  
   123  // DefaultPools is defined on the Provider interface.
   124  func (maasStorageProvider) DefaultPools() []*storage.Config {
   125  	return nil
   126  }
   127  
   128  // VolumeSource is defined on the Provider interface.
   129  func (maasStorageProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) {
   130  	// Dynamic volumes not supported.
   131  	return nil, errors.NotSupportedf("volumes")
   132  }
   133  
   134  // FilesystemSource is defined on the Provider interface.
   135  func (maasStorageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) {
   136  	return nil, errors.NotSupportedf("filesystems")
   137  }
   138  
   139  type volumeInfo struct {
   140  	name     string
   141  	sizeInGB uint64
   142  	tags     []string
   143  }
   144  
   145  // mibToGB converts the value in MiB to GB.
   146  // Juju works in MiB, MAAS expects GB.
   147  func mibToGb(m uint64) uint64 {
   148  	return common.MiBToGiB(m) * (humanize.GiByte / humanize.GByte)
   149  }
   150  
   151  // buildMAASVolumeParameters creates the MAAS volume information to include
   152  // in a request to acquire a MAAS node, based on the supplied storage parameters.
   153  func buildMAASVolumeParameters(args []storage.VolumeParams, cons constraints.Value) ([]volumeInfo, error) {
   154  	if len(args) == 0 && cons.RootDisk == nil {
   155  		return nil, nil
   156  	}
   157  	volumes := make([]volumeInfo, len(args)+1)
   158  	rootVolume := volumeInfo{name: rootDiskLabel}
   159  	if cons.RootDisk != nil {
   160  		rootVolume.sizeInGB = mibToGb(*cons.RootDisk)
   161  	}
   162  	volumes[0] = rootVolume
   163  	for i, v := range args {
   164  		cfg, err := newStorageConfig(v.Attributes)
   165  		if err != nil {
   166  			return nil, errors.Trace(err)
   167  		}
   168  		info := volumeInfo{
   169  			name:     v.Tag.Id(),
   170  			sizeInGB: mibToGb(v.Size),
   171  			tags:     cfg.tags,
   172  		}
   173  		volumes[i+1] = info
   174  	}
   175  	return volumes, nil
   176  }
   177  
   178  // volumes creates the storage volumes and attachments
   179  // corresponding to the volume info associated with a MAAS node.
   180  func (mi *maas1Instance) volumes(
   181  	mTag names.MachineTag, requestedVolumes []names.VolumeTag,
   182  ) (
   183  	[]storage.Volume, []storage.VolumeAttachment, error,
   184  ) {
   185  	var volumes []storage.Volume
   186  	var attachments []storage.VolumeAttachment
   187  
   188  	deviceInfo, ok := mi.maasObject.GetMap()["physicalblockdevice_set"]
   189  	// Older MAAS servers don't support storage.
   190  	if !ok || deviceInfo.IsNil() {
   191  		return volumes, attachments, nil
   192  	}
   193  
   194  	labelsMap, ok := mi.maasObject.GetMap()["constraint_map"]
   195  	if !ok || labelsMap.IsNil() {
   196  		return nil, nil, errors.NotFoundf("constraint map field")
   197  	}
   198  
   199  	devices, err := deviceInfo.GetArray()
   200  	if err != nil {
   201  		return nil, nil, errors.Trace(err)
   202  	}
   203  	// deviceLabel is the volume label passed
   204  	// into the acquire node call as part
   205  	// of the storage constraints parameter.
   206  	deviceLabels, err := labelsMap.GetMap()
   207  	if err != nil {
   208  		return nil, nil, errors.Annotate(err, "invalid constraint map value")
   209  	}
   210  
   211  	// Set up a collection of volumes tags which
   212  	// we specifically asked for when the node was acquired.
   213  	validVolumes := set.NewStrings()
   214  	for _, v := range requestedVolumes {
   215  		validVolumes.Add(v.Id())
   216  	}
   217  
   218  	for _, d := range devices {
   219  		deviceAttrs, err := d.GetMap()
   220  		if err != nil {
   221  			return nil, nil, errors.Trace(err)
   222  		}
   223  		// id in devices list is numeric
   224  		id, err := deviceAttrs["id"].GetFloat64()
   225  		if err != nil {
   226  			return nil, nil, errors.Annotate(err, "invalid device id")
   227  		}
   228  		// id in constraint_map field is a string
   229  		idKey := strconv.Itoa(int(id))
   230  
   231  		// Device Label.
   232  		deviceLabelValue, ok := deviceLabels[idKey]
   233  		if !ok {
   234  			logger.Debugf("acquire maas node: missing volume label for id %q", idKey)
   235  			continue
   236  		}
   237  		deviceLabel, err := deviceLabelValue.GetString()
   238  		if err != nil {
   239  			return nil, nil, errors.Annotate(err, "invalid device label")
   240  		}
   241  		// We don't explicitly allow the root volume to be specified yet.
   242  		if deviceLabel == rootDiskLabel {
   243  			continue
   244  		}
   245  		// We only care about the volumes we specifically asked for.
   246  		if !validVolumes.Contains(deviceLabel) {
   247  			continue
   248  		}
   249  
   250  		// HardwareId and DeviceName.
   251  		// First try for id_path.
   252  		idPathPrefix := "/dev/disk/by-id/"
   253  		hardwareId, err := deviceAttrs["id_path"].GetString()
   254  		var deviceName string
   255  		if err == nil {
   256  			if !strings.HasPrefix(hardwareId, idPathPrefix) {
   257  				return nil, nil, errors.Errorf("invalid device id %q", hardwareId)
   258  			}
   259  			hardwareId = hardwareId[len(idPathPrefix):]
   260  		} else {
   261  			// On VMAAS, id_path not available so try for path instead.
   262  			deviceName, err = deviceAttrs["name"].GetString()
   263  			if err != nil {
   264  				return nil, nil, errors.Annotate(err, "invalid device name")
   265  			}
   266  		}
   267  
   268  		// Size.
   269  		sizeinBytes, err := deviceAttrs["size"].GetFloat64()
   270  		if err != nil {
   271  			return nil, nil, errors.Annotate(err, "invalid device size")
   272  		}
   273  
   274  		volumeTag := names.NewVolumeTag(deviceLabel)
   275  		vol := storage.Volume{
   276  			volumeTag,
   277  			storage.VolumeInfo{
   278  				VolumeId:   volumeTag.String(),
   279  				HardwareId: hardwareId,
   280  				Size:       uint64(sizeinBytes / humanize.MiByte),
   281  				Persistent: false,
   282  			},
   283  		}
   284  		volumes = append(volumes, vol)
   285  
   286  		attachment := storage.VolumeAttachment{
   287  			volumeTag,
   288  			mTag,
   289  			storage.VolumeAttachmentInfo{
   290  				DeviceName: deviceName,
   291  				ReadOnly:   false,
   292  			},
   293  		}
   294  		attachments = append(attachments, attachment)
   295  	}
   296  	return volumes, attachments, nil
   297  }
   298  
   299  func (mi *maas2Instance) volumes(
   300  	mTag names.MachineTag, requestedVolumes []names.VolumeTag,
   301  ) (
   302  	[]storage.Volume, []storage.VolumeAttachment, error,
   303  ) {
   304  	if mi.constraintMatches.Storage == nil {
   305  		return nil, nil, errors.NotFoundf("constraint storage mapping")
   306  	}
   307  
   308  	var volumes []storage.Volume
   309  	var attachments []storage.VolumeAttachment
   310  
   311  	// Set up a collection of volumes tags which
   312  	// we specifically asked for when the node was acquired.
   313  	validVolumes := set.NewStrings()
   314  	for _, v := range requestedVolumes {
   315  		validVolumes.Add(v.Id())
   316  	}
   317  
   318  	for label, devices := range mi.constraintMatches.Storage {
   319  		// We don't explicitly allow the root volume to be specified yet.
   320  		if label == rootDiskLabel {
   321  			continue
   322  		}
   323  
   324  		// We only care about the volumes we specifically asked for.
   325  		if !validVolumes.Contains(label) {
   326  			continue
   327  		}
   328  
   329  		// There should be exactly one block device per label.
   330  		if len(devices) == 0 {
   331  			continue
   332  		} else if len(devices) > 1 {
   333  			// This should never happen, as we only request one block
   334  			// device per label. If it does happen, we'll just report
   335  			// the first block device and log this warning.
   336  			logger.Warningf(
   337  				"expected 1 block device for label %s, received %d",
   338  				label, len(devices),
   339  			)
   340  		}
   341  
   342  		device := devices[0]
   343  		volumeTag := names.NewVolumeTag(label)
   344  		vol := storage.Volume{
   345  			volumeTag,
   346  			storage.VolumeInfo{
   347  				VolumeId:   volumeTag.String(),
   348  				Size:       device.Size() / humanize.MiByte,
   349  				Persistent: false,
   350  			},
   351  		}
   352  		attachment := storage.VolumeAttachment{
   353  			volumeTag,
   354  			mTag,
   355  			storage.VolumeAttachmentInfo{
   356  				ReadOnly: false,
   357  			},
   358  		}
   359  
   360  		const devDiskByIdPrefix = "/dev/disk/by-id/"
   361  		const devPrefix = "/dev/"
   362  
   363  		if blockDev, ok := device.(gomaasapi.BlockDevice); ok {
   364  			// Handle a block device specifically that way the path used
   365  			// by Juju will always be a persistent path.
   366  			idPath := blockDev.IDPath()
   367  			if idPath == devPrefix+blockDev.Name() {
   368  				// On vMAAS (i.e. with virtio), the device name
   369  				// will be stable, and is what is used to form
   370  				// id_path.
   371  				deviceName := idPath[len(devPrefix):]
   372  				attachment.DeviceName = deviceName
   373  			} else if strings.HasPrefix(idPath, devDiskByIdPrefix) {
   374  				const wwnPrefix = "wwn-"
   375  				id := idPath[len(devDiskByIdPrefix):]
   376  				if strings.HasPrefix(id, wwnPrefix) {
   377  					vol.WWN = id[len(wwnPrefix):]
   378  				} else {
   379  					vol.HardwareId = id
   380  				}
   381  			} else {
   382  				// It's neither /dev/<name> nor /dev/disk/by-id/<hardware-id>,
   383  				// so set it as the device link and hope for
   384  				// the best. At worst, the path won't exist
   385  				// and the storage will remain pending.
   386  				attachment.DeviceLink = idPath
   387  			}
   388  		} else {
   389  			// Handle all other storage devices using the path MAAS provided.
   390  			// In the case of partitions the path is always stable because its
   391  			// based on the GUID of the partition using the dname path.
   392  			attachment.DeviceLink = device.Path()
   393  		}
   394  
   395  		volumes = append(volumes, vol)
   396  		attachments = append(attachments, attachment)
   397  	}
   398  	return volumes, attachments, nil
   399  }