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