github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/oracle/storage_volumes.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package oracle 5 6 import ( 7 "fmt" 8 "sort" 9 "strings" 10 "sync" 11 "time" 12 13 "github.com/juju/clock" 14 "github.com/juju/errors" 15 oci "github.com/juju/go-oracle-cloud/api" 16 ociCommon "github.com/juju/go-oracle-cloud/common" 17 ociResponse "github.com/juju/go-oracle-cloud/response" 18 19 "github.com/juju/juju/core/instance" 20 "github.com/juju/juju/environs/context" 21 "github.com/juju/juju/environs/tags" 22 "github.com/juju/juju/storage" 23 ) 24 25 // oracleVolumeSource implements the storage.VolumeSource interface 26 type oracleVolumeSource struct { 27 env *OracleEnviron 28 envName string // non-unique, informational only 29 modelUUID string 30 api StorageAPI 31 clock clock.Clock 32 } 33 34 // newOracleVolumeSource returns a new volume source to provide an interface 35 // for creating, destroying, describing attaching and detaching volumes in the 36 // oracle cloud environment 37 func newOracleVolumeSource(env *OracleEnviron, name, uuid string, api StorageAPI, clock clock.Clock) (*oracleVolumeSource, error) { 38 if env == nil { 39 return nil, errors.NotFoundf("environ") 40 } 41 42 if api == nil { 43 return nil, errors.NotFoundf("storage client") 44 } 45 46 return &oracleVolumeSource{ 47 env: env, 48 envName: name, 49 modelUUID: uuid, 50 api: api, 51 clock: clock, 52 }, nil 53 } 54 55 var _ storage.VolumeSource = (*oracleVolumeSource)(nil) 56 57 // resourceName returns an oracle compatible resource name. 58 func (s *oracleVolumeSource) resourceName(tag string) string { 59 return s.api.ComposeName(s.env.namespace.Value(s.envName + "-" + tag)) 60 } 61 62 func (s *oracleVolumeSource) getStoragePool(attr map[string]interface{}) (ociCommon.StoragePool, error) { 63 volumeType, ok := attr[oracleVolumeType] 64 if !ok { 65 return poolTypeMap[defaultPool], nil 66 } 67 switch volumeType.(type) { 68 case poolType: 69 if t, ok := poolTypeMap[volumeType.(poolType)]; ok { 70 return t, nil 71 } 72 return poolTypeMap[defaultPool], errors.NotFoundf("storage pool %q not found", volumeType.(poolType)) 73 } 74 return poolTypeMap[defaultPool], nil 75 } 76 77 // createVolume will create a storage volume given the storage volume parameters 78 // under the oracle cloud endpoint 79 func (s *oracleVolumeSource) createVolume(p storage.VolumeParams) (_ *storage.Volume, err error) { 80 var details ociResponse.StorageVolume 81 defer func() { 82 // gsamfira: not really sure if this is needed. The only relevant error 83 // on which we act is the one returned by the oracle API when creating 84 // the volume. If the API returned an error, there is little chance, that 85 // a volume was created. But for the sake of thoroughness, let's leave this 86 // here 87 if err != nil && details.Name != "" { 88 _ = s.api.DeleteStorageVolume(details.Name) 89 } 90 }() 91 92 // validate the parameters 93 if err := s.ValidateVolumeParams(p); err != nil { 94 return nil, errors.Trace(err) 95 } 96 name := s.resourceName(p.Tag.String()) 97 size := mibToGib(p.Size) 98 if size > maxVolumeSizeInGB || size < minVolumeSizeInGB { 99 return nil, errors.Errorf("invalid size for volume: %d", size) 100 } 101 102 poolType, err := s.getStoragePool(p.Attributes) 103 if err != nil { 104 return nil, errors.Trace(err) 105 } 106 volumeTags := []string{p.Tag.String()} 107 for k, v := range p.ResourceTags { 108 volumeTags = append(volumeTags, fmt.Sprintf("%s=%s", k, v)) 109 } 110 111 params := oci.StorageVolumeParams{ 112 Bootable: false, 113 Description: fmt.Sprintf("Juju created volume for %q", p.Tag.String()), 114 Name: name, 115 Properties: []ociCommon.StoragePool{ 116 poolType, 117 }, 118 Size: ociCommon.NewStorageSize(size, ociCommon.G), 119 Tags: volumeTags, 120 } 121 logger.Infof("creating volume: %v", params) 122 details, err = s.api.CreateStorageVolume(params) 123 if oci.IsStatusConflict(err) { 124 // Volume already exists, so return its details. 125 conflictErr := err 126 details, err = s.api.StorageVolumeDetails(name) 127 if err != nil { 128 return nil, errors.Trace(err) 129 } 130 var modelTagValue string 131 for _, tag := range details.Tags { 132 prefix := tags.JujuModel + "=" 133 if !strings.HasPrefix(tag, prefix) { 134 continue 135 } 136 modelTagValue = tag[len(prefix):] 137 } 138 if modelTagValue != s.modelUUID { 139 return nil, errors.Trace(conflictErr) 140 } 141 return &storage.Volume{p.Tag, makeVolumeInfo(details)}, nil 142 } else if err != nil { 143 return nil, errors.Trace(err) 144 } 145 146 // wait for the newly created volume to reach "Online" status 147 logger.Debugf("waiting for resource %v", details.Name) 148 if err := s.waitForResourceStatus( 149 s.fetchVolumeStatus, 150 details.Name, 151 string(ociCommon.VolumeOnline), 5*time.Minute); err != nil { 152 return nil, errors.Trace(err) 153 } 154 volume := &storage.Volume{p.Tag, makeVolumeInfo(details)} 155 logger.Debugf("volume details: %v", volume) 156 return volume, nil 157 } 158 159 // CreateVolumes is specified on the storage.VolumeSource interface 160 func (s *oracleVolumeSource) CreateVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams) ([]storage.CreateVolumesResult, error) { 161 if params == nil { 162 return []storage.CreateVolumesResult{}, nil 163 } 164 results := make([]storage.CreateVolumesResult, len(params)) 165 for i, volume := range params { 166 vol, err := s.createVolume(volume) 167 if err != nil { 168 results[i].Error = errors.Trace(err) 169 continue 170 } 171 results[i].Volume = vol 172 } 173 return results, nil 174 } 175 176 // fetchVolumeStatus polls the status of a volume and returns true if the current status 177 // coincides with the desired status 178 func (s *oracleVolumeSource) fetchVolumeStatus(name, desiredStatus string) (complete bool, err error) { 179 details, err := s.api.StorageVolumeDetails(name) 180 if err != nil { 181 return false, errors.Trace(err) 182 } 183 184 if details.Status == ociCommon.VolumeError { 185 return false, errors.Errorf("volume entered error state: %q", details.Status_detail) 186 } 187 return string(details.Status) == desiredStatus, nil 188 } 189 190 // fetchVolumeAttachmentStatus polls the status of a volume attachment and returns true if the current status 191 // coincides with the desired status 192 func (s *oracleVolumeSource) fetchVolumeAttachmentStatus(name, desiredStatus string) (bool, error) { 193 details, err := s.api.StorageAttachmentDetails(name) 194 if err != nil { 195 return false, errors.Trace(err) 196 } 197 return string(details.State) == desiredStatus, nil 198 } 199 200 // waitForResourceStatus will ping the resource until the fetch function returns true, 201 // the timeout is reached, or an error occurs. 202 func (o *oracleVolumeSource) waitForResourceStatus( 203 fetch func(name string, desiredStatus string) (complete bool, err error), 204 name, state string, 205 timeout time.Duration, 206 ) error { 207 208 timeoutTimer := o.clock.NewTimer(timeout) 209 defer timeoutTimer.Stop() 210 211 retryTimer := o.clock.NewTimer(0) 212 defer retryTimer.Stop() 213 214 for { 215 select { 216 case <-retryTimer.Chan(): 217 done, err := fetch(name, state) 218 if err != nil { 219 return err 220 } 221 if done { 222 return nil 223 } 224 retryTimer.Reset(2 * time.Second) 225 case <-timeoutTimer.Chan(): 226 return errors.Errorf( 227 "timed out waiting for resource %q to transition to %v", 228 name, state, 229 ) 230 } 231 } 232 } 233 234 // ListVolumes is specified on the storage.VolumeSource interface. 235 func (s *oracleVolumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) { 236 tag := fmt.Sprintf("%s=%s", tags.JujuModel, s.modelUUID) 237 filter := []oci.Filter{{ 238 Arg: "tags", 239 Value: tag, 240 }} 241 volumes, err := s.api.AllStorageVolumes(filter) 242 if err != nil { 243 return nil, errors.Annotate(err, "listing volumes") 244 } 245 246 ids := make([]string, len(volumes.Result)) 247 for i, volume := range volumes.Result { 248 ids[i] = volume.Name 249 } 250 251 return ids, nil 252 } 253 254 // DescribeVolumes is specified on the storage.VolumeSource interface. 255 func (s *oracleVolumeSource) DescribeVolumes(ctx context.ProviderCallContext, volIds []string) ([]storage.DescribeVolumesResult, error) { 256 if volIds == nil || len(volIds) == 0 { 257 return []storage.DescribeVolumesResult{}, nil 258 } 259 260 tag := fmt.Sprintf("%s=%s", tags.JujuModel, s.modelUUID) 261 filter := []oci.Filter{{ 262 Arg: "tags", 263 Value: tag, 264 }} 265 266 result := make([]storage.DescribeVolumesResult, len(volIds), len(volIds)) 267 volumes, err := s.api.AllStorageVolumes(filter) 268 if err != nil { 269 return nil, errors.Annotatef(err, "describe volumes") 270 } 271 asMap := map[string]ociResponse.StorageVolume{} 272 for _, val := range volumes.Result { 273 asMap[val.Name] = val 274 } 275 for i, volume := range volIds { 276 if vol, ok := asMap[volume]; ok { 277 volumeInfo := makeVolumeInfo(vol) 278 result[i].VolumeInfo = &volumeInfo 279 } else { 280 result[i].Error = errors.NotFoundf("%s", volume) 281 } 282 } 283 return result, nil 284 } 285 286 func makeVolumeInfo(vol ociResponse.StorageVolume) storage.VolumeInfo { 287 return storage.VolumeInfo{ 288 VolumeId: vol.Name, 289 // Oracle returns the size of the volume 290 // in bytes, VolumeInfo expects MiB. 291 Size: uint64(vol.Size) / (1024 * 1024), 292 Persistent: true, 293 } 294 } 295 296 // DestroyVolumes is specified on the storage.VolumeSource interface. 297 func (s *oracleVolumeSource) DestroyVolumes(ctx context.ProviderCallContext, volIds []string) ([]error, error) { 298 return foreachVolume(volIds, s.api.DeleteStorageVolume), nil 299 } 300 301 // ReleaseVolumes is specified on the storage.VolumeSource interface. 302 func (s *oracleVolumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volIds []string) ([]error, error) { 303 releaseStorageVolume := func(volumeId string) error { 304 details, err := s.api.StorageVolumeDetails(volumeId) 305 if err != nil { 306 return errors.Trace(err) 307 } 308 var newTags []string 309 for _, tag := range details.Tags { 310 fields := strings.Split(tag, "=") 311 if len(fields) != 2 { 312 newTags = append(newTags, tag) 313 continue 314 } 315 switch fields[0] { 316 case tags.JujuController, tags.JujuModel: 317 default: 318 newTags = append(newTags, tag) 319 } 320 } 321 if len(newTags) == len(details.Tags) { 322 return nil 323 } 324 details.Tags = newTags 325 return errors.Trace(s.updateVolume(volumeId, details)) 326 } 327 return foreachVolume(volIds, releaseStorageVolume), nil 328 } 329 330 func foreachVolume(volIds []string, f func(string) error) []error { 331 results := make([]error, len(volIds)) 332 wg := sync.WaitGroup{} 333 wg.Add(len(volIds)) 334 for i, val := range volIds { 335 go func(volId string, idx int) { 336 defer wg.Done() 337 results[idx] = f(volId) 338 }(val, i) 339 } 340 wg.Wait() 341 return results 342 } 343 344 // ImportVolume is specified on the storage.VolumeImporter interface. 345 func (s *oracleVolumeSource) ImportVolume(ctx context.ProviderCallContext, volumeId string, tags map[string]string) (storage.VolumeInfo, error) { 346 details, err := s.api.StorageVolumeDetails(volumeId) 347 if err != nil { 348 return storage.VolumeInfo{}, errors.Trace(err) 349 } 350 var newTags []string 351 for _, tag := range details.Tags { 352 fields := strings.Split(tag, "=") 353 if len(fields) != 2 { 354 newTags = append(newTags, tag) 355 continue 356 } 357 key, value := fields[0], fields[1] 358 if newValue, ok := tags[key]; !ok || newValue == value { 359 delete(tags, key) 360 newTags = append(newTags, tag) 361 continue 362 } 363 // The tag has changed; we'll add it in the loop below. 364 } 365 if len(tags) != 0 { 366 for key, value := range tags { 367 newTags = append(newTags, fmt.Sprintf("%s=%s", key, value)) 368 } 369 details.Tags = newTags 370 if err := s.updateVolume(volumeId, details); err != nil { 371 return storage.VolumeInfo{}, errors.Trace(err) 372 } 373 } 374 return makeVolumeInfo(details), nil 375 } 376 377 func (s *oracleVolumeSource) updateVolume(volumeId string, details ociResponse.StorageVolume) error { 378 derefString := func(s *string) string { 379 if s != nil { 380 return *s 381 } 382 return "" 383 } 384 _, err := s.api.UpdateStorageVolume( 385 oci.StorageVolumeParams{ 386 Bootable: details.Bootable, 387 Description: derefString(details.Description), 388 Imagelist: details.Imagelist, 389 Imagelist_entry: details.Imagelist_entry, 390 Name: details.Name, 391 Properties: details.Properties, 392 Size: ociCommon.StorageSize(details.Size), 393 Snapshot: derefString(details.Snapshot), 394 Snapshot_account: details.Snapshot_account, 395 Snapshot_id: details.Snapshot_id, 396 Tags: details.Tags, 397 }, 398 volumeId, 399 ) 400 return errors.Annotatef(err, "updating volume %q", volumeId) 401 } 402 403 // ValidateVolumeParams is specified on the storage.VolumeSource interface. 404 func (s *oracleVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error { 405 size := mibToGib(params.Size) 406 if size > maxVolumeSizeInGB || size < minVolumeSizeInGB { 407 return errors.Errorf("invalid size for volume in GiB %d", size) 408 } 409 return nil 410 } 411 412 func (s *oracleVolumeSource) getStorageAttachments() (map[string][]ociResponse.StorageAttachment, error) { 413 allAttachments, err := s.api.AllStorageAttachments(nil) 414 if err != nil { 415 return nil, errors.Trace(err) 416 } 417 asMap := map[string][]ociResponse.StorageAttachment{} 418 for _, val := range allAttachments.Result { 419 hostname, err := extractInstanceIDFromMachineName(val.Instance_name) 420 if err != nil { 421 return nil, err 422 } 423 if _, ok := asMap[string(hostname)]; !ok { 424 asMap[string(hostname)] = []ociResponse.StorageAttachment{ 425 val, 426 } 427 } else { 428 asMap[string(hostname)] = append(asMap[string(hostname)], val) 429 } 430 } 431 return asMap, nil 432 } 433 434 // AttachVolumes is specified on the storage.VolumeSource interface. 435 func (s *oracleVolumeSource) AttachVolumes(ctx context.ProviderCallContext, params []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) { 436 instanceIds := []instance.Id{} 437 for _, val := range params { 438 instanceIds = append(instanceIds, val.InstanceId) 439 } 440 if len(instanceIds) == 0 { 441 return []storage.AttachVolumesResult{}, nil 442 } 443 instancesAsMap, err := s.env.getOracleInstancesAsMap(instanceIds...) 444 if err != nil { 445 return []storage.AttachVolumesResult{}, errors.Trace(err) 446 } 447 attachmentsAsMap, err := s.getStorageAttachments() 448 if err != nil { 449 return []storage.AttachVolumesResult{}, errors.Trace(err) 450 } 451 452 ret := make([]storage.AttachVolumesResult, len(params)) 453 454 for i, val := range params { 455 instance, ok := instancesAsMap[string(val.InstanceId)] 456 if !ok { 457 ret[i].Error = errors.NotFoundf("instance %q was not found", string(val.InstanceId)) 458 continue 459 } 460 461 result, err := s.attachVolume(instance, attachmentsAsMap, val) 462 if err != nil { 463 ret[i].Error = errors.Trace(err) 464 continue 465 } 466 ret[i] = result 467 468 } 469 logger.Infof("returning attachments: %v", ret) 470 return ret, nil 471 } 472 473 // getFreeIndexNumber returns the first unused consecutive value in a sorted array of ints 474 // this is used to find an available index number for attaching a volume to an instance 475 func (s *oracleVolumeSource) getFreeIndexNumber(existing []int, max int) (int, error) { 476 if len(existing) == 0 { 477 return 1, nil 478 } 479 sort.Ints(existing) 480 for i := 0; i <= len(existing)-1; i++ { 481 if i+1 >= max { 482 break 483 } 484 if i+1 == len(existing) { 485 return existing[i] + 1, nil 486 } 487 if existing[0] > 1 { 488 return existing[0] - 1, nil 489 } 490 diff := existing[i+1] - existing[i] 491 if diff > 1 { 492 return existing[i] + 1, nil 493 } 494 } 495 return 0, errors.Errorf("no free index") 496 } 497 498 func (s *oracleVolumeSource) getDeviceNameForIndex(idx int) string { 499 // We use an ephemeral disk when booting instances, so we get 500 // the full range of 10 disks we can attach to an instance. 501 // Alternatively, we can create a volume from an image and attach 502 // it to the launchplan, and set it as a boot device. 503 // NOTE(gsamfira): if we ever decide to boot from volume, this 504 // needs to be addressed to return the proper device name 505 return fmt.Sprintf("%s%s", blockDevicePrefix, string([]byte{blockDeviceStartIndex + byte(idx)})) 506 } 507 508 func (s *oracleVolumeSource) attachVolume( 509 instance *oracleInstance, 510 currentAttachments map[string][]ociResponse.StorageAttachment, 511 params storage.VolumeAttachmentParams) (storage.AttachVolumesResult, error) { 512 513 // keep track of all indexes of volumes attached to the instance 514 existingIndexes := []int{} 515 instanceStorage := instance.StorageAttachments() 516 // append index numbers of volumes that were attached when creating the 517 // launchpan. Not the case in the current implementation of the provider 518 // but should this change in the future, this function will still work as 519 // expected. 520 // For information about attaching volumes at instance creation time, please 521 // see: https://docs.oracle.com/cloud/latest/stcomputecs/STCSA/op-launchplan--post.html 522 for _, val := range instanceStorage { 523 existingIndexes = append(existingIndexes, int(val.Index)) 524 } 525 526 for _, val := range currentAttachments[string(instance.Id())] { 527 // index numbers range from 1 to 10. Ignore 0 valued indexes 528 // see: https://docs.oracle.com/cloud/latest/stcomputecs/STCSA/op-storage-attachment--post.html 529 if val.Index == 0 { 530 continue 531 } 532 if val.Storage_volume_name == params.VolumeId && val.Instance_name == string(params.InstanceId) { 533 // volume is already attached to this instance. Simply return it. 534 return storage.AttachVolumesResult{ 535 VolumeAttachment: &storage.VolumeAttachment{ 536 params.Volume, 537 params.Machine, 538 storage.VolumeAttachmentInfo{ 539 DeviceName: s.getDeviceNameForIndex(int(val.Index)), 540 }, 541 }, 542 }, nil 543 } 544 // append any indexes for volumes that were attached dynamically (after instance creation) 545 existingIndexes = append(existingIndexes, int(val.Index)) 546 } 547 548 logger.Infof("fetching free index. Existing: %v, Max: %v", existingIndexes, maxDevices) 549 // gsamfira: fetch a free index number for this disk. There is a limit of 10 disks that can be attached to any 550 // instance. The index number dictates the order in which the operating system will see the disks 551 // Essentially an index for an attachment can be equated to the bus number that the disk will be made 552 // available on inside the guest. This way, an index number of 1 will be (on a linux host) xvda, index 2 553 // will be xvdb, and so on. One exception to this rule; if you boot an instance using an ephemeral disk 554 // (which we currently do), then inside the guest, that disk will be xvda. Index 1 will be xvdb, index 2 555 // will be xvdc and so on. Booting from ephemeral disks also has the added advantage that you get one 556 // extra disk attachment on the instance, and it saves us the trouble of running another operation to 557 // create the root disk from an image. 558 idx, err := s.getFreeIndexNumber(existingIndexes, maxDevices) 559 if err != nil { 560 return storage.AttachVolumesResult{Error: errors.Trace(err)}, nil 561 } 562 563 p := oci.StorageAttachmentParams{ 564 Index: ociCommon.Index(idx), 565 Instance_name: instance.machine.Name, 566 Storage_volume_name: params.VolumeId, 567 } 568 details, err := s.api.CreateStorageAttachment(p) 569 if err != nil { 570 return storage.AttachVolumesResult{Error: errors.Trace(err)}, nil 571 } 572 if err := s.waitForResourceStatus( 573 s.fetchVolumeAttachmentStatus, 574 details.Name, 575 string(ociCommon.StateAttached), 5*time.Minute); err != nil { 576 577 currentAttachments[string(instance.Id())] = append(currentAttachments[string(instance.Id())], details) 578 return storage.AttachVolumesResult{Error: errors.Trace(err)}, nil 579 } 580 currentAttachments[string(instance.Id())] = append(currentAttachments[string(instance.Id())], details) 581 582 // TODO (gsamfira): make this more OS agnostic. In Windows you get disk indexes 583 // instead of device names; however storage is not supported on windows instances (yet). 584 result := storage.AttachVolumesResult{ 585 VolumeAttachment: &storage.VolumeAttachment{ 586 params.Volume, 587 params.Machine, 588 storage.VolumeAttachmentInfo{ 589 DeviceName: s.getDeviceNameForIndex(idx), 590 }, 591 }, 592 } 593 return result, nil 594 } 595 596 // DetachVolumes is specified on the storage.VolumeSource interface. 597 func (s *oracleVolumeSource) DetachVolumes(ctx context.ProviderCallContext, params []storage.VolumeAttachmentParams) ([]error, error) { 598 attachAsMap, err := s.getStorageAttachments() 599 if err != nil { 600 return nil, errors.Trace(err) 601 } 602 toDelete := make([]string, len(params)) 603 ret := make([]error, len(params)) 604 for i, val := range params { 605 found := false 606 for _, attach := range attachAsMap[string(val.InstanceId)] { 607 if val.VolumeId == attach.Storage_volume_name { 608 toDelete[i] = attach.Name 609 found = true 610 } 611 } 612 if !found { 613 toDelete[i] = "" 614 ret[i] = errors.NotFoundf( 615 "volume attachment for instance %v and volumeID %v not found", 616 val.InstanceId, val.VolumeId) 617 } 618 } 619 for i, val := range toDelete { 620 if val == "" { 621 continue 622 } 623 ret[i] = s.api.DeleteStorageAttachment(val) 624 } 625 return ret, nil 626 }