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