github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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  	stderrors "errors"
     8  	"fmt"
     9  	"regexp"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/aws/aws-sdk-go-v2/aws"
    15  	"github.com/aws/aws-sdk-go-v2/service/ec2"
    16  	"github.com/aws/aws-sdk-go-v2/service/ec2/types"
    17  	"github.com/aws/smithy-go"
    18  	"github.com/juju/collections/set"
    19  	"github.com/juju/errors"
    20  	"github.com/juju/names/v5"
    21  	"github.com/juju/schema"
    22  	"github.com/juju/utils/v3"
    23  
    24  	"github.com/juju/juju/core/constraints"
    25  	"github.com/juju/juju/core/instance"
    26  	"github.com/juju/juju/core/os/ostype"
    27  	"github.com/juju/juju/environs/context"
    28  	"github.com/juju/juju/environs/tags"
    29  	"github.com/juju/juju/provider/common"
    30  	"github.com/juju/juju/storage"
    31  )
    32  
    33  const (
    34  	EBS_ProviderType = storage.ProviderType("ebs")
    35  
    36  	// Config attributes
    37  
    38  	// EBS_VolumeType is the ebs volume type (default standard):
    39  	//   "gp2" for General Purpose (SSD) volumes
    40  	//   "io1" for Provisioned IOPS (SSD) volumes,
    41  	//   "standard" for Magnetic volumes.
    42  	//   see volumes types below for more.
    43  	EBS_VolumeType = "volume-type"
    44  
    45  	// EBS_IOPS is the number of I/O operations per second (IOPS) per GiB
    46  	// to provision for the volume. Only valid for io1 io2 and gp3 volumes.
    47  	EBS_IOPS = "iops"
    48  
    49  	// EBS_Throughput is the max transfer troughput for gp3 volumes.
    50  	EBS_Throughput = "throughput"
    51  
    52  	// EBS_Encrypted specifies whether the volume should be encrypted.
    53  	EBS_Encrypted = "encrypted"
    54  
    55  	// EBS_KMSKeyID specifies what encryption key to use for the EBS volume.
    56  	EBS_KMSKeyID = "kms-key-id"
    57  
    58  	// Volume Aliases
    59  	// TODO(juju3): remove volume aliases and use the raw AWS names.
    60  	volumeAliasMagnetic        = "magnetic"         // standard
    61  	volumeAliasOptimizedHDD    = "optimized-hdd"    // sc1
    62  	volumeAliasColdStorage     = "cold-storage"     // sc1
    63  	volumeAliasSSD             = "ssd"              // gp2
    64  	volumeAliasProvisionedIops = "provisioned-iops" // io1
    65  
    66  	// Volume types
    67  	volumeTypeStandard = "standard"
    68  	volumeTypeGP2      = "gp2"
    69  	volumeTypeGP3      = "gp3"
    70  	volumeTypeIO1      = "io1"
    71  	volumeTypeIO2      = "io2"
    72  	volumeTypeST1      = "st1"
    73  	volumeTypeSC1      = "sc1"
    74  
    75  	rootDiskDeviceName = "/dev/sda1"
    76  
    77  	nvmeDeviceLinkPrefix = "/dev/disk/by-id/nvme-Amazon_Elastic_Block_Store_"
    78  
    79  	// defaultControllerDiskSizeMiB is the default size for the
    80  	// root disk of controller machines, if no root-disk constraint
    81  	// is specified.
    82  	defaultControllerDiskSizeMiB = 32 * 1024
    83  )
    84  
    85  // AWS error codes
    86  const (
    87  	deviceInUse        = "InvalidDevice.InUse"
    88  	attachmentNotFound = "InvalidAttachment.NotFound"
    89  	volumeNotFound     = "InvalidVolume.NotFound"
    90  	incorrectState     = "IncorrectState"
    91  )
    92  
    93  const (
    94  	volumeStatusAvailable = "available"
    95  	volumeStatusInUse     = "in-use"
    96  	volumeStatusCreating  = "creating"
    97  
    98  	attachmentStatusAttaching = "attaching"
    99  	attachmentStatusAttached  = "attached"
   100  
   101  	instanceStateShuttingDown = "shutting-down"
   102  	instanceStateTerminated   = "terminated"
   103  )
   104  
   105  // Limits for volume parameters. See:
   106  //
   107  //	http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html
   108  const (
   109  	// minMagneticVolumeSizeGiB is the minimum size for magnetic volumes in GiB.
   110  	minMagneticVolumeSizeGiB = 1
   111  
   112  	// maxMagneticVolumeSizeGiB is the maximum size for magnetic volumes in GiB.
   113  	maxMagneticVolumeSizeGiB = 1024
   114  
   115  	// minSSDVolumeSizeGiB is the minimum size for SSD volumes in GiB.
   116  	minSSDVolumeSizeGiB = 1
   117  
   118  	// maxSSDVolumeSizeGiB is the maximum size for SSD volumes in GiB.
   119  	maxSSDVolumeSizeGiB = 16 * 1024
   120  
   121  	// minProvisionedIopsVolumeSizeGiB is the minimum size of provisioned IOPS
   122  	// volumes in GiB.
   123  	minProvisionedIopsVolumeSizeGiB = 4
   124  
   125  	// maxProvisionedIopsVolumeSizeGiB is the maximum size of provisioned IOPS
   126  	// volumes in GiB.
   127  	maxProvisionedIopsVolumeSizeGiB = 16 * 1024
   128  
   129  	// maxProvisionedIopsSizeRatio is the maximum allowed ratio of IOPS to
   130  	// size (in GiB), for provisioend IOPS volumes.
   131  	maxProvisionedIopsSizeRatio = 30
   132  
   133  	// maxProvisionedIops is the maximum allowed IOPS in total for provisioned IOPS
   134  	// volumes. We take the minimum of volumeSize*maxProvisionedIopsSizeRatio and
   135  	// maxProvisionedIops.
   136  	maxProvisionedIops = 20000
   137  
   138  	// minSt1VolumeSizeGiB is the minimum volume size for st1 volume instances.
   139  	minSt1VolumeSizeGiB = 500
   140  
   141  	// maxSt1VolumeSizeGiB is the maximum volume size for st1 volume instances.
   142  	maxSt1VolumeSizeGiB = 16 * 1024
   143  
   144  	// minSc1VolumeSizeGiB is the minimum volume size for sc1 volume instances.
   145  	minSc1VolumeSizeGiB = 500
   146  
   147  	// maxSc1VolumeSizeGiB is the maximum volume size for sc1 volume instances.
   148  	maxSc1VolumeSizeGiB = 16 * 1024
   149  )
   150  
   151  const (
   152  	// devicePrefix is the prefix for device names specified when creating volumes.
   153  	devicePrefix = "/dev/sd"
   154  
   155  	// renamedDevicePrefix is the prefix for device names after they have
   156  	// been renamed. This should replace "devicePrefix" in the device name
   157  	// when recording the block device info in state.
   158  	renamedDevicePrefix = "xvd"
   159  )
   160  
   161  var deviceInUseRegexp = regexp.MustCompile(".*Attachment point .* is already in use")
   162  
   163  // StorageProviderTypes implements storage.ProviderRegistry.
   164  func (e *environ) StorageProviderTypes() ([]storage.ProviderType, error) {
   165  	return []storage.ProviderType{EBS_ProviderType}, nil
   166  }
   167  
   168  // StorageProvider implements storage.ProviderRegistry.
   169  func (e *environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) {
   170  	if t == EBS_ProviderType {
   171  		return &ebsProvider{e}, nil
   172  	}
   173  	return nil, errors.NotFoundf("storage provider %q", t)
   174  }
   175  
   176  // ebsProvider creates volume sources which use AWS EBS volumes.
   177  type ebsProvider struct {
   178  	env *environ
   179  }
   180  
   181  var _ storage.Provider = (*ebsProvider)(nil)
   182  
   183  var ebsConfigFields = schema.Fields{
   184  	EBS_VolumeType: schema.OneOf(
   185  		schema.Const(volumeAliasMagnetic),
   186  		schema.Const(volumeAliasOptimizedHDD),
   187  		schema.Const(volumeAliasColdStorage),
   188  		schema.Const(volumeAliasSSD),
   189  		schema.Const(volumeAliasProvisionedIops),
   190  		schema.Const(volumeTypeStandard),
   191  		schema.Const(volumeTypeGP2),
   192  		schema.Const(volumeTypeGP3),
   193  		schema.Const(volumeTypeIO1),
   194  		schema.Const(volumeTypeIO2),
   195  		schema.Const(volumeTypeST1),
   196  		schema.Const(volumeTypeSC1),
   197  	),
   198  	EBS_IOPS:       schema.ForceInt(),
   199  	EBS_Encrypted:  schema.Bool(),
   200  	EBS_KMSKeyID:   schema.String(),
   201  	EBS_Throughput: schema.String(),
   202  }
   203  
   204  var ebsConfigChecker = schema.FieldMap(
   205  	ebsConfigFields,
   206  	schema.Defaults{
   207  		EBS_VolumeType: volumeTypeGP2,
   208  		EBS_IOPS:       schema.Omit,
   209  		EBS_Encrypted:  false,
   210  		EBS_KMSKeyID:   schema.Omit,
   211  		EBS_Throughput: schema.Omit,
   212  	},
   213  )
   214  
   215  type ebsConfig struct {
   216  	volumeType   string
   217  	iops         int
   218  	encrypted    bool
   219  	kmsKeyID     string
   220  	throughputMB int
   221  }
   222  
   223  func newEbsConfig(attrs map[string]interface{}) (*ebsConfig, error) {
   224  	out, err := ebsConfigChecker.Coerce(attrs, nil)
   225  	if err != nil {
   226  		return nil, errors.Annotate(err, "validating EBS storage config")
   227  	}
   228  	coerced := out.(map[string]interface{})
   229  	iops, _ := coerced[EBS_IOPS].(int)
   230  	volumeType := coerced[EBS_VolumeType].(string)
   231  	kmsKeyID, _ := coerced[EBS_KMSKeyID].(string)
   232  	throughput, _ := coerced[EBS_Throughput].(string)
   233  	ebsConfig := &ebsConfig{
   234  		volumeType: volumeType,
   235  		iops:       iops,
   236  		encrypted:  coerced[EBS_Encrypted].(bool),
   237  		kmsKeyID:   kmsKeyID,
   238  	}
   239  	switch ebsConfig.volumeType {
   240  	case volumeAliasMagnetic:
   241  		ebsConfig.volumeType = volumeTypeStandard
   242  	case volumeAliasColdStorage:
   243  		ebsConfig.volumeType = volumeTypeSC1
   244  	case volumeAliasOptimizedHDD:
   245  		ebsConfig.volumeType = volumeTypeST1
   246  	case volumeAliasSSD:
   247  		ebsConfig.volumeType = volumeTypeGP2
   248  	case volumeAliasProvisionedIops:
   249  		ebsConfig.volumeType = volumeTypeIO1
   250  	}
   251  	if throughput != "" {
   252  		throughputMB, err := utils.ParseSize(throughput)
   253  		if err != nil {
   254  			return nil, errors.Annotatef(err, "parsing %q", EBS_Throughput)
   255  		}
   256  		ebsConfig.throughputMB = int(throughputMB)
   257  	}
   258  	switch ebsConfig.volumeType {
   259  	case volumeTypeIO1, volumeTypeIO2:
   260  		if ebsConfig.iops == 0 {
   261  			return nil, errors.Errorf("volume type is %q, IOPS unspecified or zero", volumeTypeIO1)
   262  		}
   263  	case volumeTypeGP3:
   264  		// iops is optional
   265  	default:
   266  		if ebsConfig.iops > 0 {
   267  			return nil, errors.Errorf("IOPS specified, but volume type is %q", volumeType)
   268  		}
   269  	}
   270  	if ebsConfig.throughputMB != 0 && ebsConfig.volumeType != volumeTypeGP3 {
   271  		return nil, errors.Errorf("%q cannot be specified when volume type is %q", EBS_Throughput, volumeType)
   272  	}
   273  	return ebsConfig, nil
   274  }
   275  
   276  func (e *ebsProvider) ValidateForK8s(map[string]any) error {
   277  	// no validation required
   278  	return nil
   279  }
   280  
   281  // ValidateConfig is defined on the Provider interface.
   282  func (e *ebsProvider) ValidateConfig(cfg *storage.Config) error {
   283  	_, err := newEbsConfig(cfg.Attrs())
   284  	return errors.Trace(err)
   285  }
   286  
   287  // Supports is defined on the Provider interface.
   288  func (e *ebsProvider) Supports(k storage.StorageKind) bool {
   289  	return k == storage.StorageKindBlock
   290  }
   291  
   292  // Scope is defined on the Provider interface.
   293  func (e *ebsProvider) Scope() storage.Scope {
   294  	return storage.ScopeEnviron
   295  }
   296  
   297  // Dynamic is defined on the Provider interface.
   298  func (e *ebsProvider) Dynamic() bool {
   299  	return true
   300  }
   301  
   302  // Releasable is defined on the Provider interface.
   303  func (*ebsProvider) Releasable() bool {
   304  	return true
   305  }
   306  
   307  // DefaultPools is defined on the Provider interface.
   308  func (e *ebsProvider) DefaultPools() []*storage.Config {
   309  	ssdPool, _ := storage.NewConfig("ebs-ssd", EBS_ProviderType, map[string]interface{}{
   310  		EBS_VolumeType: volumeAliasSSD,
   311  	})
   312  	return []*storage.Config{ssdPool}
   313  }
   314  
   315  // VolumeSource is defined on the Provider interface.
   316  func (e *ebsProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) {
   317  	environConfig := e.env.Config()
   318  	source := &ebsVolumeSource{
   319  		env:       e.env,
   320  		envName:   environConfig.Name(),
   321  		modelUUID: environConfig.UUID(),
   322  	}
   323  	return source, nil
   324  }
   325  
   326  // FilesystemSource is defined on the Provider interface.
   327  func (e *ebsProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) {
   328  	return nil, errors.NotSupportedf("filesystems")
   329  }
   330  
   331  type ebsVolumeSource struct {
   332  	env       *environ
   333  	envName   string // non-unique, informational only
   334  	modelUUID string
   335  }
   336  
   337  var _ storage.VolumeSource = (*ebsVolumeSource)(nil)
   338  
   339  // parseVolumeOptions uses storage volume parameters to make a struct used to create volumes.
   340  func parseVolumeOptions(size uint64, attrs map[string]interface{}) (_ ec2.CreateVolumeInput, _ error) {
   341  	ebsConfig, err := newEbsConfig(attrs)
   342  	if err != nil {
   343  		return ec2.CreateVolumeInput{}, errors.Trace(err)
   344  	}
   345  	if ebsConfig.iops > maxProvisionedIopsSizeRatio {
   346  		return ec2.CreateVolumeInput{}, errors.Errorf(
   347  			"specified IOPS ratio is %d/GiB, maximum is %d/GiB",
   348  			ebsConfig.iops, maxProvisionedIopsSizeRatio,
   349  		)
   350  	}
   351  
   352  	sizeInGib := mibToGib(size)
   353  	iops := uint64(ebsConfig.iops) * sizeInGib
   354  	if iops > maxProvisionedIops {
   355  		iops = maxProvisionedIops
   356  	}
   357  	vol := ec2.CreateVolumeInput{
   358  		// Juju size is MiB, AWS size is GiB.
   359  		Size:       aws.Int32(int32(sizeInGib)),
   360  		VolumeType: types.VolumeType(ebsConfig.volumeType),
   361  		Encrypted:  aws.Bool(ebsConfig.encrypted),
   362  	}
   363  	if ebsConfig.kmsKeyID != "" {
   364  		vol.KmsKeyId = aws.String(ebsConfig.kmsKeyID)
   365  	}
   366  	if iops > 0 {
   367  		vol.Iops = aws.Int32(int32(iops))
   368  	}
   369  	if ebsConfig.throughputMB > 0 {
   370  		vol.Throughput = aws.Int32(int32(ebsConfig.throughputMB))
   371  	}
   372  	return vol, nil
   373  }
   374  
   375  // CreateVolumes is specified on the storage.VolumeSource interface.
   376  func (v *ebsVolumeSource) CreateVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams) (_ []storage.CreateVolumesResult, err error) {
   377  	// First, validate the params before we use them.
   378  	results := make([]storage.CreateVolumesResult, len(params))
   379  	instanceIds := set.NewStrings()
   380  	for i, p := range params {
   381  		if err := v.ValidateVolumeParams(p); err != nil {
   382  			results[i].Error = err
   383  			continue
   384  		}
   385  		instanceIds.Add(string(p.Attachment.InstanceId))
   386  	}
   387  
   388  	instances := make(instanceCache)
   389  	if instanceIds.Size() > 1 {
   390  		if err := instances.update(v.env.ec2Client, ctx, instanceIds.Values()...); err != nil {
   391  			err := maybeConvertCredentialError(err, ctx)
   392  			logger.Debugf("querying running instances: %v", err)
   393  			// We ignore the error, because we don't want an invalid
   394  			// InstanceId reference from one VolumeParams to prevent
   395  			// the creation of another volume.
   396  			// Except if it is a credential error...
   397  			if errors.Is(err, common.ErrorCredentialNotValid) {
   398  				return nil, errors.Trace(err)
   399  			}
   400  		}
   401  	}
   402  
   403  	for i, p := range params {
   404  		if results[i].Error != nil {
   405  			continue
   406  		}
   407  		volume, attachment, err := v.createVolume(ctx, p, instances)
   408  		if err != nil {
   409  			results[i].Error = err
   410  			continue
   411  		}
   412  		results[i].Volume = volume
   413  		results[i].VolumeAttachment = attachment
   414  	}
   415  	return results, nil
   416  }
   417  
   418  func (v *ebsVolumeSource) createVolume(ctx context.ProviderCallContext, p storage.VolumeParams, instances instanceCache) (_ *storage.Volume, _ *storage.VolumeAttachment, err error) {
   419  	var volumeId *string
   420  	defer func() {
   421  		if err == nil || volumeId == nil {
   422  			return
   423  		}
   424  		if _, err := v.env.ec2Client.DeleteVolume(ctx, &ec2.DeleteVolumeInput{
   425  			VolumeId: volumeId,
   426  		}); err != nil {
   427  			logger.Errorf("error cleaning up volume %v: %v", *volumeId, maybeConvertCredentialError(err, ctx))
   428  		}
   429  	}()
   430  
   431  	// TODO(axw) if preference is to use ephemeral, use ephemeral
   432  	// until the instance stores run out. We'll need to know how
   433  	// many there are and how big each one is. We also need to
   434  	// unmap ephemeral0 in cloud-init.
   435  
   436  	// Create.
   437  	instId := string(p.Attachment.InstanceId)
   438  	if err := instances.update(v.env.ec2Client, ctx, instId); err != nil {
   439  		return nil, nil, errors.Trace(maybeConvertCredentialError(err, ctx))
   440  	}
   441  	inst, err := instances.get(instId)
   442  	if err != nil {
   443  		// Can't create the volume without the instance,
   444  		// because we need to know what its AZ is.
   445  		return nil, nil, errors.Trace(maybeConvertCredentialError(err, ctx))
   446  	}
   447  	vol, _ := parseVolumeOptions(p.Size, p.Attributes)
   448  	if inst.Placement != nil {
   449  		vol.AvailabilityZone = inst.Placement.AvailabilityZone
   450  	}
   451  
   452  	// Tag.
   453  	resourceTags := make(map[string]string)
   454  	for k, v := range p.ResourceTags {
   455  		resourceTags[k] = v
   456  	}
   457  	resourceTags[tagName] = resourceName(p.Tag, v.envName)
   458  	vol.TagSpecifications = []types.TagSpecification{
   459  		CreateTagSpecification(types.ResourceTypeVolume, resourceTags),
   460  	}
   461  
   462  	resp, err := v.env.ec2Client.CreateVolume(ctx, &vol)
   463  	if err != nil {
   464  		return nil, nil, errors.Trace(maybeConvertCredentialError(err, ctx))
   465  	}
   466  	volumeId = resp.VolumeId
   467  
   468  	volume := storage.Volume{
   469  		Tag: p.Tag,
   470  		VolumeInfo: storage.VolumeInfo{
   471  			VolumeId:   aws.ToString(volumeId),
   472  			Size:       gibToMib(uint64(aws.ToInt32(resp.Size))),
   473  			Persistent: true,
   474  		},
   475  	}
   476  	return &volume, nil, nil
   477  }
   478  
   479  // ListVolumes is specified on the storage.VolumeSource interface.
   480  func (v *ebsVolumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) {
   481  	filter := makeFilter("tag:"+tags.JujuModel, v.modelUUID)
   482  	return listVolumes(v.env.ec2Client, ctx, false, filter)
   483  }
   484  
   485  func listVolumes(client Client, ctx context.ProviderCallContext, includeRootDisks bool, filters ...types.Filter) ([]string, error) {
   486  	resp, err := client.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{
   487  		Filters: filters,
   488  	})
   489  	if err != nil {
   490  		return nil, maybeConvertCredentialError(err, ctx)
   491  	}
   492  	volumeIds := make([]string, 0, len(resp.Volumes))
   493  	for _, vol := range resp.Volumes {
   494  		var isRootDisk bool
   495  		for _, att := range vol.Attachments {
   496  			if aws.ToString(att.Device) == rootDiskDeviceName {
   497  				isRootDisk = true
   498  				break
   499  			}
   500  		}
   501  		if isRootDisk && !includeRootDisks {
   502  			// We don't want to list root disks in the output.
   503  			// These are managed by the instance provisioning
   504  			// code; they will be created and destroyed with
   505  			// instances.
   506  			continue
   507  		}
   508  		volumeIds = append(volumeIds, aws.ToString(vol.VolumeId))
   509  	}
   510  	return volumeIds, nil
   511  }
   512  
   513  // DescribeVolumes is specified on the storage.VolumeSource interface.
   514  func (v *ebsVolumeSource) DescribeVolumes(ctx context.ProviderCallContext, volIds []string) ([]storage.DescribeVolumesResult, error) {
   515  	// TODO(axw) invalid volIds here should not cause the whole
   516  	// operation to fail. If we get an invalid volume ID response,
   517  	// fall back to querying each volume individually. That should
   518  	// be rare.
   519  	resp, err := v.env.ec2Client.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{
   520  		VolumeIds: volIds,
   521  	})
   522  	if err != nil {
   523  		return nil, maybeConvertCredentialError(err, ctx)
   524  	}
   525  	byId := make(map[string]types.Volume)
   526  	for _, vol := range resp.Volumes {
   527  		byId[aws.ToString(vol.VolumeId)] = vol
   528  	}
   529  	results := make([]storage.DescribeVolumesResult, len(volIds))
   530  	for i, volId := range volIds {
   531  		vol, ok := byId[volId]
   532  		if !ok {
   533  			results[i].Error = errors.NotFoundf("%s", volId)
   534  			continue
   535  		}
   536  		results[i].VolumeInfo = &storage.VolumeInfo{
   537  			Size:       gibToMib(uint64(aws.ToInt32(vol.Size))),
   538  			VolumeId:   aws.ToString(vol.VolumeId),
   539  			Persistent: true,
   540  		}
   541  		for _, attachment := range vol.Attachments {
   542  			if aws.ToBool(attachment.DeleteOnTermination) {
   543  				results[i].VolumeInfo.Persistent = false
   544  				break
   545  			}
   546  		}
   547  	}
   548  	return results, nil
   549  }
   550  
   551  // DestroyVolumes is specified on the storage.VolumeSource interface.
   552  func (v *ebsVolumeSource) DestroyVolumes(ctx context.ProviderCallContext, volIds []string) ([]error, error) {
   553  	return foreachVolume(v.env.ec2Client, ctx, volIds, destroyVolume), nil
   554  }
   555  
   556  // ReleaseVolumes is specified on the storage.VolumeSource interface.
   557  func (v *ebsVolumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volIds []string) ([]error, error) {
   558  	return foreachVolume(v.env.ec2Client, ctx, volIds, releaseVolume), nil
   559  }
   560  
   561  func foreachVolume(client Client, ctx context.ProviderCallContext, volIds []string, f func(Client, context.ProviderCallContext, string) error) []error {
   562  	var wg sync.WaitGroup
   563  	wg.Add(len(volIds))
   564  	results := make([]error, len(volIds))
   565  	for i, volumeId := range volIds {
   566  		go func(i int, volumeId string) {
   567  			defer wg.Done()
   568  			results[i] = f(client, ctx, volumeId)
   569  		}(i, volumeId)
   570  	}
   571  	wg.Wait()
   572  	return results
   573  }
   574  
   575  var destroyVolumeAttempt = utils.AttemptStrategy{
   576  	Total: 5 * time.Minute,
   577  	Delay: 5 * time.Second,
   578  }
   579  
   580  func destroyVolume(client Client, ctx context.ProviderCallContext, volumeId string) (err error) {
   581  	defer func() {
   582  		if err != nil {
   583  			if ec2ErrCode(err) == volumeNotFound || errors.IsNotFound(err) {
   584  				// Either the volume isn't found, or we queried the
   585  				// instance corresponding to a DeleteOnTermination
   586  				// attachment; in either case, the volume is or will
   587  				// be destroyed.
   588  				logger.Tracef("Ignoring error destroying volume %q: %v", volumeId, err)
   589  				err = nil
   590  			} else {
   591  				err = maybeConvertCredentialError(err, ctx)
   592  			}
   593  		}
   594  	}()
   595  
   596  	logger.Debugf("destroying %q", volumeId)
   597  
   598  	// Volumes must not be in-use when destroying. A volume may
   599  	// still be in-use when the instance it is attached to is
   600  	// in the process of being terminated.
   601  	volume, err := waitVolume(client, ctx, volumeId, destroyVolumeAttempt, func(volume *types.Volume) (bool, error) {
   602  		if volume.State != volumeStatusInUse {
   603  			// Volume is not in use, it should be OK to destroy now.
   604  			return true, nil
   605  		}
   606  		if len(volume.Attachments) == 0 {
   607  			// There are no attachments remaining now; keep querying
   608  			// until volume transitions out of in-use.
   609  			return false, nil
   610  		}
   611  		var deleteOnTermination []string
   612  		var args []storage.VolumeAttachmentParams
   613  		for _, a := range volume.Attachments {
   614  			switch a.State {
   615  			case attachmentStatusAttaching, attachmentStatusAttached:
   616  				// The volume is attaching or attached to an
   617  				// instance, we need for it to be detached
   618  				// before we can destroy it.
   619  				args = append(args, storage.VolumeAttachmentParams{
   620  					AttachmentParams: storage.AttachmentParams{
   621  						InstanceId: instance.Id(aws.ToString(a.InstanceId)),
   622  					},
   623  					VolumeId: volumeId,
   624  				})
   625  				if aws.ToBool(a.DeleteOnTermination) {
   626  					// The volume is still attached, and the
   627  					// attachment is "delete on termination";
   628  					// check if the related instance is being
   629  					// terminated, in which case we can stop
   630  					// waiting and skip destroying the volume.
   631  					//
   632  					// Note: we still accrue in "args" above
   633  					// in case the instance is not terminating;
   634  					// in that case we detach and destroy as
   635  					// usual.
   636  					deleteOnTermination = append(
   637  						deleteOnTermination, aws.ToString(a.InstanceId),
   638  					)
   639  				}
   640  			}
   641  		}
   642  		if len(deleteOnTermination) > 0 {
   643  			filter := makeFilter("instance-id", deleteOnTermination...)
   644  			resp, err := client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{
   645  				Filters: []types.Filter{filter},
   646  			})
   647  			if err != nil {
   648  				return false, errors.Trace(err)
   649  			}
   650  			for _, reservation := range resp.Reservations {
   651  				for _, instance := range reservation.Instances {
   652  					switch instance.State.Name {
   653  					case instanceStateShuttingDown, instanceStateTerminated:
   654  						// The instance is or will be terminated,
   655  						// and so the volume will be deleted by
   656  						// virtue of delete-on-termination.
   657  						return true, nil
   658  					}
   659  				}
   660  			}
   661  		}
   662  		if len(args) == 0 {
   663  			return false, nil
   664  		}
   665  		results, err := detachVolumes(client, ctx, args)
   666  		if err != nil {
   667  			return false, errors.Trace(err)
   668  		}
   669  		for _, err := range results {
   670  			if err != nil {
   671  				return false, errors.Trace(err)
   672  			}
   673  		}
   674  		return false, nil
   675  	})
   676  	if err != nil {
   677  		if err == errWaitVolumeTimeout {
   678  			return errors.Errorf("timed out waiting for volume %v to not be in-use", volumeId)
   679  		}
   680  		return errors.Trace(err)
   681  	}
   682  	if volume.State == volumeStatusInUse {
   683  		// If the volume is in-use, that means it will be
   684  		// handled by delete-on-termination and we have
   685  		// nothing more to do.
   686  		return nil
   687  	}
   688  	_, err = client.DeleteVolume(ctx, &ec2.DeleteVolumeInput{
   689  		VolumeId: aws.String(volumeId),
   690  	})
   691  	return errors.Annotatef(maybeConvertCredentialError(err, ctx), "destroying %q", volumeId)
   692  }
   693  
   694  func releaseVolume(client Client, ctx context.ProviderCallContext, volumeId string) error {
   695  	logger.Debugf("releasing %q", volumeId)
   696  	_, err := waitVolume(client, ctx, volumeId, destroyVolumeAttempt, func(volume *types.Volume) (bool, error) {
   697  		if volume.State == volumeStatusAvailable {
   698  			return true, nil
   699  		}
   700  		for _, a := range volume.Attachments {
   701  			if aws.ToBool(a.DeleteOnTermination) {
   702  				return false, errors.New("delete-on-termination flag is set")
   703  			}
   704  			switch a.State {
   705  			case attachmentStatusAttaching, attachmentStatusAttached:
   706  				return false, errors.New("attachments still active")
   707  			}
   708  		}
   709  		return false, nil
   710  	})
   711  	if err != nil {
   712  		if err == errWaitVolumeTimeout {
   713  			return errors.Errorf("timed out waiting for volume %v to become available", volumeId)
   714  		}
   715  		return errors.Annotatef(maybeConvertCredentialError(err, ctx), "cannot release volume %q", volumeId)
   716  	}
   717  	// Releasing the volume just means dropping the
   718  	// tags that associate it with the model and
   719  	// controller.
   720  	tags := map[string]string{
   721  		tags.JujuModel:      "",
   722  		tags.JujuController: "",
   723  	}
   724  	return errors.Annotate(tagResources(client, ctx, tags, volumeId), "tagging volume")
   725  }
   726  
   727  // ValidateVolumeParams is specified on the storage.VolumeSource interface.
   728  func (v *ebsVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error {
   729  	vol, err := parseVolumeOptions(params.Size, params.Attributes)
   730  	if err != nil {
   731  		return err
   732  	}
   733  	var minVolumeSize, maxVolumeSize int32
   734  	switch vol.VolumeType {
   735  	case volumeTypeStandard:
   736  		minVolumeSize = minMagneticVolumeSizeGiB
   737  		maxVolumeSize = maxMagneticVolumeSizeGiB
   738  	case volumeTypeGP2, types.VolumeTypeGp3:
   739  		minVolumeSize = minSSDVolumeSizeGiB
   740  		maxVolumeSize = maxSSDVolumeSizeGiB
   741  	case volumeTypeIO1, types.VolumeTypeIo2:
   742  		// TODO(juju3): check io2 max disk size re: io2 block express on r5b instances.
   743  		minVolumeSize = minProvisionedIopsVolumeSizeGiB
   744  		maxVolumeSize = maxProvisionedIopsVolumeSizeGiB
   745  	case volumeTypeST1:
   746  		minVolumeSize = minSt1VolumeSizeGiB
   747  		maxVolumeSize = maxSt1VolumeSizeGiB
   748  	case volumeTypeSC1:
   749  		minVolumeSize = minSc1VolumeSizeGiB
   750  		maxVolumeSize = maxSc1VolumeSizeGiB
   751  	}
   752  	volSize := aws.ToInt32(vol.Size)
   753  	if volSize < minVolumeSize {
   754  		return errors.Errorf(
   755  			"volume size is %d GiB, must be at least %d GiB",
   756  			volSize, minVolumeSize,
   757  		)
   758  	}
   759  	if volSize > maxVolumeSize {
   760  		return errors.Errorf(
   761  			"volume size %d GiB exceeds the maximum of %d GiB",
   762  			volSize, maxVolumeSize,
   763  		)
   764  	}
   765  	return nil
   766  }
   767  
   768  // AttachVolumes is specified on the storage.VolumeSource interface.
   769  func (v *ebsVolumeSource) AttachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) {
   770  	// We need the instance type for each instance we are
   771  	// attaching to so we can determine how to identify the
   772  	// volume attachment
   773  	instIds := set.NewStrings()
   774  	for _, p := range attachParams {
   775  		instIds.Add(string(p.InstanceId))
   776  	}
   777  	instances := make(instanceCache)
   778  	if err := instances.update(v.env.ec2Client, ctx, instIds.Values()...); err != nil {
   779  		err := maybeConvertCredentialError(err, ctx)
   780  		logger.Debugf("querying running instances: %v", err)
   781  		// We ignore the error, because we don't want an invalid
   782  		// InstanceId reference from one VolumeParams to prevent
   783  		// the creation of another volume.
   784  		// Except if it is a credential error...
   785  		if errors.Is(err, common.ErrorCredentialNotValid) {
   786  			return nil, errors.Trace(err)
   787  		}
   788  	}
   789  
   790  	results := make([]storage.AttachVolumesResult, len(attachParams))
   791  	for i, params := range attachParams {
   792  		instId := string(params.InstanceId)
   793  
   794  		// By default we should allocate device names without the
   795  		// trailing number. Block devices with a trailing number are
   796  		// not liked by some applications, e.g. Ceph, which want full
   797  		// disks.
   798  		//
   799  		// TODO(axw) introduce a configuration option if and when
   800  		// someone asks for it to enable use of numbers. This option
   801  		// must error if used with an "hvm" instance type.
   802  		const numbers = false
   803  		nextDeviceName := blockDeviceNamer(numbers)
   804  		_, deviceName, err := v.attachOneVolume(ctx, nextDeviceName, params.VolumeId, instId)
   805  		if err != nil {
   806  			results[i].Error = maybeConvertCredentialError(err, ctx)
   807  			continue
   808  		}
   809  
   810  		var attachmentInfo storage.VolumeAttachmentInfo
   811  		// The newer hypervisor attaches EBS volumes
   812  		// as NVMe devices, and the block device names
   813  		// are unpredictable from here.
   814  		//
   815  		// Instead of using device name, we fill in
   816  		// the device link, based on the statically
   817  		// defined model name ("Amazon Elastic Block Store",
   818  		// and serial (vol123456abcdef...); the serial
   819  		// is the same as the volume ID without the "-".
   820  		//
   821  		// NOTE(axw) inst.Hypervisor still says "xen" for
   822  		// affected instance types, which would seem to
   823  		// be a lie. There's no way to tell how a volume will
   824  		// be exposed so we have to assume an nvme link - the
   825  		// subsequent matching code will correctly skip the link
   826  		// and match against device name for non-nvme volumes.
   827  		sn := strings.Replace(params.VolumeId, "-", "", 1)
   828  		attachmentInfo.DeviceLink = nvmeDeviceLinkPrefix + sn
   829  		attachmentInfo.DeviceName = deviceName
   830  
   831  		results[i].VolumeAttachment = &storage.VolumeAttachment{
   832  			Volume:               params.Volume,
   833  			Machine:              params.Machine,
   834  			VolumeAttachmentInfo: attachmentInfo,
   835  		}
   836  	}
   837  	return results, nil
   838  }
   839  
   840  func (v *ebsVolumeSource) attachOneVolume(
   841  	ctx context.ProviderCallContext,
   842  	nextDeviceName func() (string, string, error),
   843  	volumeId, instId string,
   844  ) (string, string, error) {
   845  	// Wait for the volume to move out of "creating".
   846  	volume, err := v.waitVolumeCreated(ctx, volumeId)
   847  	if err != nil {
   848  		return "", "", errors.Trace(maybeConvertCredentialError(err, ctx))
   849  	}
   850  
   851  	// Possible statuses:
   852  	//    creating | available | in-use | deleting | deleted | error
   853  	volState := volume.State
   854  	switch volState {
   855  	default:
   856  		return "", "", errors.Errorf("cannot attach to volume with status %q", volState)
   857  
   858  	case volumeStatusInUse:
   859  		// Volume is already attached; see if it's attached to the
   860  		// instance requested.
   861  		attachments := volume.Attachments
   862  		if len(attachments) != 1 {
   863  			return "", "", errors.Errorf("volume %v has unexpected attachment count: %v", volumeId, len(attachments))
   864  		}
   865  		if id := aws.ToString(attachments[0].InstanceId); id != instId {
   866  			return "", "", errors.Errorf("volume %v is attached to %v", volumeId, id)
   867  		}
   868  		requestDeviceName := aws.ToString(attachments[0].Device)
   869  		actualDeviceName := renamedDevicePrefix + requestDeviceName[len(devicePrefix):]
   870  		return requestDeviceName, actualDeviceName, nil
   871  
   872  	case volumeStatusAvailable:
   873  		// Attempt to attach below.
   874  		break
   875  	}
   876  	for {
   877  		requestDeviceName, actualDeviceName, err := nextDeviceName()
   878  		if err != nil {
   879  			// Can't attach any more volumes.
   880  			return "", "", err
   881  		}
   882  		_, err = v.env.ec2Client.AttachVolume(ctx, &ec2.AttachVolumeInput{
   883  			Device:     aws.String(requestDeviceName),
   884  			InstanceId: aws.String(instId),
   885  			VolumeId:   aws.String(volumeId),
   886  		})
   887  		var apiErr smithy.APIError
   888  		if stderrors.As(errors.Cause(err), &apiErr) {
   889  			switch apiErr.ErrorCode() {
   890  			case invalidParameterValue:
   891  				// InvalidParameterValue is returned by AttachVolume
   892  				// rather than InvalidDevice.InUse as the docs would
   893  				// suggest.
   894  				if !deviceInUseRegexp.MatchString(apiErr.ErrorMessage()) {
   895  					break
   896  				}
   897  				fallthrough
   898  
   899  			case deviceInUse:
   900  				// deviceInUse means that the requested device name
   901  				// is in use already. Try again with the next name.
   902  				continue
   903  			}
   904  		}
   905  		if err != nil {
   906  			return "", "", errors.Annotate(maybeConvertCredentialError(err, ctx), "attaching volume")
   907  		}
   908  		return requestDeviceName, actualDeviceName, nil
   909  	}
   910  }
   911  
   912  func (v *ebsVolumeSource) waitVolumeCreated(ctx context.ProviderCallContext, volumeId string) (*types.Volume, error) {
   913  	var attempt = utils.AttemptStrategy{
   914  		Total: 5 * time.Second,
   915  		Delay: 200 * time.Millisecond,
   916  	}
   917  	var lastStatus types.VolumeState
   918  	volume, err := waitVolume(v.env.ec2Client, ctx, volumeId, attempt, func(volume *types.Volume) (bool, error) {
   919  		state := volume.State
   920  		lastStatus = state
   921  		return lastStatus != volumeStatusCreating, nil
   922  	})
   923  	if err == errWaitVolumeTimeout {
   924  		return nil, errors.Errorf(
   925  			"timed out waiting for volume %v to become available (%v)",
   926  			volumeId, lastStatus,
   927  		)
   928  	} else if err != nil {
   929  		return nil, errors.Trace(maybeConvertCredentialError(err, ctx))
   930  	}
   931  	return volume, nil
   932  }
   933  
   934  var errWaitVolumeTimeout = errors.New("timed out")
   935  
   936  func waitVolume(
   937  	client Client,
   938  	ctx context.ProviderCallContext,
   939  	volumeId string,
   940  	attempt utils.AttemptStrategy,
   941  	pred func(v *types.Volume) (bool, error),
   942  ) (*types.Volume, error) {
   943  	for a := attempt.Start(); a.Next(); {
   944  		volume, err := describeVolume(client, ctx, volumeId)
   945  		if err != nil {
   946  			return nil, errors.Trace(err)
   947  		}
   948  		ok, err := pred(volume)
   949  		if err != nil {
   950  			return nil, errors.Trace(err)
   951  		}
   952  		if ok {
   953  			return volume, nil
   954  		}
   955  	}
   956  	return nil, errWaitVolumeTimeout
   957  }
   958  
   959  func describeVolume(client Client, ctx context.ProviderCallContext, volumeId string) (*types.Volume, error) {
   960  	resp, err := client.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{
   961  		VolumeIds: []string{volumeId},
   962  	})
   963  	if err != nil {
   964  		return nil, errors.Annotate(maybeConvertCredentialError(err, ctx), "querying volume")
   965  	}
   966  	if len(resp.Volumes) == 0 {
   967  		return nil, errors.NotFoundf("%v", volumeId)
   968  	} else if len(resp.Volumes) != 1 {
   969  		return nil, errors.Errorf("expected one volume, got %d", len(resp.Volumes))
   970  	}
   971  	return &resp.Volumes[0], nil
   972  }
   973  
   974  type instanceCache map[string]types.Instance
   975  
   976  func (c instanceCache) update(ec2client Client, ctx context.ProviderCallContext, ids ...string) error {
   977  	if len(ids) == 1 {
   978  		if _, ok := c[ids[0]]; ok {
   979  			return nil
   980  		}
   981  	}
   982  
   983  	stateFilter := makeFilter("instance-state-name", "running")
   984  	idFilter := makeFilter("instance-id", ids...)
   985  	resp, err := ec2client.DescribeInstances(ctx, &ec2.DescribeInstancesInput{
   986  		InstanceIds: ids,
   987  		Filters:     []types.Filter{stateFilter, idFilter},
   988  	})
   989  	if err != nil {
   990  		return errors.Annotate(maybeConvertCredentialError(err, ctx), "querying instance details")
   991  	}
   992  	for j := range resp.Reservations {
   993  		r := resp.Reservations[j]
   994  		for _, inst := range r.Instances {
   995  			c[*inst.InstanceId] = inst
   996  		}
   997  	}
   998  	return nil
   999  }
  1000  
  1001  func (c instanceCache) get(id string) (types.Instance, error) {
  1002  	inst, ok := c[id]
  1003  	if !ok {
  1004  		return types.Instance{}, errors.Errorf("cannot attach to non-running instance %v", id)
  1005  	}
  1006  	return inst, nil
  1007  }
  1008  
  1009  // DetachVolumes is specified on the storage.VolumeSource interface.
  1010  func (v *ebsVolumeSource) DetachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]error, error) {
  1011  	return detachVolumes(v.env.ec2Client, ctx, attachParams)
  1012  }
  1013  
  1014  func detachVolumes(client Client, ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]error, error) {
  1015  	results := make([]error, len(attachParams))
  1016  	for i, params := range attachParams {
  1017  		_, err := client.DetachVolume(ctx, &ec2.DetachVolumeInput{
  1018  			VolumeId:   aws.String(params.VolumeId),
  1019  			InstanceId: aws.String(string(params.InstanceId)),
  1020  		})
  1021  		// Process aws specific error information.
  1022  		switch ec2ErrCode(err) {
  1023  		case incorrectState:
  1024  			// incorrect state means this volume is "available",
  1025  			// i.e. is not attached to any machine.
  1026  			err = nil
  1027  		case attachmentNotFound:
  1028  			// attachment not found means this volume is already detached.
  1029  			err = nil
  1030  		}
  1031  		if err != nil {
  1032  			results[i] = errors.Annotatef(
  1033  				maybeConvertCredentialError(err, ctx), "detaching %s from %s",
  1034  				names.ReadableString(params.Volume),
  1035  				names.ReadableString(params.Machine),
  1036  			)
  1037  		}
  1038  	}
  1039  	return results, nil
  1040  }
  1041  
  1042  // ImportVolume is specified on the storage.VolumeImporter interface.
  1043  func (v *ebsVolumeSource) ImportVolume(ctx context.ProviderCallContext, volumeId string, tags map[string]string) (storage.VolumeInfo, error) {
  1044  	resp, err := v.env.ec2Client.DescribeVolumes(ctx, &ec2.DescribeVolumesInput{
  1045  		VolumeIds: []string{volumeId},
  1046  	})
  1047  	if err != nil {
  1048  		// TODO(axw) check for "not found" response, massage error message?
  1049  		return storage.VolumeInfo{}, maybeConvertCredentialError(err, ctx)
  1050  	}
  1051  	if len(resp.Volumes) != 1 {
  1052  		return storage.VolumeInfo{}, errors.Errorf("expected 1 volume result, got %d", len(resp.Volumes))
  1053  	}
  1054  	vol := resp.Volumes[0]
  1055  	volState := vol.State
  1056  	if volState != volumeStatusAvailable {
  1057  		return storage.VolumeInfo{}, errors.Errorf("cannot import volume with status %q", volState)
  1058  	}
  1059  	if err := tagResources(v.env.ec2Client, ctx, tags, volumeId); err != nil {
  1060  		return storage.VolumeInfo{}, errors.Annotate(err, "tagging volume")
  1061  	}
  1062  	return storage.VolumeInfo{
  1063  		VolumeId:   volumeId,
  1064  		Size:       gibToMib(uint64(aws.ToInt32(vol.Size))),
  1065  		Persistent: true,
  1066  	}, nil
  1067  }
  1068  
  1069  var errTooManyVolumes = errors.New("too many EBS volumes to attach")
  1070  
  1071  // blockDeviceNamer returns a function that cycles through block device names.
  1072  //
  1073  // The returned function returns the device name that should be used in
  1074  // requests to the EC2 API, and and also the (kernel) device name as it
  1075  // will appear on the machine.
  1076  //
  1077  // See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
  1078  func blockDeviceNamer(numbers bool) func() (requestName, actualName string, err error) {
  1079  	const (
  1080  		// deviceLetterMin is the first letter to use for EBS block device names.
  1081  		deviceLetterMin = 'f'
  1082  		// deviceLetterMax is the last letter to use for EBS block device names.
  1083  		deviceLetterMax = 'z'
  1084  		// deviceNumMax is the maximum value for trailing numbers on block device name.
  1085  		deviceNumMax = 6
  1086  	)
  1087  	var n int
  1088  	letterRepeats := 1
  1089  	if numbers {
  1090  		letterRepeats = deviceNumMax
  1091  	}
  1092  	return func() (string, string, error) {
  1093  		letter := deviceLetterMin + (n / letterRepeats)
  1094  		if letter > deviceLetterMax {
  1095  			return "", "", errTooManyVolumes
  1096  		}
  1097  		deviceName := devicePrefix + fmt.Sprintf("%c", letter)
  1098  		if numbers {
  1099  			// Suffix is a digit from [1, deviceNumMax)
  1100  			deviceName += fmt.Sprintf("%d", 1+(n%deviceNumMax))
  1101  		}
  1102  		n++
  1103  		realDeviceName := renamedDevicePrefix + deviceName[len(devicePrefix):]
  1104  		return deviceName, realDeviceName, nil
  1105  	}
  1106  }
  1107  
  1108  func minRootDiskSizeMiB(osname string) uint64 {
  1109  	return gibToMib(common.MinRootDiskSizeGiB(ostype.OSTypeForName(osname)))
  1110  }
  1111  
  1112  // getBlockDeviceMappings translates constraints into BlockDeviceMappings.
  1113  //
  1114  // The first entry is always the root disk mapping, followed by instance
  1115  // stores (ephemeral disks).
  1116  func getBlockDeviceMappings(
  1117  	cons constraints.Value,
  1118  	osname string,
  1119  	controller bool,
  1120  	rootDisk *storage.VolumeParams,
  1121  ) ([]types.BlockDeviceMapping, error) {
  1122  	minRootDiskSizeMiB := minRootDiskSizeMiB(osname)
  1123  	rootDiskSizeMiB := minRootDiskSizeMiB
  1124  	if controller {
  1125  		rootDiskSizeMiB = defaultControllerDiskSizeMiB
  1126  	}
  1127  	if cons.RootDisk != nil {
  1128  		if *cons.RootDisk >= minRootDiskSizeMiB {
  1129  			rootDiskSizeMiB = *cons.RootDisk
  1130  		} else {
  1131  			logger.Infof(
  1132  				"Ignoring root-disk constraint of %dM because it is smaller than the minimum size %dM",
  1133  				*cons.RootDisk,
  1134  				minRootDiskSizeMiB,
  1135  			)
  1136  		}
  1137  	}
  1138  
  1139  	rootDiskMapping := types.BlockDeviceMapping{
  1140  		DeviceName: aws.String(rootDiskDeviceName),
  1141  		Ebs: &types.EbsBlockDevice{
  1142  			VolumeSize: aws.Int32(int32(mibToGib(rootDiskSizeMiB))),
  1143  		},
  1144  	}
  1145  	if rootDisk != nil {
  1146  		config, err := newEbsConfig(rootDisk.Attributes)
  1147  		if err != nil {
  1148  			return nil, errors.Annotatef(err, "parsing root disk attributes")
  1149  		}
  1150  		if config.encrypted {
  1151  			rootDiskMapping.Ebs.Encrypted = aws.Bool(config.encrypted)
  1152  		}
  1153  		if config.iops > 0 {
  1154  			rootDiskMapping.Ebs.Iops = aws.Int32(int32(config.iops))
  1155  		}
  1156  		if config.volumeType != "" {
  1157  			rootDiskMapping.Ebs.VolumeType = types.VolumeType(config.volumeType)
  1158  		}
  1159  		if config.kmsKeyID != "" {
  1160  			rootDiskMapping.Ebs.KmsKeyId = aws.String(config.kmsKeyID)
  1161  		}
  1162  		if config.throughputMB > 0 {
  1163  			rootDiskMapping.Ebs.Throughput = aws.Int32(int32(config.throughputMB))
  1164  		}
  1165  	}
  1166  
  1167  	// The first block device is for the root disk.
  1168  	blockDeviceMappings := []types.BlockDeviceMapping{rootDiskMapping}
  1169  
  1170  	// Not all machines have this many instance stores.
  1171  	// Instances will be started with as many of the
  1172  	// instance stores as they can support.
  1173  	blockDeviceMappings = append(blockDeviceMappings, []types.BlockDeviceMapping{{
  1174  		VirtualName: aws.String("ephemeral0"),
  1175  		DeviceName:  aws.String("/dev/sdb"),
  1176  	}, {
  1177  		VirtualName: aws.String("ephemeral1"),
  1178  		DeviceName:  aws.String("/dev/sdc"),
  1179  	}, {
  1180  		VirtualName: aws.String("ephemeral2"),
  1181  		DeviceName:  aws.String("/dev/sdd"),
  1182  	}, {
  1183  		VirtualName: aws.String("ephemeral3"),
  1184  		DeviceName:  aws.String("/dev/sde"),
  1185  	}}...)
  1186  
  1187  	return blockDeviceMappings, nil
  1188  }
  1189  
  1190  // mibToGib converts mebibytes to gibibytes.
  1191  // AWS expects GiB, we work in MiB; round up
  1192  // to nearest GiB.
  1193  func mibToGib(m uint64) uint64 {
  1194  	return (m + 1023) / 1024
  1195  }
  1196  
  1197  // gibToMib converts gibibytes to mebibytes.
  1198  func gibToMib(g uint64) uint64 {
  1199  	return g * 1024
  1200  }