github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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/errors"
    13  	"github.com/juju/names"
    14  	"github.com/juju/schema"
    15  	"github.com/juju/utils/set"
    16  
    17  	"github.com/juju/juju/constraints"
    18  	"github.com/juju/juju/environs/config"
    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  // maasStorageProvider allows volumes to be specified when a node is acquired.
    38  type maasStorageProvider struct{}
    39  
    40  var storageConfigFields = schema.Fields{
    41  	tagsAttribute: schema.OneOf(
    42  		schema.List(schema.String()),
    43  		schema.String(),
    44  	),
    45  }
    46  
    47  var storageConfigChecker = schema.FieldMap(
    48  	storageConfigFields,
    49  	schema.Defaults{
    50  		tagsAttribute: schema.Omit,
    51  	},
    52  )
    53  
    54  type storageConfig struct {
    55  	tags []string
    56  }
    57  
    58  func newStorageConfig(attrs map[string]interface{}) (*storageConfig, error) {
    59  	out, err := storageConfigChecker.Coerce(attrs, nil)
    60  	if err != nil {
    61  		return nil, errors.Annotate(err, "validating MAAS storage config")
    62  	}
    63  	coerced := out.(map[string]interface{})
    64  	var tags []string
    65  	switch v := coerced[tagsAttribute].(type) {
    66  	case []string:
    67  		tags = v
    68  	case string:
    69  		fields := strings.Split(v, ",")
    70  		for _, f := range fields {
    71  			f = strings.TrimSpace(f)
    72  			if len(f) == 0 {
    73  				continue
    74  			}
    75  			if i := strings.IndexFunc(f, unicode.IsSpace); i >= 0 {
    76  				return nil, errors.Errorf("tags may not contain whitespace: %q", f)
    77  			}
    78  			tags = append(tags, f)
    79  		}
    80  	}
    81  	return &storageConfig{tags: tags}, nil
    82  }
    83  
    84  // ValidateConfig is defined on the Provider interface.
    85  func (e *maasStorageProvider) ValidateConfig(cfg *storage.Config) error {
    86  	_, err := newStorageConfig(cfg.Attrs())
    87  	return errors.Trace(err)
    88  }
    89  
    90  // Supports is defined on the Provider interface.
    91  func (e *maasStorageProvider) Supports(k storage.StorageKind) bool {
    92  	return k == storage.StorageKindBlock
    93  }
    94  
    95  // Scope is defined on the Provider interface.
    96  func (e *maasStorageProvider) Scope() storage.Scope {
    97  	return storage.ScopeEnviron
    98  }
    99  
   100  // Dynamic is defined on the Provider interface.
   101  func (e *maasStorageProvider) Dynamic() bool {
   102  	return false
   103  }
   104  
   105  // VolumeSource is defined on the Provider interface.
   106  func (e *maasStorageProvider) VolumeSource(environConfig *config.Config, providerConfig *storage.Config) (storage.VolumeSource, error) {
   107  	// Dynamic volumes not supported.
   108  	return nil, errors.NotSupportedf("volumes")
   109  }
   110  
   111  // FilesystemSource is defined on the Provider interface.
   112  func (e *maasStorageProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) {
   113  	return nil, errors.NotSupportedf("filesystems")
   114  }
   115  
   116  type volumeInfo struct {
   117  	name     string
   118  	sizeInGB uint64
   119  	tags     []string
   120  }
   121  
   122  // mibToGB converts the value in MiB to GB.
   123  // Juju works in MiB, MAAS expects GB.
   124  func mibToGb(m uint64) uint64 {
   125  	return common.MiBToGiB(m) * (humanize.GiByte / humanize.GByte)
   126  }
   127  
   128  // buildMAASVolumeParameters creates the MAAS volume information to include
   129  // in a request to acquire a MAAS node, based on the supplied storage parameters.
   130  func buildMAASVolumeParameters(args []storage.VolumeParams, cons constraints.Value) ([]volumeInfo, error) {
   131  	if len(args) == 0 && cons.RootDisk == nil {
   132  		return nil, nil
   133  	}
   134  	volumes := make([]volumeInfo, len(args)+1)
   135  	rootVolume := volumeInfo{name: rootDiskLabel}
   136  	if cons.RootDisk != nil {
   137  		rootVolume.sizeInGB = mibToGb(*cons.RootDisk)
   138  	}
   139  	volumes[0] = rootVolume
   140  	for i, v := range args {
   141  		cfg, err := newStorageConfig(v.Attributes)
   142  		if err != nil {
   143  			return nil, errors.Trace(err)
   144  		}
   145  		info := volumeInfo{
   146  			name:     v.Tag.Id(),
   147  			sizeInGB: mibToGb(v.Size),
   148  			tags:     cfg.tags,
   149  		}
   150  		volumes[i+1] = info
   151  	}
   152  	return volumes, nil
   153  }
   154  
   155  // volumes creates the storage volumes and attachments
   156  // corresponding to the volume info associated with a MAAS node.
   157  func (mi *maas1Instance) volumes(
   158  	mTag names.MachineTag, requestedVolumes []names.VolumeTag,
   159  ) (
   160  	[]storage.Volume, []storage.VolumeAttachment, error,
   161  ) {
   162  	var volumes []storage.Volume
   163  	var attachments []storage.VolumeAttachment
   164  
   165  	deviceInfo, ok := mi.maasObject.GetMap()["physicalblockdevice_set"]
   166  	// Older MAAS servers don't support storage.
   167  	if !ok || deviceInfo.IsNil() {
   168  		return volumes, attachments, nil
   169  	}
   170  
   171  	labelsMap, ok := mi.maasObject.GetMap()["constraint_map"]
   172  	if !ok || labelsMap.IsNil() {
   173  		return nil, nil, errors.NotFoundf("constraint map field")
   174  	}
   175  
   176  	devices, err := deviceInfo.GetArray()
   177  	if err != nil {
   178  		return nil, nil, errors.Trace(err)
   179  	}
   180  	// deviceLabel is the volume label passed
   181  	// into the acquire node call as part
   182  	// of the storage constraints parameter.
   183  	deviceLabels, err := labelsMap.GetMap()
   184  	if err != nil {
   185  		return nil, nil, errors.Annotate(err, "invalid constraint map value")
   186  	}
   187  
   188  	// Set up a collection of volumes tags which
   189  	// we specifically asked for when the node was acquired.
   190  	validVolumes := set.NewStrings()
   191  	for _, v := range requestedVolumes {
   192  		validVolumes.Add(v.Id())
   193  	}
   194  
   195  	for _, d := range devices {
   196  		deviceAttrs, err := d.GetMap()
   197  		if err != nil {
   198  			return nil, nil, errors.Trace(err)
   199  		}
   200  		// id in devices list is numeric
   201  		id, err := deviceAttrs["id"].GetFloat64()
   202  		if err != nil {
   203  			return nil, nil, errors.Annotate(err, "invalid device id")
   204  		}
   205  		// id in constraint_map field is a string
   206  		idKey := strconv.Itoa(int(id))
   207  
   208  		// Device Label.
   209  		deviceLabelValue, ok := deviceLabels[idKey]
   210  		if !ok {
   211  			logger.Debugf("acquire maas node: missing volume label for id %q", idKey)
   212  			continue
   213  		}
   214  		deviceLabel, err := deviceLabelValue.GetString()
   215  		if err != nil {
   216  			return nil, nil, errors.Annotate(err, "invalid device label")
   217  		}
   218  		// We don't explicitly allow the root volume to be specified yet.
   219  		if deviceLabel == rootDiskLabel {
   220  			continue
   221  		}
   222  		// We only care about the volumes we specifically asked for.
   223  		if !validVolumes.Contains(deviceLabel) {
   224  			continue
   225  		}
   226  
   227  		// HardwareId and DeviceName.
   228  		// First try for id_path.
   229  		idPathPrefix := "/dev/disk/by-id/"
   230  		hardwareId, err := deviceAttrs["id_path"].GetString()
   231  		var deviceName string
   232  		if err == nil {
   233  			if !strings.HasPrefix(hardwareId, idPathPrefix) {
   234  				return nil, nil, errors.Errorf("invalid device id %q", hardwareId)
   235  			}
   236  			hardwareId = hardwareId[len(idPathPrefix):]
   237  		} else {
   238  			// On VMAAS, id_path not available so try for path instead.
   239  			deviceName, err = deviceAttrs["name"].GetString()
   240  			if err != nil {
   241  				return nil, nil, errors.Annotate(err, "invalid device name")
   242  			}
   243  		}
   244  
   245  		// Size.
   246  		sizeinBytes, err := deviceAttrs["size"].GetFloat64()
   247  		if err != nil {
   248  			return nil, nil, errors.Annotate(err, "invalid device size")
   249  		}
   250  
   251  		volumeTag := names.NewVolumeTag(deviceLabel)
   252  		vol := storage.Volume{
   253  			volumeTag,
   254  			storage.VolumeInfo{
   255  				VolumeId:   volumeTag.String(),
   256  				HardwareId: hardwareId,
   257  				Size:       uint64(sizeinBytes / humanize.MiByte),
   258  				Persistent: false,
   259  			},
   260  		}
   261  		volumes = append(volumes, vol)
   262  
   263  		attachment := storage.VolumeAttachment{
   264  			volumeTag,
   265  			mTag,
   266  			storage.VolumeAttachmentInfo{
   267  				DeviceName: deviceName,
   268  				ReadOnly:   false,
   269  			},
   270  		}
   271  		attachments = append(attachments, attachment)
   272  	}
   273  	return volumes, attachments, nil
   274  }
   275  
   276  func (mi *maas2Instance) volumes(
   277  	mTag names.MachineTag, requestedVolumes []names.VolumeTag,
   278  ) (
   279  	[]storage.Volume, []storage.VolumeAttachment, error,
   280  ) {
   281  	if mi.constraintMatches.Storage == nil {
   282  		return nil, nil, errors.NotFoundf("constraint storage mapping")
   283  	}
   284  
   285  	var volumes []storage.Volume
   286  	var attachments []storage.VolumeAttachment
   287  
   288  	// Set up a collection of volumes tags which
   289  	// we specifically asked for when the node was acquired.
   290  	validVolumes := set.NewStrings()
   291  	for _, v := range requestedVolumes {
   292  		validVolumes.Add(v.Id())
   293  	}
   294  
   295  	for label, device := range mi.constraintMatches.Storage {
   296  		// We don't explicitly allow the root volume to be specified yet.
   297  		if label == rootDiskLabel {
   298  			continue
   299  		}
   300  		// We only care about the volumes we specifically asked for.
   301  		if !validVolumes.Contains(label) {
   302  			continue
   303  		}
   304  
   305  		volumeTag := names.NewVolumeTag(label)
   306  		vol := storage.Volume{
   307  			volumeTag,
   308  			storage.VolumeInfo{
   309  				VolumeId:   volumeTag.String(),
   310  				Size:       uint64(device.Size() / humanize.MiByte),
   311  				Persistent: false,
   312  			},
   313  		}
   314  		volumes = append(volumes, vol)
   315  
   316  		attachment := storage.VolumeAttachment{
   317  			volumeTag,
   318  			mTag,
   319  			storage.VolumeAttachmentInfo{
   320  				DeviceLink: device.Path(),
   321  				ReadOnly:   false,
   322  			},
   323  		}
   324  		attachments = append(attachments, attachment)
   325  	}
   326  	return volumes, attachments, nil
   327  }