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 }