github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/azure/storage.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 stdcontext "context" 8 "fmt" 9 "path" 10 "strings" 11 "sync" 12 13 "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-10-01/compute" 14 armstorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2018-07-01/storage" 15 azurestorage "github.com/Azure/azure-sdk-for-go/storage" 16 "github.com/Azure/go-autorest/autorest/to" 17 "github.com/juju/errors" 18 "github.com/juju/schema" 19 "gopkg.in/juju/names.v2" 20 21 "github.com/juju/juju/core/instance" 22 "github.com/juju/juju/environs/context" 23 "github.com/juju/juju/provider/azure/internal/armtemplates" 24 internalazurestorage "github.com/juju/juju/provider/azure/internal/azurestorage" 25 "github.com/juju/juju/provider/azure/internal/errorutils" 26 "github.com/juju/juju/storage" 27 ) 28 29 const ( 30 azureStorageProviderType = "azure" 31 32 accountTypeAttr = "account-type" 33 accountTypeStandardLRS = "Standard_LRS" 34 accountTypePremiumLRS = "Premium_LRS" 35 36 // volumeSizeMaxGiB is the maximum disk size (in gibibytes) for Azure disks. 37 // 38 // See: https://azure.microsoft.com/en-gb/documentation/articles/virtual-machines-disks-vhds/ 39 volumeSizeMaxGiB = 1023 40 41 // osDiskVHDContainer is the name of the blob container for VHDs 42 // backing OS disks. 43 osDiskVHDContainer = "osvhds" 44 45 // dataDiskVHDContainer is the name of the blob container for VHDs 46 // backing data disks. 47 dataDiskVHDContainer = "datavhds" 48 49 // vhdExtension is the filename extension we give to VHDs we create. 50 vhdExtension = ".vhd" 51 ) 52 53 // StorageProviderTypes implements storage.ProviderRegistry. 54 func (env *azureEnviron) StorageProviderTypes() ([]storage.ProviderType, error) { 55 return []storage.ProviderType{azureStorageProviderType}, nil 56 } 57 58 // StorageProvider implements storage.ProviderRegistry. 59 func (env *azureEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) { 60 if t == azureStorageProviderType { 61 return &azureStorageProvider{env}, nil 62 } 63 return nil, errors.NotFoundf("storage provider %q", t) 64 } 65 66 // azureStorageProvider is a storage provider for Azure disks. 67 type azureStorageProvider struct { 68 env *azureEnviron 69 } 70 71 var _ storage.Provider = (*azureStorageProvider)(nil) 72 73 var azureStorageConfigFields = schema.Fields{ 74 accountTypeAttr: schema.OneOf( 75 schema.Const(accountTypeStandardLRS), 76 schema.Const(accountTypePremiumLRS), 77 ), 78 } 79 80 var azureStorageConfigChecker = schema.FieldMap( 81 azureStorageConfigFields, 82 schema.Defaults{ 83 accountTypeAttr: accountTypeStandardLRS, 84 }, 85 ) 86 87 type azureStorageConfig struct { 88 storageType compute.DiskStorageAccountTypes 89 } 90 91 func newAzureStorageConfig(attrs map[string]interface{}) (*azureStorageConfig, error) { 92 coerced, err := azureStorageConfigChecker.Coerce(attrs, nil) 93 if err != nil { 94 return nil, errors.Annotate(err, "validating Azure storage config") 95 } 96 attrs = coerced.(map[string]interface{}) 97 azureStorageConfig := &azureStorageConfig{ 98 storageType: compute.DiskStorageAccountTypes(attrs[accountTypeAttr].(string)), 99 } 100 return azureStorageConfig, nil 101 } 102 103 // ValidateConfig is part of the Provider interface. 104 func (e *azureStorageProvider) ValidateConfig(cfg *storage.Config) error { 105 _, err := newAzureStorageConfig(cfg.Attrs()) 106 return errors.Trace(err) 107 } 108 109 // Supports is part of the Provider interface. 110 func (e *azureStorageProvider) Supports(k storage.StorageKind) bool { 111 return k == storage.StorageKindBlock 112 } 113 114 // Scope is part of the Provider interface. 115 func (e *azureStorageProvider) Scope() storage.Scope { 116 return storage.ScopeEnviron 117 } 118 119 // Dynamic is part of the Provider interface. 120 func (e *azureStorageProvider) Dynamic() bool { 121 return true 122 } 123 124 // Releasable is part of the Provider interface. 125 func (e *azureStorageProvider) Releasable() bool { 126 // NOTE(axw) Azure storage is currently tied to a model, and cannot 127 // be released or imported. To support releasing and importing, we'll 128 // need Azure to support moving managed disks between resource groups. 129 return false 130 } 131 132 // DefaultPools is part of the Provider interface. 133 func (e *azureStorageProvider) DefaultPools() []*storage.Config { 134 premiumPool, _ := storage.NewConfig("azure-premium", azureStorageProviderType, map[string]interface{}{ 135 accountTypeAttr: accountTypePremiumLRS, 136 }) 137 return []*storage.Config{premiumPool} 138 } 139 140 // VolumeSource is part of the Provider interface. 141 func (e *azureStorageProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) { 142 // Check to see if the environment has a storage account, 143 // which means it uses unmanaged disks. All models created 144 // before Juju 2.3 will have a storage account already, so 145 // it's safe to do the check up front. 146 maybeStorageClient, maybeStorageAccount, err := e.env.maybeGetStorageClient() 147 if err != nil { 148 return nil, errors.Trace(err) 149 } 150 return &azureVolumeSource{e.env, maybeStorageAccount, maybeStorageClient}, nil 151 } 152 153 // FilesystemSource is part of the Provider interface. 154 func (e *azureStorageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { 155 return nil, errors.NotSupportedf("filesystems") 156 } 157 158 type azureVolumeSource struct { 159 env *azureEnviron 160 maybeStorageAccount *armstorage.Account 161 maybeStorageClient internalazurestorage.Client 162 } 163 164 // CreateVolumes is specified on the storage.VolumeSource interface. 165 func (v *azureVolumeSource) CreateVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams) (_ []storage.CreateVolumesResult, err error) { 166 results := make([]storage.CreateVolumesResult, len(params)) 167 for i, p := range params { 168 if err := v.ValidateVolumeParams(p); err != nil { 169 results[i].Error = err 170 continue 171 } 172 } 173 if v.maybeStorageClient == nil { 174 v.createManagedDiskVolumes(ctx, params, results) 175 return results, nil 176 } 177 return results, v.createUnmanagedDiskVolumes(ctx, params, results) 178 } 179 180 // createManagedDiskVolumes creates volumes with associated managed disks. 181 func (v *azureVolumeSource) createManagedDiskVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams, results []storage.CreateVolumesResult) { 182 for i, p := range params { 183 if results[i].Error != nil { 184 continue 185 } 186 volume, err := v.createManagedDiskVolume(ctx, p) 187 if err != nil { 188 results[i].Error = err 189 continue 190 } 191 results[i].Volume = volume 192 } 193 } 194 195 // createManagedDiskVolume creates a managed disk. 196 func (v *azureVolumeSource) createManagedDiskVolume(ctx context.ProviderCallContext, p storage.VolumeParams) (*storage.Volume, error) { 197 cfg, err := newAzureStorageConfig(p.Attributes) 198 if err != nil { 199 return nil, errors.Trace(err) 200 } 201 202 diskTags := make(map[string]*string) 203 for k, v := range p.ResourceTags { 204 diskTags[k] = to.StringPtr(v) 205 } 206 diskName := p.Tag.String() 207 sizeInGib := mibToGib(p.Size) 208 diskModel := compute.Disk{ 209 Name: to.StringPtr(diskName), 210 Location: to.StringPtr(v.env.location), 211 Tags: diskTags, 212 Sku: &compute.DiskSku{ 213 Name: cfg.storageType, 214 }, 215 DiskProperties: &compute.DiskProperties{ 216 CreationData: &compute.CreationData{CreateOption: compute.Empty}, 217 DiskSizeGB: to.Int32Ptr(int32(sizeInGib)), 218 }, 219 } 220 221 diskClient := compute.DisksClient{v.env.disk} 222 sdkCtx := stdcontext.Background() 223 future, err := diskClient.CreateOrUpdate(sdkCtx, v.env.resourceGroup, diskName, diskModel) 224 if err != nil { 225 return nil, errorutils.HandleCredentialError(errors.Annotatef(err, "creating disk for volume %q", p.Tag.Id()), ctx) 226 } 227 err = future.WaitForCompletionRef(sdkCtx, diskClient.Client) 228 if err != nil { 229 return nil, errorutils.HandleCredentialError(errors.Annotatef(err, "creating disk for volume %q", p.Tag.Id()), ctx) 230 } 231 result, err := future.Result(diskClient) 232 if err != nil && !isNotFoundResult(result.Response) { 233 return nil, errors.Annotatef(err, "creating disk for volume %q", p.Tag.Id()) 234 } 235 236 volume := storage.Volume{ 237 p.Tag, 238 storage.VolumeInfo{ 239 VolumeId: diskName, 240 Size: gibToMib(uint64(to.Int32(result.DiskSizeGB))), 241 Persistent: true, 242 }, 243 } 244 return &volume, nil 245 } 246 247 // createUnmanagedDiskVolumes creates volumes with associated unmanaged disks (blobs). 248 func (v *azureVolumeSource) createUnmanagedDiskVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams, results []storage.CreateVolumesResult) error { 249 var instanceIds []instance.Id 250 for i, p := range params { 251 if results[i].Error != nil { 252 continue 253 } 254 instanceIds = append(instanceIds, p.Attachment.InstanceId) 255 } 256 if len(instanceIds) == 0 { 257 return nil 258 } 259 virtualMachines, err := v.virtualMachines(ctx, instanceIds) 260 if err != nil { 261 return errors.Annotate(err, "getting virtual machines") 262 } 263 // Update VirtualMachine objects in-memory, 264 // and then perform the updates all at once. 265 for i, p := range params { 266 if results[i].Error != nil { 267 continue 268 } 269 vm, ok := virtualMachines[p.Attachment.InstanceId] 270 if !ok { 271 continue 272 } 273 if vm.err != nil { 274 results[i].Error = vm.err 275 continue 276 } 277 volume, volumeAttachment, err := v.createUnmanagedDiskVolume(vm.vm, p) 278 if err != nil { 279 results[i].Error = err 280 vm.err = err 281 continue 282 } 283 results[i].Volume = volume 284 results[i].VolumeAttachment = volumeAttachment 285 } 286 287 updateResults, err := v.updateVirtualMachines(ctx, virtualMachines, instanceIds) 288 if err != nil { 289 return errors.Annotate(err, "updating virtual machines") 290 } 291 for i, err := range updateResults { 292 if results[i].Error != nil || err == nil { 293 continue 294 } 295 results[i].Error = err 296 results[i].Volume = nil 297 results[i].VolumeAttachment = nil 298 } 299 return nil 300 } 301 302 // createUnmanagedDiskVolume updates the provided VirtualMachine's 303 // StorageProfile with the parameters for creating a new unmanaged 304 // data disk. We don't actually interact with the Azure API until 305 // after all changes to the VirtualMachine are made. 306 func (v *azureVolumeSource) createUnmanagedDiskVolume( 307 vm *compute.VirtualMachine, 308 p storage.VolumeParams, 309 ) (*storage.Volume, *storage.VolumeAttachment, error) { 310 311 diskName := p.Tag.String() 312 sizeInGib := mibToGib(p.Size) 313 volumeAttachment, err := v.addDataDisk( 314 vm, 315 diskName, 316 p.Tag, 317 p.Attachment.Machine, 318 compute.DiskCreateOptionTypesEmpty, 319 to.Int32Ptr(int32(sizeInGib)), 320 ) 321 if err != nil { 322 return nil, nil, errors.Trace(err) 323 } 324 // Data disks associate VHDs to machines. In Juju's storage model, 325 // the VHD is the volume and the disk is the volume attachment. 326 volume := storage.Volume{ 327 p.Tag, 328 storage.VolumeInfo{ 329 VolumeId: diskName, 330 Size: gibToMib(sizeInGib), 331 Persistent: true, 332 }, 333 } 334 return &volume, volumeAttachment, nil 335 } 336 337 // ListVolumes is specified on the storage.VolumeSource interface. 338 func (v *azureVolumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) { 339 if v.maybeStorageClient == nil { 340 return v.listManagedDiskVolumes(ctx) 341 } 342 return v.listUnmanagedDiskVolumes(ctx) 343 } 344 345 func (v *azureVolumeSource) listManagedDiskVolumes(ctx context.ProviderCallContext) ([]string, error) { 346 var volumeIds []string 347 diskClient := compute.DisksClient{v.env.disk} 348 sdkCtx := stdcontext.Background() 349 list, err := diskClient.ListComplete(sdkCtx) 350 if err != nil { 351 return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing disks"), ctx) 352 } 353 for ; list.NotDone(); err = list.NextWithContext(sdkCtx) { 354 if err != nil { 355 return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing disks"), ctx) 356 } 357 diskName := to.String(list.Value().Name) 358 if _, err := names.ParseVolumeTag(diskName); err != nil { 359 continue 360 } 361 volumeIds = append(volumeIds, diskName) 362 } 363 return volumeIds, nil 364 } 365 366 func (v *azureVolumeSource) listUnmanagedDiskVolumes(ctx context.ProviderCallContext) ([]string, error) { 367 blobs, err := v.listBlobs(ctx) 368 if err != nil { 369 return nil, errors.Annotate(err, "listing volumes") 370 } 371 volumeIds := make([]string, 0, len(blobs)) 372 for _, blob := range blobs { 373 volumeId, ok := blobVolumeId(blob) 374 if !ok { 375 continue 376 } 377 volumeIds = append(volumeIds, volumeId) 378 } 379 return volumeIds, nil 380 } 381 382 // listBlobs returns a list of blobs in the data-disk container. 383 func (v *azureVolumeSource) listBlobs(ctx context.ProviderCallContext) ([]internalazurestorage.Blob, error) { 384 blobsClient := v.maybeStorageClient.GetBlobService() 385 vhdContainer := blobsClient.GetContainerReference(dataDiskVHDContainer) 386 // TODO(axw) consider taking a set of IDs and computing the 387 // longest common prefix to pass in the parameters 388 blobs, err := vhdContainer.Blobs() 389 if err != nil { 390 errorutils.HandleCredentialError(err, ctx) 391 if err, ok := err.(azurestorage.AzureStorageServiceError); ok { 392 switch err.Code { 393 case "ContainerNotFound": 394 return nil, nil 395 } 396 } 397 return nil, errors.Annotate(err, "listing blobs") 398 } 399 return blobs, nil 400 } 401 402 // DescribeVolumes is specified on the storage.VolumeSource interface. 403 func (v *azureVolumeSource) DescribeVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) { 404 if v.maybeStorageClient == nil { 405 return v.describeManagedDiskVolumes(ctx, volumeIds) 406 } 407 return v.describeUnmanagedDiskVolumes(ctx, volumeIds) 408 } 409 410 func (v *azureVolumeSource) describeManagedDiskVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) { 411 diskClient := compute.DisksClient{v.env.disk} 412 results := make([]storage.DescribeVolumesResult, len(volumeIds)) 413 var wg sync.WaitGroup 414 sdkCtx := stdcontext.Background() 415 for i, volumeId := range volumeIds { 416 wg.Add(1) 417 go func(i int, volumeId string) { 418 defer wg.Done() 419 disk, err := diskClient.Get(sdkCtx, v.env.resourceGroup, volumeId) 420 if err != nil { 421 if isNotFoundResult(disk.Response) { 422 err = errors.NotFoundf("disk %s", volumeId) 423 } 424 results[i].Error = errorutils.HandleCredentialError(err, ctx) 425 return 426 } 427 results[i].VolumeInfo = &storage.VolumeInfo{ 428 VolumeId: volumeId, 429 Size: gibToMib(uint64(to.Int32(disk.DiskSizeGB))), 430 Persistent: true, 431 } 432 }(i, volumeId) 433 } 434 wg.Wait() 435 return results, nil 436 } 437 438 func (v *azureVolumeSource) describeUnmanagedDiskVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) { 439 blobs, err := v.listBlobs(ctx) 440 if err != nil { 441 return nil, errors.Annotate(err, "listing volumes") 442 } 443 444 byVolumeId := make(map[string]internalazurestorage.Blob) 445 for _, blob := range blobs { 446 volumeId, ok := blobVolumeId(blob) 447 if !ok { 448 continue 449 } 450 byVolumeId[volumeId] = blob 451 } 452 453 results := make([]storage.DescribeVolumesResult, len(volumeIds)) 454 for i, volumeId := range volumeIds { 455 blob, ok := byVolumeId[volumeId] 456 if !ok { 457 results[i].Error = errors.NotFoundf("%s", volumeId) 458 continue 459 } 460 sizeInMib := blob.Properties().ContentLength / (1024 * 1024) 461 results[i].VolumeInfo = &storage.VolumeInfo{ 462 VolumeId: volumeId, 463 Size: uint64(sizeInMib), 464 Persistent: true, 465 } 466 } 467 468 return results, nil 469 } 470 471 // DestroyVolumes is specified on the storage.VolumeSource interface. 472 func (v *azureVolumeSource) DestroyVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 473 if v.maybeStorageClient == nil { 474 return v.destroyManagedDiskVolumes(ctx, volumeIds) 475 } 476 return v.destroyUnmanagedDiskVolumes(ctx, volumeIds) 477 } 478 479 func (v *azureVolumeSource) destroyManagedDiskVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 480 diskClient := compute.DisksClient{v.env.disk} 481 sdkCtx := stdcontext.Background() 482 return foreachVolume(volumeIds, func(volumeId string) error { 483 future, err := diskClient.Delete(sdkCtx, v.env.resourceGroup, volumeId) 484 if err != nil { 485 if !isNotFoundResponse(future.Response()) { 486 return errorutils.HandleCredentialError(errors.Annotatef(err, "deleting disk %q", volumeId), ctx) 487 } 488 return nil 489 } 490 err = future.WaitForCompletionRef(sdkCtx, diskClient.Client) 491 if err != nil { 492 return errors.Annotatef(err, "deleting disk %q", volumeId) 493 } 494 result, err := future.Result(diskClient) 495 if err != nil && !isNotFoundResult(result) { 496 return errors.Annotatef(err, "deleting disk %q", volumeId) 497 } 498 return nil 499 }), nil 500 } 501 502 func (v *azureVolumeSource) destroyUnmanagedDiskVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 503 blobsClient := v.maybeStorageClient.GetBlobService() 504 vhdContainer := blobsClient.GetContainerReference(dataDiskVHDContainer) 505 return foreachVolume(volumeIds, func(volumeId string) error { 506 vhdBlob := vhdContainer.Blob(volumeId + vhdExtension) 507 _, err := vhdBlob.DeleteIfExists(nil) 508 return errorutils.HandleCredentialError(errors.Annotatef(err, "deleting blob %q", vhdBlob.Name()), ctx) 509 }), nil 510 } 511 512 func foreachVolume(volumeIds []string, f func(string) error) []error { 513 results := make([]error, len(volumeIds)) 514 var wg sync.WaitGroup 515 for i, volumeId := range volumeIds { 516 wg.Add(1) 517 go func(i int, volumeId string) { 518 defer wg.Done() 519 results[i] = f(volumeId) 520 }(i, volumeId) 521 } 522 wg.Wait() 523 return results 524 } 525 526 // ReleaseVolumes is specified on the storage.VolumeSource interface. 527 func (v *azureVolumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 528 // Releasing volumes is not supported, see azureStorageProvider.Releasable. 529 // 530 // When managed disks can be moved between resource groups, we may want to 531 // support releasing unmanaged disks. We'll need to create a managed disk 532 // from the blob, and then release that. 533 return nil, errors.NotSupportedf("ReleaseVolumes") 534 } 535 536 // ValidateVolumeParams is specified on the storage.VolumeSource interface. 537 func (v *azureVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error { 538 if mibToGib(params.Size) > volumeSizeMaxGiB { 539 return errors.Errorf( 540 "%d GiB exceeds the maximum of %d GiB", 541 mibToGib(params.Size), 542 volumeSizeMaxGiB, 543 ) 544 } 545 return nil 546 } 547 548 // AttachVolumes is specified on the storage.VolumeSource interface. 549 func (v *azureVolumeSource) AttachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) { 550 results := make([]storage.AttachVolumesResult, len(attachParams)) 551 instanceIds := make([]instance.Id, len(attachParams)) 552 for i, p := range attachParams { 553 instanceIds[i] = p.InstanceId 554 } 555 if len(instanceIds) == 0 { 556 return results, nil 557 } 558 virtualMachines, err := v.virtualMachines(ctx, instanceIds) 559 if err != nil { 560 return nil, errors.Annotate(err, "getting virtual machines") 561 } 562 563 // Update VirtualMachine objects in-memory, 564 // and then perform the updates all at once. 565 // 566 // An attachment does not require an update 567 // if it is pre-existing, so we keep a record 568 // of which VMs need updating. 569 changed := make(map[instance.Id]bool, len(virtualMachines)) 570 for i, p := range attachParams { 571 vm, ok := virtualMachines[p.InstanceId] 572 if !ok { 573 continue 574 } 575 if vm.err != nil { 576 results[i].Error = vm.err 577 continue 578 } 579 volumeAttachment, updated, err := v.attachVolume(vm.vm, p) 580 if err != nil { 581 results[i].Error = err 582 vm.err = err 583 continue 584 } 585 results[i].VolumeAttachment = volumeAttachment 586 if updated { 587 changed[p.InstanceId] = true 588 } 589 } 590 for _, instanceId := range instanceIds { 591 if !changed[instanceId] { 592 delete(virtualMachines, instanceId) 593 } 594 } 595 596 updateResults, err := v.updateVirtualMachines(ctx, virtualMachines, instanceIds) 597 if err != nil { 598 return nil, errors.Annotate(err, "updating virtual machines") 599 } 600 for i, err := range updateResults { 601 if results[i].Error != nil || err == nil { 602 continue 603 } 604 results[i].Error = err 605 results[i].VolumeAttachment = nil 606 } 607 return results, nil 608 } 609 610 func (v *azureVolumeSource) attachVolume( 611 vm *compute.VirtualMachine, 612 p storage.VolumeAttachmentParams, 613 ) (_ *storage.VolumeAttachment, updated bool, _ error) { 614 615 var dataDisks []compute.DataDisk 616 if vm.StorageProfile.DataDisks != nil { 617 dataDisks = *vm.StorageProfile.DataDisks 618 } 619 620 diskName := p.VolumeId 621 for _, disk := range dataDisks { 622 if to.String(disk.Name) != diskName { 623 continue 624 } 625 // Disk is already attached. 626 volumeAttachment := &storage.VolumeAttachment{ 627 p.Volume, 628 p.Machine, 629 storage.VolumeAttachmentInfo{ 630 BusAddress: diskBusAddress(to.Int32(disk.Lun)), 631 }, 632 } 633 return volumeAttachment, false, nil 634 } 635 636 volumeAttachment, err := v.addDataDisk(vm, diskName, p.Volume, p.Machine, compute.DiskCreateOptionTypesAttach, nil) 637 if err != nil { 638 return nil, false, errors.Trace(err) 639 } 640 return volumeAttachment, true, nil 641 } 642 643 func (v *azureVolumeSource) addDataDisk( 644 vm *compute.VirtualMachine, 645 diskName string, 646 volumeTag names.VolumeTag, 647 machineTag names.Tag, 648 createOption compute.DiskCreateOptionTypes, 649 diskSizeGB *int32, 650 ) (*storage.VolumeAttachment, error) { 651 652 lun, err := nextAvailableLUN(vm) 653 if err != nil { 654 return nil, errors.Annotate(err, "choosing LUN") 655 } 656 657 dataDisk := compute.DataDisk{ 658 Lun: to.Int32Ptr(lun), 659 Name: to.StringPtr(diskName), 660 Caching: compute.CachingTypesReadWrite, 661 CreateOption: createOption, 662 DiskSizeGB: diskSizeGB, 663 } 664 if v.maybeStorageAccount == nil { 665 // This model uses managed disks. 666 diskResourceID := v.diskResourceID(diskName) 667 dataDisk.ManagedDisk = &compute.ManagedDiskParameters{ 668 ID: to.StringPtr(diskResourceID), 669 } 670 } else { 671 // This model uses unmanaged disks. 672 dataDisksRoot := dataDiskVhdRoot(v.maybeStorageAccount) 673 vhdURI := dataDisksRoot + diskName + vhdExtension 674 dataDisk.Vhd = &compute.VirtualHardDisk{to.StringPtr(vhdURI)} 675 } 676 677 var dataDisks []compute.DataDisk 678 if vm.StorageProfile.DataDisks != nil { 679 dataDisks = *vm.StorageProfile.DataDisks 680 } 681 dataDisks = append(dataDisks, dataDisk) 682 vm.StorageProfile.DataDisks = &dataDisks 683 684 return &storage.VolumeAttachment{ 685 volumeTag, 686 machineTag, 687 storage.VolumeAttachmentInfo{ 688 BusAddress: diskBusAddress(lun), 689 }, 690 }, nil 691 } 692 693 // DetachVolumes is specified on the storage.VolumeSource interface. 694 func (v *azureVolumeSource) DetachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]error, error) { 695 results := make([]error, len(attachParams)) 696 instanceIds := make([]instance.Id, len(attachParams)) 697 for i, p := range attachParams { 698 instanceIds[i] = p.InstanceId 699 } 700 if len(instanceIds) == 0 { 701 return results, nil 702 } 703 virtualMachines, err := v.virtualMachines(ctx, instanceIds) 704 if err != nil { 705 return nil, errors.Annotate(err, "getting virtual machines") 706 } 707 708 // Update VirtualMachine objects in-memory, 709 // and then perform the updates all at once. 710 // 711 // An detachment does not require an update 712 // if the disk isn't attached, so we keep a 713 // record of which VMs need updating. 714 changed := make(map[instance.Id]bool, len(virtualMachines)) 715 for i, p := range attachParams { 716 vm, ok := virtualMachines[p.InstanceId] 717 if !ok { 718 continue 719 } 720 if vm.err != nil { 721 results[i] = vm.err 722 continue 723 } 724 if v.detachVolume(vm.vm, p) { 725 changed[p.InstanceId] = true 726 } 727 } 728 for _, instanceId := range instanceIds { 729 if !changed[instanceId] { 730 delete(virtualMachines, instanceId) 731 } 732 } 733 734 updateResults, err := v.updateVirtualMachines(ctx, virtualMachines, instanceIds) 735 if err != nil { 736 return nil, errors.Annotate(err, "updating virtual machines") 737 } 738 for i, err := range updateResults { 739 if results[i] != nil || err == nil { 740 continue 741 } 742 results[i] = err 743 } 744 return results, nil 745 } 746 747 func (v *azureVolumeSource) detachVolume( 748 vm *compute.VirtualMachine, 749 p storage.VolumeAttachmentParams, 750 ) (updated bool) { 751 752 var dataDisks []compute.DataDisk 753 if vm.StorageProfile.DataDisks != nil { 754 dataDisks = *vm.StorageProfile.DataDisks 755 } 756 for i, disk := range dataDisks { 757 if to.String(disk.Name) != p.VolumeId { 758 continue 759 } 760 dataDisks = append(dataDisks[:i], dataDisks[i+1:]...) 761 vm.StorageProfile.DataDisks = &dataDisks 762 return true 763 } 764 return false 765 } 766 767 // diskResourceID returns the full resource ID for a disk, given its name. 768 func (v *azureVolumeSource) diskResourceID(name string) string { 769 return path.Join( 770 "/subscriptions", 771 v.env.subscriptionId, 772 "resourceGroups", 773 v.env.resourceGroup, 774 "providers", 775 "Microsoft.Compute", 776 "disks", 777 name, 778 ) 779 } 780 781 type maybeVirtualMachine struct { 782 vm *compute.VirtualMachine 783 err error 784 } 785 786 // virtualMachines returns a mapping of instance IDs to VirtualMachines and 787 // errors, for each of the specified instance IDs. 788 func (v *azureVolumeSource) virtualMachines(ctx context.ProviderCallContext, instanceIds []instance.Id) (map[instance.Id]*maybeVirtualMachine, error) { 789 vmsClient := compute.VirtualMachinesClient{v.env.compute} 790 sdkCtx := stdcontext.Background() 791 result, err := vmsClient.ListComplete(sdkCtx, v.env.resourceGroup) 792 if err != nil { 793 return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing virtual machines"), ctx) 794 } 795 796 all := make(map[instance.Id]*compute.VirtualMachine) 797 for ; result.NotDone(); err = result.NextWithContext(sdkCtx) { 798 if err != nil { 799 return nil, errors.Annotate(err, "listing disks") 800 } 801 vmCopy := result.Value() 802 all[instance.Id(to.String(vmCopy.Name))] = &vmCopy 803 } 804 results := make(map[instance.Id]*maybeVirtualMachine) 805 for _, id := range instanceIds { 806 result := &maybeVirtualMachine{vm: all[id]} 807 if result.vm == nil { 808 result.err = errors.NotFoundf("instance %v", id) 809 } 810 results[id] = result 811 } 812 return results, nil 813 } 814 815 // updateVirtualMachines updates virtual machines in the given map by iterating 816 // through the list of instance IDs in order, and updating each corresponding 817 // virtual machine at most once. 818 func (v *azureVolumeSource) updateVirtualMachines( 819 ctx context.ProviderCallContext, 820 virtualMachines map[instance.Id]*maybeVirtualMachine, instanceIds []instance.Id, 821 ) ([]error, error) { 822 results := make([]error, len(instanceIds)) 823 vmsClient := compute.VirtualMachinesClient{v.env.compute} 824 for i, instanceId := range instanceIds { 825 vm, ok := virtualMachines[instanceId] 826 if !ok { 827 continue 828 } 829 if vm.err != nil { 830 results[i] = vm.err 831 continue 832 } 833 sdkCtx := stdcontext.Background() 834 future, err := vmsClient.CreateOrUpdate( 835 sdkCtx, 836 v.env.resourceGroup, to.String(vm.vm.Name), *vm.vm, 837 ) 838 if err != nil { 839 if errorutils.MaybeInvalidateCredential(err, ctx) { 840 return nil, errors.Trace(err) 841 } 842 results[i] = err 843 vm.err = err 844 continue 845 } 846 err = future.WaitForCompletionRef(sdkCtx, vmsClient.Client) 847 if err != nil { 848 results[i] = err 849 vm.err = err 850 continue 851 } 852 _, err = future.Result(vmsClient) 853 if err != nil { 854 results[i] = err 855 vm.err = err 856 continue 857 } 858 // successfully updated, don't update again 859 delete(virtualMachines, instanceId) 860 } 861 return results, nil 862 } 863 864 func nextAvailableLUN(vm *compute.VirtualMachine) (int32, error) { 865 // Pick the smallest LUN not in use. We have to choose them in order, 866 // or the disks don't show up. 867 var inUse [32]bool 868 if vm.StorageProfile.DataDisks != nil { 869 for _, disk := range *vm.StorageProfile.DataDisks { 870 lun := to.Int32(disk.Lun) 871 if lun < 0 || lun > 31 { 872 logger.Debugf("ignore disk with invalid LUN: %+v", disk) 873 continue 874 } 875 inUse[lun] = true 876 } 877 } 878 for i, inUse := range inUse { 879 if !inUse { 880 return int32(i), nil 881 } 882 } 883 return -1, errors.New("all LUNs are in use") 884 } 885 886 // diskBusAddress returns the value to use in the BusAddress field of 887 // VolumeAttachmentInfo for a disk with the specified LUN. 888 func diskBusAddress(lun int32) string { 889 return fmt.Sprintf("scsi@5:0.0.%d", lun) 890 } 891 892 // mibToGib converts mebibytes to gibibytes. 893 // AWS expects GiB, we work in MiB; round up 894 // to nearest GiB. 895 func mibToGib(m uint64) uint64 { 896 return (m + 1023) / 1024 897 } 898 899 // gibToMib converts gibibytes to mebibytes. 900 func gibToMib(g uint64) uint64 { 901 return g * 1024 902 } 903 904 // dataDiskVhdRoot returns the URL to the blob container in which we store the 905 // VHDs for data disks for the environment. 906 func dataDiskVhdRoot(storageAccount *armstorage.Account) string { 907 return blobContainerURL(storageAccount, dataDiskVHDContainer) 908 } 909 910 // blobContainer returns the URL to the named blob container. 911 func blobContainerURL(storageAccount *armstorage.Account, container string) string { 912 return fmt.Sprintf( 913 "%s%s/", 914 to.String(storageAccount.PrimaryEndpoints.Blob), 915 container, 916 ) 917 } 918 919 // blobVolumeId returns the volume ID for a blob, and a boolean reporting 920 // whether or not the blob's name matches the scheme we use. 921 func blobVolumeId(blob internalazurestorage.Blob) (string, bool) { 922 blobName := blob.Name() 923 if !strings.HasSuffix(blobName, vhdExtension) { 924 return "", false 925 } 926 volumeId := blobName[:len(blobName)-len(vhdExtension)] 927 if _, err := names.ParseVolumeTag(volumeId); err != nil { 928 return "", false 929 } 930 return volumeId, true 931 } 932 933 // getStorageClient returns a new storage client, given an environ config 934 // and a constructor. 935 func getStorageClient( 936 newClient internalazurestorage.NewClientFunc, 937 storageEndpoint string, 938 storageAccount *armstorage.Account, 939 storageAccountKey *armstorage.AccountKey, 940 ) (internalazurestorage.Client, error) { 941 storageAccountName := to.String(storageAccount.Name) 942 const useHTTPS = true 943 return newClient( 944 storageAccountName, 945 to.String(storageAccountKey.Value), 946 storageEndpoint, 947 azurestorage.DefaultAPIVersion, 948 useHTTPS, 949 ) 950 } 951 952 // getStorageAccountKey returns the key for the storage account. 953 func getStorageAccountKey( 954 client armstorage.AccountsClient, 955 resourceGroup, accountName string, 956 ) (*armstorage.AccountKey, error) { 957 logger.Debugf("getting keys for storage account %q", accountName) 958 sdkCtx := stdcontext.Background() 959 listKeysResult, err := client.ListKeys(sdkCtx, resourceGroup, accountName) 960 if err != nil { 961 if isNotFoundResult(listKeysResult.Response) { 962 return nil, errors.NewNotFound(err, "storage account keys not found") 963 } 964 return nil, errors.Annotate(err, "listing storage account keys") 965 } 966 if listKeysResult.Keys == nil { 967 return nil, errors.NotFoundf("storage account keys") 968 } 969 970 // We need a storage key with full permissions. 971 var fullKey *armstorage.AccountKey 972 for _, key := range *listKeysResult.Keys { 973 logger.Debugf("storage account key: %#v", key) 974 // At least some of the time, Azure returns the permissions 975 // in title-case, which does not match the constant. 976 if strings.ToUpper(string(key.Permissions)) != strings.ToUpper(string(armstorage.Full)) { 977 continue 978 } 979 fullKey = &key 980 break 981 } 982 if fullKey == nil { 983 return nil, errors.NotFoundf( 984 "storage account key with %q permission", 985 armstorage.Full, 986 ) 987 } 988 return fullKey, nil 989 } 990 991 // storageAccountTemplateResource returns a template resource definition 992 // for creating a storage account. 993 func storageAccountTemplateResource( 994 location string, 995 envTags map[string]string, 996 accountName, accountType string, 997 ) armtemplates.Resource { 998 return armtemplates.Resource{ 999 APIVersion: storageAPIVersion, 1000 Type: "Microsoft.Storage/storageAccounts", 1001 Name: accountName, 1002 Location: location, 1003 Tags: envTags, 1004 StorageSku: &armstorage.Sku{ 1005 Name: armstorage.SkuName(accountType), 1006 }, 1007 } 1008 }