github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/provider/azure/disks.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package azure
     5  
     6  import (
     7  	"fmt"
     8  	"path"
     9  	"strings"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/schema"
    13  	"launchpad.net/gwacl"
    14  
    15  	"github.com/juju/juju/environs/config"
    16  	"github.com/juju/juju/instance"
    17  	"github.com/juju/juju/storage"
    18  )
    19  
    20  const (
    21  	storageProviderType = storage.ProviderType("azure")
    22  )
    23  
    24  const (
    25  	// volumeSizeMaxGiB is the maximum disk size (in gibibytes) for Azure disks.
    26  	//
    27  	// See: https://azure.microsoft.com/en-gb/documentation/articles/virtual-machines-disks-vhds/
    28  	volumeSizeMaxGiB = 1023
    29  )
    30  
    31  // azureStorageProvider is a storage provider for Azure disks.
    32  type azureStorageProvider struct{}
    33  
    34  var _ storage.Provider = (*azureStorageProvider)(nil)
    35  
    36  var azureStorageConfigFields = schema.Fields{}
    37  
    38  var azureStorageConfigChecker = schema.FieldMap(
    39  	azureStorageConfigFields,
    40  	schema.Defaults{},
    41  )
    42  
    43  type azureStorageConfig struct {
    44  }
    45  
    46  func newAzureStorageConfig(attrs map[string]interface{}) (*azureStorageConfig, error) {
    47  	_, err := azureStorageConfigChecker.Coerce(attrs, nil)
    48  	if err != nil {
    49  		return nil, errors.Annotate(err, "validating Azure storage config")
    50  	}
    51  	azureStorageConfig := &azureStorageConfig{}
    52  	return azureStorageConfig, nil
    53  }
    54  
    55  // ValidateConfig is defined on the Provider interface.
    56  func (e *azureStorageProvider) ValidateConfig(cfg *storage.Config) error {
    57  	_, err := newAzureStorageConfig(cfg.Attrs())
    58  	return errors.Trace(err)
    59  }
    60  
    61  // Supports is defined on the Provider interface.
    62  func (e *azureStorageProvider) Supports(k storage.StorageKind) bool {
    63  	return k == storage.StorageKindBlock
    64  }
    65  
    66  // Scope is defined on the Provider interface.
    67  func (e *azureStorageProvider) Scope() storage.Scope {
    68  	return storage.ScopeEnviron
    69  }
    70  
    71  // Dynamic is defined on the Provider interface.
    72  func (e *azureStorageProvider) Dynamic() bool {
    73  	return true
    74  }
    75  
    76  // VolumeSource is defined on the Provider interface.
    77  func (e *azureStorageProvider) VolumeSource(environConfig *config.Config, cfg *storage.Config) (storage.VolumeSource, error) {
    78  	env, err := NewEnviron(environConfig)
    79  	if err != nil {
    80  		return nil, errors.Trace(err)
    81  	}
    82  	uuid, ok := environConfig.UUID()
    83  	if !ok {
    84  		return nil, errors.NotFoundf("environment UUID")
    85  	}
    86  	source := &azureVolumeSource{
    87  		env:     env,
    88  		envName: environConfig.Name(),
    89  		envUUID: uuid,
    90  	}
    91  	return source, nil
    92  }
    93  
    94  // FilesystemSource is defined on the Provider interface.
    95  func (e *azureStorageProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) {
    96  	return nil, errors.NotSupportedf("filesystems")
    97  }
    98  
    99  type azureVolumeSource struct {
   100  	env     *azureEnviron
   101  	envName string // non-unique, informational only
   102  	envUUID string
   103  }
   104  
   105  var _ storage.VolumeSource = (*azureVolumeSource)(nil)
   106  
   107  // CreateVolumes is specified on the storage.VolumeSource interface.
   108  func (v *azureVolumeSource) CreateVolumes(params []storage.VolumeParams) (_ []storage.CreateVolumesResult, err error) {
   109  
   110  	// First, validate the params before we use them.
   111  	results := make([]storage.CreateVolumesResult, len(params))
   112  	for i, p := range params {
   113  		if err := v.ValidateVolumeParams(p); err != nil {
   114  			results[i].Error = err
   115  			continue
   116  		}
   117  	}
   118  
   119  	// TODO(axw) cache results of GetRole from createVolume for multiple
   120  	// attachments to the same machine. When doing so, be careful to
   121  	// ensure the cached role's in-use LUNs are updated between attachments.
   122  
   123  	for i, p := range params {
   124  		if results[i].Error != nil {
   125  			continue
   126  		}
   127  		volume, volumeAttachment, err := v.createVolume(p)
   128  		if err != nil {
   129  			results[i].Error = err
   130  			continue
   131  		}
   132  		results[i].Volume = volume
   133  		results[i].VolumeAttachment = volumeAttachment
   134  	}
   135  	return results, nil
   136  }
   137  
   138  func (v *azureVolumeSource) createVolume(p storage.VolumeParams) (*storage.Volume, *storage.VolumeAttachment, error) {
   139  	cloudServiceName, roleName := v.env.splitInstanceId(p.Attachment.InstanceId)
   140  	if roleName == "" {
   141  		return nil, nil, errors.NotSupportedf("attaching disks to legacy instances")
   142  	}
   143  	deploymentName := deploymentNameV2(cloudServiceName)
   144  
   145  	// Get the role first so we know which LUNs are in use.
   146  	role, err := v.getRole(p.Attachment.InstanceId)
   147  	if err != nil {
   148  		return nil, nil, errors.Annotate(err, "getting role")
   149  	}
   150  	lun, err := nextAvailableLUN(role)
   151  	if err != nil {
   152  		return nil, nil, errors.Annotate(err, "choosing LUN")
   153  	}
   154  
   155  	diskLabel := fmt.Sprintf("%s%s", v.env.getEnvPrefix(), p.Tag.String())
   156  	vhdFilename := p.Tag.String() + ".vhd"
   157  	mediaLink := v.vhdMediaLinkPrefix() + vhdFilename
   158  
   159  	// Create and attach a disk to the instance.
   160  	sizeInGib := mibToGib(p.Size)
   161  	if err := v.env.api.AddDataDisk(&gwacl.AddDataDiskRequest{
   162  		ServiceName:    cloudServiceName,
   163  		DeploymentName: deploymentName,
   164  		RoleName:       roleName,
   165  		DataVirtualHardDisk: gwacl.DataVirtualHardDisk{
   166  			DiskLabel:           diskLabel,
   167  			LogicalDiskSizeInGB: int(sizeInGib),
   168  			MediaLink:           mediaLink,
   169  			LUN:                 lun,
   170  		},
   171  	}); err != nil {
   172  		return nil, nil, errors.Annotate(err, "adding data disk")
   173  	}
   174  
   175  	// Data disks associate VHDs to machines. In Juju's storage model,
   176  	// the VHD is the volume and the disk is the volume attachment.
   177  	//
   178  	// Note that we don't currently support attaching/detaching volumes
   179  	// (see note on Persistent below), but using the VHD name as the
   180  	// volume ID at least allows that as a future option.
   181  	volumeId := vhdFilename
   182  
   183  	volume := storage.Volume{
   184  		p.Tag,
   185  		storage.VolumeInfo{
   186  			VolumeId: volumeId,
   187  			Size:     gibToMib(sizeInGib),
   188  			// We don't currently support persistent volumes in
   189  			// Azure, as it requires removal of "comp=media" when
   190  			// deleting VMs, complicating cleanup.
   191  			Persistent: false,
   192  		},
   193  	}
   194  	volumeAttachment := storage.VolumeAttachment{
   195  		p.Tag,
   196  		p.Attachment.Machine,
   197  		storage.VolumeAttachmentInfo{
   198  			BusAddress: diskBusAddress(lun),
   199  		},
   200  	}
   201  	return &volume, &volumeAttachment, nil
   202  }
   203  
   204  // vhdMediaLinkPrefix returns the media link prefix for disks
   205  // associated with the environment. gwacl's helper returns
   206  // http scheme URLs; we use https to simplify matching what
   207  // Azure returns. Azure always returns https, even if you
   208  // specified http originally.
   209  func (v *azureVolumeSource) vhdMediaLinkPrefix() string {
   210  	storageAccount := v.env.ecfg.storageAccountName()
   211  	dir := path.Join("vhds", v.envUUID)
   212  	mediaLink := gwacl.CreateVirtualHardDiskMediaLink(storageAccount, dir) + "/"
   213  	mediaLink = "https://" + mediaLink[len("http://"):]
   214  	return mediaLink
   215  }
   216  
   217  // ListVolumes is specified on the storage.VolumeSource interface.
   218  func (v *azureVolumeSource) ListVolumes() ([]string, error) {
   219  	disks, err := v.listDisks()
   220  	if err != nil {
   221  		return nil, errors.Trace(err)
   222  	}
   223  	volumeIds := make([]string, len(disks))
   224  	for i, disk := range disks {
   225  		_, volumeId := path.Split(disk.MediaLink)
   226  		volumeIds[i] = volumeId
   227  	}
   228  	return volumeIds, nil
   229  }
   230  
   231  func (v *azureVolumeSource) listDisks() ([]gwacl.Disk, error) {
   232  	disks, err := v.env.api.ListDisks()
   233  	if err != nil {
   234  		return nil, errors.Annotate(err, "listing disks")
   235  	}
   236  	mediaLinkPrefix := v.vhdMediaLinkPrefix()
   237  	matching := make([]gwacl.Disk, 0, len(disks))
   238  	for _, disk := range disks {
   239  		if strings.HasPrefix(disk.MediaLink, mediaLinkPrefix) {
   240  			matching = append(matching, disk)
   241  		}
   242  	}
   243  	return matching, nil
   244  }
   245  
   246  // DescribeVolumes is specified on the storage.VolumeSource interface.
   247  func (v *azureVolumeSource) DescribeVolumes(volIds []string) ([]storage.DescribeVolumesResult, error) {
   248  	disks, err := v.listDisks()
   249  	if err != nil {
   250  		return nil, errors.Annotate(err, "listing disks")
   251  	}
   252  
   253  	byVolumeId := make(map[string]gwacl.Disk)
   254  	for _, disk := range disks {
   255  		_, volumeId := path.Split(disk.MediaLink)
   256  		byVolumeId[volumeId] = disk
   257  	}
   258  
   259  	results := make([]storage.DescribeVolumesResult, len(volIds))
   260  	for i, volumeId := range volIds {
   261  		disk, ok := byVolumeId[volumeId]
   262  		if !ok {
   263  			results[i].Error = errors.NotFoundf("volume %v", volumeId)
   264  			continue
   265  		}
   266  		results[i].VolumeInfo = &storage.VolumeInfo{
   267  			VolumeId: volumeId,
   268  			Size:     gibToMib(uint64(disk.LogicalSizeInGB)),
   269  			// We don't support persistent volumes at the moment;
   270  			// see CreateVolumes.
   271  			Persistent: false,
   272  		}
   273  	}
   274  
   275  	return results, nil
   276  }
   277  
   278  // DestroyVolumes is specified on the storage.VolumeSource interface.
   279  func (v *azureVolumeSource) DestroyVolumes(volIds []string) ([]error, error) {
   280  	// We don't currently support persistent volumes.
   281  	return nil, errors.NotSupportedf("DestroyVolumes")
   282  }
   283  
   284  // ValidateVolumeParams is specified on the storage.VolumeSource interface.
   285  func (v *azureVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error {
   286  	if mibToGib(params.Size) > volumeSizeMaxGiB {
   287  		return errors.Errorf(
   288  			"%d GiB exceeds the maximum of %d GiB",
   289  			mibToGib(params.Size),
   290  			volumeSizeMaxGiB,
   291  		)
   292  	}
   293  	return nil
   294  }
   295  
   296  // AttachVolumes is specified on the storage.VolumeSource interface.
   297  func (v *azureVolumeSource) AttachVolumes(attachParams []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) {
   298  	// We don't currently support persistent volumes, but we do need to
   299  	// support "reattaching" volumes to machines; i.e. just verify that
   300  	// the attachment is in place, and fail otherwise.
   301  
   302  	type maybeRole struct {
   303  		role *gwacl.PersistentVMRole
   304  		err  error
   305  	}
   306  
   307  	roles := make(map[instance.Id]maybeRole)
   308  	for _, p := range attachParams {
   309  		if _, ok := roles[p.InstanceId]; ok {
   310  			continue
   311  		}
   312  		role, err := v.getRole(p.InstanceId)
   313  		roles[p.InstanceId] = maybeRole{role, err}
   314  	}
   315  
   316  	results := make([]storage.AttachVolumesResult, len(attachParams))
   317  	for i, p := range attachParams {
   318  		maybeRole := roles[p.InstanceId]
   319  		if maybeRole.err != nil {
   320  			results[i].Error = maybeRole.err
   321  			continue
   322  		}
   323  		volumeAttachment, err := v.attachVolume(p, maybeRole.role)
   324  		if err != nil {
   325  			results[i].Error = err
   326  			continue
   327  		}
   328  		results[i].VolumeAttachment = volumeAttachment
   329  	}
   330  	return results, nil
   331  }
   332  
   333  func (v *azureVolumeSource) attachVolume(
   334  	p storage.VolumeAttachmentParams,
   335  	role *gwacl.PersistentVMRole,
   336  ) (*storage.VolumeAttachment, error) {
   337  
   338  	var disks []gwacl.DataVirtualHardDisk
   339  	if role.DataVirtualHardDisks != nil {
   340  		disks = *role.DataVirtualHardDisks
   341  	}
   342  
   343  	// Check if the disk is already attached to the machine.
   344  	mediaLinkPrefix := v.vhdMediaLinkPrefix()
   345  	for _, disk := range disks {
   346  		if !strings.HasPrefix(disk.MediaLink, mediaLinkPrefix) {
   347  			continue
   348  		}
   349  		_, volumeId := path.Split(disk.MediaLink)
   350  		if volumeId != p.VolumeId {
   351  			continue
   352  		}
   353  		return &storage.VolumeAttachment{
   354  			p.Volume,
   355  			p.Machine,
   356  			storage.VolumeAttachmentInfo{
   357  				BusAddress: diskBusAddress(disk.LUN),
   358  			},
   359  		}, nil
   360  	}
   361  
   362  	// If the disk is not attached already, the AttachVolumes call must
   363  	// fail. We do not support persistent volumes at the moment, and if
   364  	// we get here it means that the disk has been detached out of band.
   365  	return nil, errors.NotSupportedf("attaching volumes")
   366  }
   367  
   368  // DetachVolumes is specified on the storage.VolumeSource interface.
   369  func (v *azureVolumeSource) DetachVolumes(attachParams []storage.VolumeAttachmentParams) ([]error, error) {
   370  	// We don't currently support persistent volumes.
   371  	return nil, errors.NotSupportedf("detaching volumes")
   372  }
   373  
   374  func (v *azureVolumeSource) getRole(id instance.Id) (*gwacl.PersistentVMRole, error) {
   375  	cloudServiceName, roleName := v.env.splitInstanceId(id)
   376  	if roleName == "" {
   377  		return nil, errors.NotSupportedf("attaching disks to legacy instances")
   378  	}
   379  	deploymentName := deploymentNameV2(cloudServiceName)
   380  	return v.env.api.GetRole(&gwacl.GetRoleRequest{
   381  		ServiceName:    cloudServiceName,
   382  		DeploymentName: deploymentName,
   383  		RoleName:       roleName,
   384  	})
   385  }
   386  
   387  func nextAvailableLUN(role *gwacl.PersistentVMRole) (int, error) {
   388  	// Pick the smallest LUN not in use. We have to choose them in order,
   389  	// or the disks don't show up.
   390  	var inUse [32]bool
   391  	if role.DataVirtualHardDisks != nil {
   392  		for _, disk := range *role.DataVirtualHardDisks {
   393  			if disk.LUN < 0 || disk.LUN > 31 {
   394  				logger.Warningf("ignore disk with invalid LUN: %+v", disk)
   395  				continue
   396  			}
   397  			inUse[disk.LUN] = true
   398  		}
   399  	}
   400  	for i, inUse := range inUse {
   401  		if !inUse {
   402  			return i, nil
   403  		}
   404  	}
   405  	return -1, errors.New("all LUNs are in use")
   406  }
   407  
   408  // diskBusAddress returns the value to use in the BusAddress field of
   409  // VolumeAttachmentInfo for a disk with the specified LUN.
   410  func diskBusAddress(lun int) string {
   411  	return fmt.Sprintf("scsi@5:0.0.%d", lun)
   412  }
   413  
   414  // mibToGib converts mebibytes to gibibytes.
   415  // AWS expects GiB, we work in MiB; round up
   416  // to nearest GiB.
   417  func mibToGib(m uint64) uint64 {
   418  	return (m + 1023) / 1024
   419  }
   420  
   421  // gibToMib converts gibibytes to mebibytes.
   422  func gibToMib(g uint64) uint64 {
   423  	return g * 1024
   424  }