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 }