github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/oci/storage_volumes.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package oci 5 6 import ( 7 "context" 8 "fmt" 9 "strconv" 10 "time" 11 12 "github.com/juju/clock" 13 "github.com/juju/errors" 14 ociCore "github.com/oracle/oci-go-sdk/v65/core" 15 16 "github.com/juju/juju/core/instance" 17 envcontext "github.com/juju/juju/environs/context" 18 "github.com/juju/juju/environs/tags" 19 allProvidersCommon "github.com/juju/juju/provider/common" 20 "github.com/juju/juju/provider/oci/common" 21 "github.com/juju/juju/storage" 22 ) 23 24 func mibToGib(m uint64) uint64 { 25 return (m + 1023) / 1024 26 } 27 28 // isAuthFailure is a helper function that's used to reduce line noise. 29 // It's typically called within err != nil blocks. 30 var isAuthFailure = func(err error, ctx envcontext.ProviderCallContext) bool { 31 return allProvidersCommon.MaybeHandleCredentialError(common.IsAuthorisationFailure, err, ctx) 32 } 33 34 type volumeSource struct { 35 env *Environ 36 envName string 37 modelUUID string 38 storageAPI StorageClient 39 computeAPI ComputeClient 40 clock clock.Clock 41 } 42 43 var _ storage.VolumeSource = (*volumeSource)(nil) 44 45 func (v *volumeSource) getVolumeStatus(resourceID *string) (string, error) { 46 request := ociCore.GetVolumeRequest{ 47 VolumeId: resourceID, 48 } 49 50 response, err := v.storageAPI.GetVolume(context.Background(), request) 51 if err != nil { 52 if v.env.isNotFound(response.RawResponse) { 53 return "", errors.NotFoundf("volume not found: %s", *resourceID) 54 } else { 55 return "", err 56 } 57 } 58 return string(response.Volume.LifecycleState), nil 59 } 60 61 func (v *volumeSource) createVolume(ctx envcontext.ProviderCallContext, p storage.VolumeParams, instanceMap map[instance.Id]*ociInstance) (_ *storage.Volume, err error) { 62 var details ociCore.CreateVolumeResponse 63 defer func() { 64 if err != nil && details.Id != nil { 65 req := ociCore.DeleteVolumeRequest{ 66 VolumeId: details.Id, 67 } 68 response, nestedErr := v.storageAPI.DeleteVolume(context.Background(), req) 69 if nestedErr != nil && !v.env.isNotFound(response.RawResponse) { 70 logger.Warningf("failed to cleanup volume: %s", *details.Id) 71 return 72 } 73 nestedErr = v.env.waitForResourceStatus( 74 v.getVolumeStatus, details.Id, 75 string(ociCore.VolumeLifecycleStateTerminated), 76 5*time.Minute) 77 if nestedErr != nil && !errors.IsNotFound(nestedErr) { 78 logger.Warningf("failed to cleanup volume: %s", *details.Id) 79 return 80 } 81 } 82 }() 83 if err := v.ValidateVolumeParams(p); err != nil { 84 return nil, errors.Trace(err) 85 } 86 if p.Attachment == nil { 87 return nil, errors.Errorf("volume %s has no attachments", p.Tag.String()) 88 } 89 instanceId := p.Attachment.InstanceId 90 inst, ok := instanceMap[instanceId] 91 if !ok { 92 ociInstances, err := v.env.getOciInstances(ctx, instanceId) 93 if err != nil { 94 common.HandleCredentialError(err, ctx) 95 return nil, errors.Trace(err) 96 } 97 inst = ociInstances[0] 98 instanceMap[instanceId] = inst 99 } 100 101 availabilityZone := inst.availabilityZone() 102 name := p.Tag.String() 103 104 volTags := map[string]string{} 105 if p.ResourceTags != nil { 106 volTags = p.ResourceTags 107 } 108 volTags[tags.JujuModel] = v.modelUUID 109 110 size := int64(p.Size) 111 requestDetails := ociCore.CreateVolumeDetails{ 112 AvailabilityDomain: &availabilityZone, 113 CompartmentId: v.env.ecfg().compartmentID(), 114 DisplayName: &name, 115 SizeInMBs: &size, 116 FreeformTags: volTags, 117 } 118 119 request := ociCore.CreateVolumeRequest{ 120 CreateVolumeDetails: requestDetails, 121 } 122 123 result, err := v.storageAPI.CreateVolume(context.Background(), request) 124 if err != nil { 125 return nil, errors.Trace(err) 126 } 127 err = v.env.waitForResourceStatus( 128 v.getVolumeStatus, result.Volume.Id, 129 string(ociCore.VolumeLifecycleStateAvailable), 130 5*time.Minute) 131 if err != nil { 132 return nil, errors.Trace(err) 133 } 134 135 volumeDetails, err := v.storageAPI.GetVolume( 136 context.Background(), ociCore.GetVolumeRequest{VolumeId: result.Volume.Id}) 137 if err != nil { 138 common.HandleCredentialError(err, ctx) 139 return nil, errors.Trace(err) 140 } 141 142 return &storage.Volume{Tag: p.Tag, VolumeInfo: makeVolumeInfo(volumeDetails.Volume)}, nil 143 } 144 145 func makeVolumeInfo(vol ociCore.Volume) storage.VolumeInfo { 146 var size uint64 147 if vol.SizeInMBs != nil { 148 size = uint64(*vol.SizeInMBs) 149 } else if vol.SizeInGBs != nil { 150 size = uint64(*vol.SizeInGBs * 1024) 151 } 152 153 return storage.VolumeInfo{ 154 VolumeId: *vol.Id, 155 Size: size, 156 Persistent: true, 157 } 158 } 159 160 func (v *volumeSource) CreateVolumes(ctx envcontext.ProviderCallContext, params []storage.VolumeParams) ([]storage.CreateVolumesResult, error) { 161 logger.Debugf("Creating volumes: %v", params) 162 if params == nil { 163 return []storage.CreateVolumesResult{}, nil 164 } 165 var credErr error 166 167 results := make([]storage.CreateVolumesResult, len(params)) 168 instanceMap := map[instance.Id]*ociInstance{} 169 for i, volume := range params { 170 if credErr != nil { 171 results[i].Error = errors.Trace(credErr) 172 continue 173 } 174 vol, err := v.createVolume(ctx, volume, instanceMap) 175 if err != nil { 176 if isAuthFailure(err, ctx) { 177 credErr = err 178 common.HandleCredentialError(err, ctx) 179 } 180 results[i].Error = errors.Trace(err) 181 continue 182 } 183 results[i].Volume = vol 184 } 185 return results, nil 186 } 187 188 func (v *volumeSource) allVolumes() (map[string]ociCore.Volume, error) { 189 result := map[string]ociCore.Volume{} 190 volumes, err := v.storageAPI.ListVolumes(context.Background(), v.env.ecfg().compartmentID()) 191 if err != nil { 192 return nil, err 193 } 194 195 for _, val := range volumes { 196 if t, ok := val.FreeformTags[tags.JujuModel]; !ok { 197 continue 198 } else { 199 if t != "" && t != v.modelUUID { 200 continue 201 } 202 } 203 result[*val.Id] = val 204 } 205 return result, nil 206 } 207 208 func (v *volumeSource) ListVolumes(ctx envcontext.ProviderCallContext) ([]string, error) { 209 var ids []string 210 volumes, err := v.allVolumes() 211 if err != nil { 212 common.HandleCredentialError(err, ctx) 213 return nil, errors.Trace(err) 214 } 215 216 for k := range volumes { 217 ids = append(ids, k) 218 } 219 return ids, nil 220 } 221 222 func (v *volumeSource) DescribeVolumes(ctx envcontext.ProviderCallContext, volIds []string) ([]storage.DescribeVolumesResult, error) { 223 result := make([]storage.DescribeVolumesResult, len(volIds), len(volIds)) 224 225 allVolumes, err := v.allVolumes() 226 if err != nil { 227 common.HandleCredentialError(err, ctx) 228 return nil, errors.Trace(err) 229 } 230 231 for i, val := range volIds { 232 if volume, ok := allVolumes[val]; ok { 233 volumeInfo := makeVolumeInfo(volume) 234 result[i].VolumeInfo = &volumeInfo 235 } else { 236 result[i].Error = errors.NotFoundf("%s", volume) 237 } 238 } 239 return result, nil 240 } 241 242 func (v *volumeSource) DestroyVolumes(ctx envcontext.ProviderCallContext, volIds []string) ([]error, error) { 243 volumes, err := v.allVolumes() 244 if err != nil { 245 common.HandleCredentialError(err, ctx) 246 return nil, errors.Trace(err) 247 } 248 249 var credErr error 250 errs := make([]error, len(volIds)) 251 252 for idx, volId := range volIds { 253 if credErr != nil { 254 errs[idx] = errors.Trace(credErr) 255 continue 256 } 257 volumeDetails, ok := volumes[volId] 258 if !ok { 259 errs[idx] = errors.NotFoundf("no such volume %s", volId) 260 continue 261 } 262 request := ociCore.DeleteVolumeRequest{ 263 VolumeId: volumeDetails.Id, 264 } 265 266 response, err := v.storageAPI.DeleteVolume(context.Background(), request) 267 if err != nil && !v.env.isNotFound(response.RawResponse) { 268 if isAuthFailure(err, ctx) { 269 common.HandleCredentialError(err, ctx) 270 credErr = err 271 } 272 errs[idx] = errors.Trace(err) 273 continue 274 } 275 err = v.env.waitForResourceStatus( 276 v.getVolumeStatus, volumeDetails.Id, 277 string(ociCore.VolumeLifecycleStateTerminated), 278 5*time.Minute) 279 if err != nil && !errors.IsNotFound(err) { 280 if isAuthFailure(err, ctx) { 281 common.HandleCredentialError(err, ctx) 282 credErr = err 283 } 284 errs[idx] = errors.Trace(err) 285 } else { 286 errs[idx] = nil 287 } 288 } 289 return errs, nil 290 } 291 292 func (v *volumeSource) ReleaseVolumes(ctx envcontext.ProviderCallContext, volIds []string) ([]error, error) { 293 volumes, err := v.allVolumes() 294 if err != nil { 295 return nil, errors.Trace(err) 296 } 297 298 var credErr error 299 errs := make([]error, len(volIds)) 300 tagsToRemove := []string{ 301 tags.JujuModel, 302 tags.JujuController, 303 } 304 for idx, volId := range volIds { 305 if credErr != nil { 306 errs[idx] = errors.Trace(credErr) 307 continue 308 } 309 volumeDetails, ok := volumes[volId] 310 if !ok { 311 errs[idx] = errors.NotFoundf("no such volume %s", volId) 312 continue 313 } 314 currentTags := volumeDetails.FreeformTags 315 needsUpdate := false 316 for _, tag := range tagsToRemove { 317 if _, ok := currentTags[tag]; ok { 318 needsUpdate = true 319 currentTags[tag] = "" 320 } 321 } 322 if needsUpdate { 323 requestDetails := ociCore.UpdateVolumeDetails{ 324 FreeformTags: currentTags, 325 } 326 request := ociCore.UpdateVolumeRequest{ 327 UpdateVolumeDetails: requestDetails, 328 VolumeId: volumeDetails.Id, 329 } 330 331 _, err := v.storageAPI.UpdateVolume(context.Background(), request) 332 if err != nil { 333 if isAuthFailure(err, ctx) { 334 common.HandleCredentialError(err, ctx) 335 credErr = err 336 } 337 errs[idx] = errors.Trace(err) 338 } else { 339 errs[idx] = nil 340 } 341 } 342 } 343 return errs, nil 344 } 345 346 func (v *volumeSource) ValidateVolumeParams(params storage.VolumeParams) error { 347 size := mibToGib(params.Size) 348 if size < minVolumeSizeInGB || size > maxVolumeSizeInGB { 349 return errors.Errorf( 350 "invalid volume size %d. Valid range is %d - %d (GiB)", size, minVolumeSizeInGB, maxVolumeSizeInGB) 351 } 352 return nil 353 } 354 355 func (v *volumeSource) volumeAttachments(instanceId instance.Id) ([]ociCore.IScsiVolumeAttachment, error) { 356 instId := string(instanceId) 357 358 attachments, err := v.computeAPI.ListVolumeAttachments(context.Background(), v.env.ecfg().compartmentID(), &instId) 359 if err != nil { 360 return nil, errors.Trace(err) 361 } 362 ret := make([]ociCore.IScsiVolumeAttachment, len(attachments)) 363 364 for idx, att := range attachments { 365 // The oracle oci client will return a VolumeAttachment type, which is an 366 // interface. This is due to the fact that they will at some point support 367 // different attachment types. For the moment, there is only iSCSI, as stated 368 // in the documentation, at the time of this writing: 369 // https://docs.us-phoenix-1.oraclecloud.com/api/#/en/iaas/20160918/requests/AttachVolumeDetails 370 // 371 // So we need to cast it back to IScsiVolumeAttachment{} to be able to access 372 // the connection info we need, and possibly chap secrets to be able to connect 373 // to the volume. 374 baseType, ok := att.(ociCore.IScsiVolumeAttachment) 375 if !ok { 376 return nil, errors.Errorf("invalid attachment type. Expected iscsi") 377 } 378 379 if baseType.LifecycleState == ociCore.VolumeAttachmentLifecycleStateDetached { 380 continue 381 } 382 ret[idx] = baseType 383 } 384 return ret, nil 385 } 386 387 func makeVolumeAttachmentResult(attachment ociCore.IScsiVolumeAttachment, param storage.VolumeAttachmentParams) (storage.AttachVolumesResult, error) { 388 if attachment.Port == nil || attachment.Iqn == nil { 389 return storage.AttachVolumesResult{}, errors.Errorf("invalid attachment info") 390 } 391 port := strconv.Itoa(*attachment.Port) 392 planInfo := &storage.VolumeAttachmentPlanInfo{ 393 DeviceType: storage.DeviceTypeISCSI, 394 DeviceAttributes: map[string]string{ 395 "iqn": *attachment.Iqn, 396 "address": *attachment.Ipv4, 397 "port": port, 398 }, 399 } 400 if attachment.ChapSecret != nil && attachment.ChapUsername != nil { 401 planInfo.DeviceAttributes["chap-user"] = *attachment.ChapUsername 402 planInfo.DeviceAttributes["chap-secret"] = *attachment.ChapSecret 403 } 404 result := storage.AttachVolumesResult{ 405 VolumeAttachment: &storage.VolumeAttachment{ 406 Volume: param.Volume, 407 Machine: param.Machine, 408 VolumeAttachmentInfo: storage.VolumeAttachmentInfo{ 409 PlanInfo: planInfo, 410 }, 411 }, 412 } 413 return result, nil 414 } 415 416 func (v *volumeSource) attachVolume(ctx envcontext.ProviderCallContext, param storage.VolumeAttachmentParams) (_ storage.AttachVolumesResult, err error) { 417 var details ociCore.AttachVolumeResponse 418 defer func() { 419 volAttach := details.VolumeAttachment 420 if volAttach != nil && err != nil && volAttach.GetId() != nil { 421 req := ociCore.DetachVolumeRequest{ 422 VolumeAttachmentId: volAttach.GetId(), 423 } 424 res, nestedErr := v.computeAPI.DetachVolume(context.Background(), req) 425 if nestedErr != nil && !v.env.isNotFound(res.RawResponse) { 426 logger.Warningf("failed to cleanup volume attachment: %v", volAttach.GetId()) 427 return 428 } 429 nestedErr = v.env.waitForResourceStatus( 430 v.getAttachmentStatus, volAttach.GetId(), 431 string(ociCore.VolumeAttachmentLifecycleStateDetached), 432 5*time.Minute) 433 if nestedErr != nil && !errors.IsNotFound(nestedErr) { 434 logger.Warningf("failed to cleanup volume attachment: %v", volAttach.GetId()) 435 return 436 } 437 } 438 }() 439 440 instances, err := v.env.getOciInstances(ctx, param.InstanceId) 441 if err != nil { 442 common.HandleCredentialError(err, ctx) 443 return storage.AttachVolumesResult{}, errors.Trace(err) 444 } 445 if len(instances) != 1 { 446 return storage.AttachVolumesResult{}, errors.Errorf("expected 1 instance, got %d", len(instances)) 447 } 448 inst := instances[0] 449 if inst.raw.LifecycleState == ociCore.InstanceLifecycleStateTerminated || inst.raw.LifecycleState == ociCore.InstanceLifecycleStateTerminating { 450 return storage.AttachVolumesResult{}, errors.Errorf("invalid instance state for volume attachment: %s", inst.raw.LifecycleState) 451 } 452 453 if err := inst.waitForMachineStatus( 454 ociCore.InstanceLifecycleStateRunning, 455 5*time.Minute); err != nil { 456 return storage.AttachVolumesResult{}, errors.Trace(err) 457 } 458 459 volumeAttachments, err := v.volumeAttachments(param.InstanceId) 460 if err != nil { 461 common.HandleCredentialError(err, ctx) 462 return storage.AttachVolumesResult{}, errors.Trace(err) 463 } 464 465 for _, val := range volumeAttachments { 466 if val.VolumeId == nil || val.InstanceId == nil { 467 continue 468 } 469 if *val.VolumeId == param.VolumeId && *val.InstanceId == string(param.InstanceId) { 470 // Volume already attached. Return info. 471 return makeVolumeAttachmentResult(val, param) 472 } 473 } 474 475 instID := string(param.InstanceId) 476 useChap := true 477 displayName := fmt.Sprintf("%s_%s", instID, param.VolumeId) 478 attachDetails := ociCore.AttachIScsiVolumeDetails{ 479 InstanceId: &instID, 480 VolumeId: ¶m.VolumeId, 481 UseChap: &useChap, 482 DisplayName: &displayName, 483 } 484 request := ociCore.AttachVolumeRequest{ 485 AttachVolumeDetails: attachDetails, 486 } 487 488 details, err = v.computeAPI.AttachVolume(context.Background(), request) 489 if err != nil { 490 common.HandleCredentialError(err, ctx) 491 return storage.AttachVolumesResult{}, errors.Trace(err) 492 } 493 494 err = v.env.waitForResourceStatus( 495 v.getAttachmentStatus, details.VolumeAttachment.GetId(), 496 string(ociCore.VolumeAttachmentLifecycleStateAttached), 497 5*time.Minute) 498 if err != nil { 499 common.HandleCredentialError(err, ctx) 500 return storage.AttachVolumesResult{}, errors.Trace(err) 501 } 502 503 detailsReq := ociCore.GetVolumeAttachmentRequest{ 504 VolumeAttachmentId: details.VolumeAttachment.GetId(), 505 } 506 507 response, err := v.computeAPI.GetVolumeAttachment(context.Background(), detailsReq) 508 if err != nil { 509 common.HandleCredentialError(err, ctx) 510 return storage.AttachVolumesResult{}, errors.Trace(err) 511 } 512 513 baseType, ok := response.VolumeAttachment.(ociCore.IScsiVolumeAttachment) 514 if !ok { 515 return storage.AttachVolumesResult{}, errors.Errorf("invalid attachment type. Expected iscsi") 516 } 517 518 return makeVolumeAttachmentResult(baseType, param) 519 } 520 521 func (v *volumeSource) getAttachmentStatus(resourceID *string) (string, error) { 522 request := ociCore.GetVolumeAttachmentRequest{ 523 VolumeAttachmentId: resourceID, 524 } 525 526 response, err := v.computeAPI.GetVolumeAttachment(context.Background(), request) 527 if err != nil { 528 if v.env.isNotFound(response.RawResponse) { 529 return "", errors.NotFoundf("volume attachment not found: %s", *resourceID) 530 } else { 531 return "", err 532 } 533 } 534 return string(response.VolumeAttachment.GetLifecycleState()), nil 535 } 536 537 func (v *volumeSource) AttachVolumes(ctx envcontext.ProviderCallContext, params []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) { 538 var instanceIds []instance.Id 539 for _, val := range params { 540 instanceIds = append(instanceIds, val.InstanceId) 541 } 542 if len(instanceIds) == 0 { 543 return []storage.AttachVolumesResult{}, nil 544 } 545 ret := make([]storage.AttachVolumesResult, len(params)) 546 instancesAsMap, err := v.env.getOciInstancesAsMap(ctx, instanceIds...) 547 if err != nil { 548 if isAuthFailure(err, ctx) { 549 common.HandleCredentialError(err, ctx) 550 // Exit out early to improve readability on handling credential 551 // errors. 552 for idx := range params { 553 ret[idx].Error = errors.Trace(err) 554 } 555 return ret, nil 556 } 557 return []storage.AttachVolumesResult{}, errors.Trace(err) 558 } 559 560 for idx, volParam := range params { 561 _, ok := instancesAsMap[volParam.InstanceId] 562 if !ok { 563 // this really should not happen, given how getOciInstancesAsMap() 564 // works 565 ret[idx].Error = errors.NotFoundf("instance %q was not found", volParam.InstanceId) 566 continue 567 } 568 569 result, err := v.attachVolume(ctx, volParam) 570 if err != nil { 571 if isAuthFailure(err, ctx) { 572 common.HandleCredentialError(err, ctx) 573 } 574 ret[idx].Error = errors.Trace(err) 575 } else { 576 ret[idx] = result 577 } 578 } 579 return ret, nil 580 } 581 582 func (v *volumeSource) DetachVolumes(ctx envcontext.ProviderCallContext, params []storage.VolumeAttachmentParams) ([]error, error) { 583 var credErr error 584 ret := make([]error, len(params)) 585 instanceAttachmentMap := map[instance.Id][]ociCore.IScsiVolumeAttachment{} 586 587 for idx, param := range params { 588 if credErr != nil { 589 ret[idx] = errors.Trace(credErr) 590 continue 591 } 592 593 instAtt, ok := instanceAttachmentMap[param.InstanceId] 594 if !ok { 595 currentAttachments, err := v.volumeAttachments(param.InstanceId) 596 if err != nil { 597 if isAuthFailure(err, ctx) { 598 credErr = err 599 common.HandleCredentialError(err, ctx) 600 } 601 ret[idx] = errors.Trace(err) 602 continue 603 } 604 instAtt = currentAttachments 605 instanceAttachmentMap[param.InstanceId] = instAtt 606 } 607 for _, attachment := range instAtt { 608 if credErr != nil { 609 ret[idx] = errors.Trace(credErr) 610 continue 611 } 612 logger.Tracef("volume ID is: %v", attachment.VolumeId) 613 if attachment.VolumeId != nil && param.VolumeId == *attachment.VolumeId && attachment.LifecycleState != ociCore.VolumeAttachmentLifecycleStateDetached { 614 if attachment.LifecycleState != ociCore.VolumeAttachmentLifecycleStateDetaching { 615 request := ociCore.DetachVolumeRequest{ 616 VolumeAttachmentId: attachment.Id, 617 } 618 619 res, err := v.computeAPI.DetachVolume(context.Background(), request) 620 if err != nil && !v.env.isNotFound(res.RawResponse) { 621 if isAuthFailure(err, ctx) { 622 credErr = err 623 common.HandleCredentialError(err, ctx) 624 } 625 ret[idx] = errors.Trace(err) 626 break 627 } 628 } 629 err := v.env.waitForResourceStatus( 630 v.getAttachmentStatus, attachment.Id, 631 string(ociCore.VolumeAttachmentLifecycleStateDetached), 632 5*time.Minute) 633 if err != nil && !errors.IsNotFound(err) { 634 if isAuthFailure(err, ctx) { 635 credErr = err 636 common.HandleCredentialError(err, ctx) 637 } 638 ret[idx] = errors.Trace(err) 639 logger.Warningf("failed to detach volume: %s", *attachment.Id) 640 } else { 641 ret[idx] = nil 642 } 643 } 644 } 645 } 646 return ret, nil 647 }