github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/provider/ec2/ebs.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package ec2
     5  
     6  import (
     7  	"regexp"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/schema"
    13  	"github.com/juju/utils"
    14  	"github.com/juju/utils/set"
    15  	"gopkg.in/amz.v3/ec2"
    16  
    17  	"github.com/juju/juju/constraints"
    18  	"github.com/juju/juju/environs/config"
    19  	"github.com/juju/juju/storage"
    20  	"github.com/juju/juju/storage/poolmanager"
    21  )
    22  
    23  const (
    24  	EBS_ProviderType = storage.ProviderType("ebs")
    25  
    26  	// Config attributes
    27  
    28  	// The volume type (default standard):
    29  	//   "gp2" for General Purpose (SSD) volumes
    30  	//   "io1" for Provisioned IOPS (SSD) volumes,
    31  	//   "standard" for Magnetic volumes.
    32  	EBS_VolumeType = "volume-type"
    33  
    34  	// The number of I/O operations per second (IOPS) to provision for the volume.
    35  	// Only valid for Provisioned IOPS (SSD) volumes.
    36  	EBS_IOPS = "iops"
    37  
    38  	// Specifies whether the volume should be encrypted.
    39  	EBS_Encrypted = "encrypted"
    40  
    41  	volumeTypeMagnetic        = "magnetic"         // standard
    42  	volumeTypeSsd             = "ssd"              // gp2
    43  	volumeTypeProvisionedIops = "provisioned-iops" // io1
    44  	volumeTypeStandard        = "standard"
    45  	volumeTypeGp2             = "gp2"
    46  	volumeTypeIo1             = "io1"
    47  )
    48  
    49  // AWS error codes
    50  const (
    51  	deviceInUse        = "InvalidDevice.InUse"
    52  	volumeInUse        = "VolumeInUse"
    53  	attachmentNotFound = "InvalidAttachment.NotFound"
    54  	incorrectState     = "IncorrectState"
    55  )
    56  
    57  const (
    58  	volumeStatusAvailable = "available"
    59  	volumeStatusInUse     = "in-use"
    60  	volumeStatusCreating  = "creating"
    61  )
    62  
    63  const (
    64  	// minRootDiskSizeMiB is the minimum/default size (in mebibytes) for ec2 root disks.
    65  	minRootDiskSizeMiB uint64 = 8 * 1024
    66  
    67  	// provisionedIopsvolumeSizeMinGiB is the minimum disk size (in gibibytes)
    68  	// for provisioned IOPS EBS volumes.
    69  	provisionedIopsvolumeSizeMinGiB = 10 // 10 GiB
    70  
    71  	// volumeSizeMaxGiB is the maximum disk size (in gibibytes) for EBS volumes.
    72  	volumeSizeMaxGiB = 1024 // 1024 GiB
    73  
    74  	// maxProvisionedIopsSizeRatio is the maximum allowed ratio of IOPS to
    75  	// size (in GiB), for provisioend IOPS volumes.
    76  	maxProvisionedIopsSizeRatio = 30
    77  
    78  	// devicePrefix is the prefix for device names specified when creating volumes.
    79  	devicePrefix = "/dev/sd"
    80  
    81  	// renamedDevicePrefix is the prefix for device names after they have
    82  	// been renamed. This should replace "devicePrefix" in the device name
    83  	// when recording the block device info in state.
    84  	renamedDevicePrefix = "xvd"
    85  )
    86  
    87  var deviceInUseRegexp = regexp.MustCompile(".*Attachment point .* is already in use")
    88  
    89  func init() {
    90  	ebsssdPool, _ := storage.NewConfig("ebs-ssd", EBS_ProviderType, map[string]interface{}{
    91  		EBS_VolumeType: volumeTypeSsd,
    92  	})
    93  	defaultPools := []*storage.Config{
    94  		ebsssdPool,
    95  	}
    96  	poolmanager.RegisterDefaultStoragePools(defaultPools)
    97  }
    98  
    99  // ebsProvider creates volume sources which use AWS EBS volumes.
   100  type ebsProvider struct{}
   101  
   102  var _ storage.Provider = (*ebsProvider)(nil)
   103  
   104  var ebsConfigFields = schema.Fields{
   105  	storage.Persistent: schema.Bool(),
   106  	EBS_VolumeType: schema.OneOf(
   107  		schema.Const(volumeTypeMagnetic),
   108  		schema.Const(volumeTypeSsd),
   109  		schema.Const(volumeTypeProvisionedIops),
   110  		schema.Const(volumeTypeStandard),
   111  		schema.Const(volumeTypeGp2),
   112  		schema.Const(volumeTypeIo1),
   113  	),
   114  	EBS_IOPS:      schema.ForceInt(),
   115  	EBS_Encrypted: schema.Bool(),
   116  }
   117  
   118  var ebsConfigChecker = schema.FieldMap(
   119  	ebsConfigFields,
   120  	schema.Defaults{
   121  		storage.Persistent: false,
   122  		EBS_VolumeType:     volumeTypeMagnetic,
   123  		EBS_IOPS:           schema.Omit,
   124  		EBS_Encrypted:      false,
   125  	},
   126  )
   127  
   128  type ebsConfig struct {
   129  	persistent bool
   130  	volumeType string
   131  	iops       int
   132  	encrypted  bool
   133  }
   134  
   135  func newEbsConfig(attrs map[string]interface{}) (*ebsConfig, error) {
   136  	out, err := ebsConfigChecker.Coerce(attrs, nil)
   137  	if err != nil {
   138  		return nil, errors.Annotate(err, "validating EBS storage config")
   139  	}
   140  	coerced := out.(map[string]interface{})
   141  	iops, _ := coerced[EBS_IOPS].(int)
   142  	volumeType := coerced[EBS_VolumeType].(string)
   143  	ebsConfig := &ebsConfig{
   144  		persistent: coerced[storage.Persistent].(bool),
   145  		volumeType: volumeType,
   146  		iops:       iops,
   147  		encrypted:  coerced[EBS_Encrypted].(bool),
   148  	}
   149  	switch ebsConfig.volumeType {
   150  	case volumeTypeMagnetic:
   151  		ebsConfig.volumeType = volumeTypeStandard
   152  	case volumeTypeSsd:
   153  		ebsConfig.volumeType = volumeTypeGp2
   154  	case volumeTypeProvisionedIops:
   155  		ebsConfig.volumeType = volumeTypeIo1
   156  	}
   157  	if ebsConfig.iops > 0 && ebsConfig.volumeType != volumeTypeIo1 {
   158  		return nil, errors.Errorf("IOPS specified, but volume type is %q", volumeType)
   159  	} else if ebsConfig.iops == 0 && ebsConfig.volumeType == volumeTypeIo1 {
   160  		return nil, errors.Errorf("volume type is %q, IOPS unspecified or zero", volumeTypeIo1)
   161  	}
   162  	return ebsConfig, nil
   163  }
   164  
   165  // ValidateConfig is defined on the Provider interface.
   166  func (e *ebsProvider) ValidateConfig(cfg *storage.Config) error {
   167  	_, err := newEbsConfig(cfg.Attrs())
   168  	return errors.Trace(err)
   169  }
   170  
   171  // Supports is defined on the Provider interface.
   172  func (e *ebsProvider) Supports(k storage.StorageKind) bool {
   173  	return k == storage.StorageKindBlock
   174  }
   175  
   176  // Scope is defined on the Provider interface.
   177  func (e *ebsProvider) Scope() storage.Scope {
   178  	return storage.ScopeEnviron
   179  }
   180  
   181  // Dynamic is defined on the Provider interface.
   182  func (e *ebsProvider) Dynamic() bool {
   183  	return true
   184  }
   185  
   186  // VolumeSource is defined on the Provider interface.
   187  func (e *ebsProvider) VolumeSource(environConfig *config.Config, cfg *storage.Config) (storage.VolumeSource, error) {
   188  	ec2, _, _, err := awsClients(environConfig)
   189  	if err != nil {
   190  		return nil, errors.Annotate(err, "creating AWS clients")
   191  	}
   192  	source := &ebsVolumeSource{ec2: ec2, envName: environConfig.Name()}
   193  	return source, nil
   194  }
   195  
   196  // FilesystemSource is defined on the Provider interface.
   197  func (e *ebsProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) {
   198  	return nil, errors.NotSupportedf("filesystems")
   199  }
   200  
   201  type ebsVolumeSource struct {
   202  	ec2     *ec2.EC2
   203  	envName string // non-unique, informational only
   204  }
   205  
   206  var _ storage.VolumeSource = (*ebsVolumeSource)(nil)
   207  
   208  // parseVolumeOptions uses storage volume parameters to make a struct used to create volumes.
   209  func parseVolumeOptions(size uint64, attrs map[string]interface{}) (_ ec2.CreateVolume, persistent bool, _ error) {
   210  	ebsConfig, err := newEbsConfig(attrs)
   211  	if err != nil {
   212  		return ec2.CreateVolume{}, false, errors.Trace(err)
   213  	}
   214  	vol := ec2.CreateVolume{
   215  		// Juju size is MiB, AWS size is GiB.
   216  		VolumeSize: int(mibToGib(size)),
   217  		VolumeType: ebsConfig.volumeType,
   218  		Encrypted:  ebsConfig.encrypted,
   219  		IOPS:       int64(ebsConfig.iops),
   220  	}
   221  	return vol, ebsConfig.persistent, nil
   222  }
   223  
   224  // CreateVolumes is specified on the storage.VolumeSource interface.
   225  func (v *ebsVolumeSource) CreateVolumes(params []storage.VolumeParams) (_ []storage.Volume, _ []storage.VolumeAttachment, err error) {
   226  	volumes := make([]storage.Volume, 0, len(params))
   227  	volumeAttachments := make([]storage.VolumeAttachment, 0, len(params))
   228  
   229  	// If there's an error, we delete any ones that are created.
   230  	defer func() {
   231  		if err != nil && len(volumes) > 0 {
   232  			volIds := make([]string, len(volumes))
   233  			for i, v := range volumes {
   234  				volIds[i] = v.VolumeId
   235  			}
   236  			err2 := v.DestroyVolumes(volIds)
   237  			for i, volErr := range err2 {
   238  				if volErr == nil {
   239  					continue
   240  				}
   241  				logger.Warningf("error cleaning up volume %v: %v", volumes[i].Tag, volErr)
   242  			}
   243  		}
   244  	}()
   245  
   246  	// TODO(axw) if preference is to use ephemeral, use ephemeral
   247  	// until the instance stores run out. We'll need to know how
   248  	// many there are and how big each one is. We also need to
   249  	// unmap ephemeral0 in cloud-init.
   250  
   251  	// First, validate the params before we use them.
   252  	instanceIds := set.NewStrings()
   253  	for _, p := range params {
   254  		if err := v.ValidateVolumeParams(p); err != nil {
   255  			return nil, nil, errors.Trace(err)
   256  		}
   257  		instanceIds.Add(string(p.Attachment.InstanceId))
   258  	}
   259  	instances, err := v.instances(instanceIds.Values())
   260  	if err != nil {
   261  		return nil, nil, errors.Annotate(err, "querying instance details")
   262  	}
   263  
   264  	for _, p := range params {
   265  		instId := string(p.Attachment.InstanceId)
   266  		vol, persistent, _ := parseVolumeOptions(p.Size, p.Attributes)
   267  		vol.AvailZone = instances[instId].AvailZone
   268  		resp, err := v.ec2.CreateVolume(vol)
   269  		if err != nil {
   270  			return nil, nil, err
   271  		}
   272  		volumeId := resp.Id
   273  		volumes = append(volumes, storage.Volume{
   274  			p.Tag,
   275  			storage.VolumeInfo{
   276  				VolumeId: volumeId,
   277  				Size:     gibToMib(uint64(resp.Size)),
   278  				// TODO(axw) Later, when we handle destruction of
   279  				// volumes within Juju, we should not mark any
   280  				// EBS volumes as persistent.
   281  				Persistent: persistent,
   282  			},
   283  		})
   284  
   285  		resourceTags := make(map[string]string)
   286  		for k, v := range p.ResourceTags {
   287  			resourceTags[k] = v
   288  		}
   289  		resourceTags[tagName] = resourceName(p.Tag, v.envName)
   290  		if err := tagResources(v.ec2, resourceTags, volumeId); err != nil {
   291  			return nil, nil, errors.Annotate(err, "tagging volume")
   292  		}
   293  
   294  		nextDeviceName := blockDeviceNamer(instances[instId])
   295  		requestDeviceName, actualDeviceName, err := v.attachOneVolume(nextDeviceName, resp.Volume.Id, instId, false)
   296  		if err != nil {
   297  			return nil, nil, errors.Annotatef(err, "attaching %v to %v", resp.Volume.Id, instId)
   298  		}
   299  		_, err = v.ec2.ModifyInstanceAttribute(&ec2.ModifyInstanceAttribute{
   300  			InstanceId: instId,
   301  			BlockDeviceMappings: []ec2.InstanceBlockDeviceMapping{{
   302  				DeviceName:          requestDeviceName,
   303  				VolumeId:            volumeId,
   304  				DeleteOnTermination: !persistent,
   305  			}},
   306  		}, nil)
   307  		if err != nil {
   308  			return nil, nil, errors.Annotatef(err, "binding termination of %v to %v", resp.Volume.Id, instId)
   309  		}
   310  		volumeAttachments = append(volumeAttachments, storage.VolumeAttachment{
   311  			p.Tag,
   312  			p.Attachment.Machine,
   313  			storage.VolumeAttachmentInfo{
   314  				DeviceName: actualDeviceName,
   315  			},
   316  		})
   317  	}
   318  	return volumes, volumeAttachments, nil
   319  }
   320  
   321  // DescribeVolumes is specified on the storage.VolumeSource interface.
   322  func (v *ebsVolumeSource) DescribeVolumes(volIds []string) ([]storage.VolumeInfo, error) {
   323  	resp, err := v.ec2.Volumes(volIds, nil)
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	vols := make([]storage.VolumeInfo, len(resp.Volumes))
   328  	for i, vol := range resp.Volumes {
   329  		vols[i] = storage.VolumeInfo{
   330  			Size:     gibToMib(uint64(vol.Size)),
   331  			VolumeId: vol.Id,
   332  		}
   333  		for _, attachment := range vol.Attachments {
   334  			if !attachment.DeleteOnTermination {
   335  				vols[i].Persistent = true
   336  				break
   337  			}
   338  		}
   339  	}
   340  	return vols, nil
   341  }
   342  
   343  // DestroyVolumes is specified on the storage.VolumeSource interface.
   344  func (v *ebsVolumeSource) DestroyVolumes(volIds []string) []error {
   345  	results := make([]error, len(volIds))
   346  	for i, volumeId := range volIds {
   347  		if _, err := v.ec2.DeleteVolume(volumeId); err != nil {
   348  			results[i] = errors.Annotatef(err, "destroying %q", volumeId)
   349  		}
   350  	}
   351  	return results
   352  }
   353  
   354  // ValidateVolumeParams is specified on the storage.VolumeSource interface.
   355  func (v *ebsVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error {
   356  	vol, _, err := parseVolumeOptions(params.Size, params.Attributes)
   357  	if err != nil {
   358  		return err
   359  	}
   360  	if vol.VolumeSize > volumeSizeMaxGiB {
   361  		return errors.Errorf("%d GiB exceeds the maximum of %d GiB", vol.VolumeSize, volumeSizeMaxGiB)
   362  	}
   363  	if vol.VolumeType == volumeTypeIo1 {
   364  		if vol.VolumeSize < provisionedIopsvolumeSizeMinGiB {
   365  			return errors.Errorf(
   366  				"volume size is %d GiB, must be at least %d GiB for provisioned IOPS",
   367  				vol.VolumeSize,
   368  				provisionedIopsvolumeSizeMinGiB,
   369  			)
   370  		}
   371  	}
   372  	if vol.IOPS > 0 {
   373  		minSize := int(vol.IOPS / maxProvisionedIopsSizeRatio)
   374  		if vol.VolumeSize < minSize {
   375  			return errors.Errorf(
   376  				"volume size is %d GiB, must be at least %d GiB to support %d IOPS",
   377  				vol.VolumeSize, minSize, vol.IOPS,
   378  			)
   379  		}
   380  	}
   381  	return nil
   382  }
   383  
   384  // AttachVolumes is specified on the storage.VolumeSource interface.
   385  func (v *ebsVolumeSource) AttachVolumes(attachParams []storage.VolumeAttachmentParams) (attachments []storage.VolumeAttachment, err error) {
   386  	// If there's an error, we detach any ones that are attached.
   387  	var attached []storage.VolumeAttachmentParams
   388  	defer func() {
   389  		if err != nil && len(attachments) > 0 {
   390  			err2 := v.DetachVolumes(attached)
   391  			if err2 != nil {
   392  				logger.Warningf("error detaching volumes: %v", err2)
   393  			}
   394  		}
   395  	}()
   396  
   397  	// We need the virtualisation types for each instance we are
   398  	// attaching to so we can determine the device name.
   399  	instIds := set.NewStrings()
   400  	for _, p := range attachParams {
   401  		instIds.Add(string(p.InstanceId))
   402  	}
   403  	instances, err := v.instances(instIds.Values())
   404  	if err != nil {
   405  		return nil, errors.Trace(err)
   406  	}
   407  
   408  	for _, params := range attachParams {
   409  		instId := string(params.InstanceId)
   410  		nextDeviceName := blockDeviceNamer(instances[instId])
   411  		_, deviceName, err := v.attachOneVolume(nextDeviceName, params.VolumeId, instId, false)
   412  		if err != nil {
   413  			return nil, errors.Annotatef(err, "attaching %v to %v", params.VolumeId, params.InstanceId)
   414  		}
   415  		attached = append(attached, params)
   416  		attachments = append(attachments, storage.VolumeAttachment{
   417  			params.Volume,
   418  			params.Machine,
   419  			storage.VolumeAttachmentInfo{
   420  				DeviceName: deviceName,
   421  			},
   422  		})
   423  	}
   424  	return attachments, nil
   425  }
   426  
   427  func (v *ebsVolumeSource) attachOneVolume(
   428  	nextDeviceName func() (string, string, error),
   429  	volumeId, instId string,
   430  	deleteOnTermination bool,
   431  ) (string, string, error) {
   432  	// Wait for the volume to move out of "creating".
   433  	volume, err := v.waitVolumeCreated(volumeId)
   434  	if err != nil {
   435  		return "", "", errors.Trace(err)
   436  	}
   437  
   438  	// Possible statuses:
   439  	//    creating | available | in-use | deleting | deleted | error
   440  	switch volume.Status {
   441  	default:
   442  		return "", "", errors.Errorf("cannot attach to volume with status %q", volume.Status)
   443  
   444  	case volumeStatusInUse:
   445  		// Volume is already attached; see if it's attached to the
   446  		// instance requested.
   447  		attachments := volume.Attachments
   448  		if len(attachments) != 1 {
   449  			return "", "", errors.Annotatef(err, "volume %v has unexpected attachment count: %v", volumeId, len(attachments))
   450  		}
   451  		if attachments[0].InstanceId != instId {
   452  			return "", "", errors.Annotatef(err, "volume %v is attached to %v", volumeId, attachments[0].InstanceId)
   453  		}
   454  		requestDeviceName := attachments[0].Device
   455  		actualDeviceName := renamedDevicePrefix + requestDeviceName[len(devicePrefix):]
   456  		return requestDeviceName, actualDeviceName, nil
   457  
   458  	case volumeStatusAvailable:
   459  		// Attempt to attach below.
   460  		break
   461  	}
   462  
   463  	for {
   464  		requestDeviceName, actualDeviceName, err := nextDeviceName()
   465  		if err != nil {
   466  			// Can't attach any more volumes.
   467  			return "", "", err
   468  		}
   469  		_, err = v.ec2.AttachVolume(volumeId, instId, requestDeviceName)
   470  		if ec2Err, ok := err.(*ec2.Error); ok {
   471  			switch ec2Err.Code {
   472  			case invalidParameterValue:
   473  				// InvalidParameterValue is returned by AttachVolume
   474  				// rather than InvalidDevice.InUse as the docs would
   475  				// suggest.
   476  				if !deviceInUseRegexp.MatchString(ec2Err.Message) {
   477  					break
   478  				}
   479  				fallthrough
   480  
   481  			case deviceInUse:
   482  				// deviceInUse means that the requested device name
   483  				// is in use already. Try again with the next name.
   484  				continue
   485  			}
   486  		}
   487  		if err != nil {
   488  			return "", "", errors.Annotate(err, "attaching volume")
   489  		}
   490  		return requestDeviceName, actualDeviceName, nil
   491  	}
   492  }
   493  
   494  func (v *ebsVolumeSource) waitVolumeCreated(volumeId string) (*ec2.Volume, error) {
   495  	var attempt = utils.AttemptStrategy{
   496  		Total: 5 * time.Second,
   497  		Delay: 200 * time.Millisecond,
   498  	}
   499  	for a := attempt.Start(); a.Next(); {
   500  		volume, err := v.describeVolume(volumeId)
   501  		if err != nil {
   502  			return nil, errors.Trace(err)
   503  		}
   504  		if volume.Status != volumeStatusCreating {
   505  			return volume, nil
   506  		}
   507  	}
   508  	return nil, errors.Errorf("timed out waiting for volume %v to become available", volumeId)
   509  }
   510  
   511  func (v *ebsVolumeSource) describeVolume(volumeId string) (*ec2.Volume, error) {
   512  	resp, err := v.ec2.Volumes([]string{volumeId}, nil)
   513  	if err != nil {
   514  		return nil, errors.Annotate(err, "querying volume")
   515  	}
   516  	if len(resp.Volumes) != 1 {
   517  		return nil, errors.Errorf("expected one volume, got %d", len(resp.Volumes))
   518  	}
   519  	return &resp.Volumes[0], nil
   520  }
   521  
   522  // instances returns a mapping from the specified instance IDs to ec2.Instance
   523  // structures. If any of the specified IDs does not refer to a running instance,
   524  // it will cause an error to be returned.
   525  func (v *ebsVolumeSource) instances(instIds []string) (map[string]ec2.Instance, error) {
   526  	instances := make(map[string]ec2.Instance)
   527  	// Can only attach to running instances.
   528  	filter := ec2.NewFilter()
   529  	filter.Add("instance-state-name", "running")
   530  	resp, err := v.ec2.Instances(instIds, filter)
   531  	if err != nil {
   532  		return nil, err
   533  	}
   534  	for j := range resp.Reservations {
   535  		r := &resp.Reservations[j]
   536  		for _, inst := range r.Instances {
   537  			instances[inst.InstanceId] = inst
   538  		}
   539  	}
   540  	// TODO(wallyworld) - retry to allow instances to get to running state.
   541  	if len(instances) < len(instIds) {
   542  		notRunning := set.NewStrings(instIds...)
   543  		for id, _ := range instances {
   544  			notRunning.Remove(id)
   545  		}
   546  		return nil, errors.Errorf(
   547  			"volumes can only be attached to running instances, these instances are not running: %v",
   548  			strings.Join(notRunning.Values(), ","),
   549  		)
   550  	}
   551  	return instances, nil
   552  }
   553  
   554  // DetachVolumes is specified on the storage.VolumeSource interface.
   555  func (v *ebsVolumeSource) DetachVolumes(attachParams []storage.VolumeAttachmentParams) error {
   556  	for _, params := range attachParams {
   557  		_, err := v.ec2.DetachVolume(params.VolumeId, string(params.InstanceId), "", false)
   558  		// Process aws specific error information.
   559  		if err != nil {
   560  			if ec2Err, ok := err.(*ec2.Error); ok {
   561  				switch ec2Err.Code {
   562  				// attachment not found means this volume is already detached.
   563  				case attachmentNotFound:
   564  					err = nil
   565  				}
   566  			}
   567  		}
   568  		if err != nil {
   569  			return errors.Annotatef(err, "detaching %v from %v", params.Volume, params.Machine)
   570  		}
   571  	}
   572  	return nil
   573  }
   574  
   575  var errTooManyVolumes = errors.New("too many EBS volumes to attach")
   576  
   577  // blockDeviceNamer returns a function that cycles through block device names.
   578  //
   579  // The returned function returns the device name that should be used in
   580  // requests to the EC2 API, and and also the (kernel) device name as it
   581  // will appear on the machine.
   582  //
   583  // See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
   584  func blockDeviceNamer(inst ec2.Instance) func() (requestName, actualName string, err error) {
   585  	const (
   586  		// deviceLetterMin is the first letter to use for EBS block device names.
   587  		deviceLetterMin = 'f'
   588  		// deviceLetterMax is the last letter to use for EBS block device names.
   589  		deviceLetterMax = 'p'
   590  		// deviceNumMax is the maximum value for trailing numbers on block device name.
   591  		deviceNumMax = 6
   592  	)
   593  	var n int
   594  	letterRepeats := 1
   595  	numbers := inst.VirtType == "paravirtual"
   596  	if numbers {
   597  		letterRepeats = deviceNumMax
   598  	}
   599  	return func() (string, string, error) {
   600  		letter := deviceLetterMin + (n / letterRepeats)
   601  		if letter > deviceLetterMax {
   602  			return "", "", errTooManyVolumes
   603  		}
   604  		deviceName := devicePrefix + string(letter)
   605  		if numbers {
   606  			deviceName += string('1' + (n % deviceNumMax))
   607  		}
   608  		n++
   609  		realDeviceName := renamedDevicePrefix + deviceName[len(devicePrefix):]
   610  		return deviceName, realDeviceName, nil
   611  	}
   612  }
   613  
   614  // getBlockDeviceMappings translates constraints into BlockDeviceMappings.
   615  //
   616  // The first entry is always the root disk mapping, followed by instance
   617  // stores (ephemeral disks).
   618  func getBlockDeviceMappings(cons constraints.Value) ([]ec2.BlockDeviceMapping, error) {
   619  	rootDiskSizeMiB := minRootDiskSizeMiB
   620  	if cons.RootDisk != nil {
   621  		if *cons.RootDisk >= minRootDiskSizeMiB {
   622  			rootDiskSizeMiB = *cons.RootDisk
   623  		} else {
   624  			logger.Infof(
   625  				"Ignoring root-disk constraint of %dM because it is smaller than the EC2 image size of %dM",
   626  				*cons.RootDisk,
   627  				minRootDiskSizeMiB,
   628  			)
   629  		}
   630  	}
   631  
   632  	// The first block device is for the root disk.
   633  	blockDeviceMappings := []ec2.BlockDeviceMapping{{
   634  		DeviceName: "/dev/sda1",
   635  		VolumeSize: int64(mibToGib(rootDiskSizeMiB)),
   636  	}}
   637  
   638  	// Not all machines have this many instance stores.
   639  	// Instances will be started with as many of the
   640  	// instance stores as they can support.
   641  	blockDeviceMappings = append(blockDeviceMappings, []ec2.BlockDeviceMapping{{
   642  		VirtualName: "ephemeral0",
   643  		DeviceName:  "/dev/sdb",
   644  	}, {
   645  		VirtualName: "ephemeral1",
   646  		DeviceName:  "/dev/sdc",
   647  	}, {
   648  		VirtualName: "ephemeral2",
   649  		DeviceName:  "/dev/sdd",
   650  	}, {
   651  		VirtualName: "ephemeral3",
   652  		DeviceName:  "/dev/sde",
   653  	}}...)
   654  
   655  	return blockDeviceMappings, nil
   656  }
   657  
   658  // mibToGib converts mebibytes to gibibytes.
   659  // AWS expects GiB, we work in MiB; round up
   660  // to nearest GiB.
   661  func mibToGib(m uint64) uint64 {
   662  	return (m + 1023) / 1024
   663  }
   664  
   665  // gibToMib converts gibibytes to mebibytes.
   666  func gibToMib(g uint64) uint64 {
   667  	return g * 1024
   668  }