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 }