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  }