github.com/openshift/installer@v1.4.17/pkg/infrastructure/azure/storage.go (about) 1 package azure 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/Azure/azure-sdk-for-go/sdk/azcore" 12 "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" 13 "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" 14 "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" 15 "github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming" 16 "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" 17 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" 18 "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage" 19 "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob" 20 "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob" 21 "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob" 22 "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/pageblob" 23 "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas" 24 "github.com/sirupsen/logrus" 25 26 aztypes "github.com/openshift/installer/pkg/types/azure" 27 ) 28 29 var ( 30 vaultsClient *armkeyvault.VaultsClient 31 keysClient *armkeyvault.KeysClient 32 ) 33 34 // CreateStorageAccountInput contains the input parameters for creating a 35 // storage account. 36 type CreateStorageAccountInput struct { 37 SubscriptionID string 38 ResourceGroupName string 39 StorageAccountName string 40 Region string 41 Tags map[string]*string 42 CustomerManagedKey *aztypes.CustomerManagedKey 43 CloudName aztypes.CloudEnvironment 44 TokenCredential azcore.TokenCredential 45 CloudConfiguration cloud.Configuration 46 } 47 48 // CreateStorageAccountOutput contains the return values after creating a 49 // storage account. 50 type CreateStorageAccountOutput struct { 51 StorageAccount *armstorage.Account 52 StorageAccountsClient *armstorage.AccountsClient 53 StorageClientFactory *armstorage.ClientFactory 54 StorageAccountKeys []armstorage.AccountKey 55 } 56 57 // CreateStorageAccount creates a new storage account. 58 func CreateStorageAccount(ctx context.Context, in *CreateStorageAccountInput) (*CreateStorageAccountOutput, error) { 59 minimumTLSVersion := armstorage.MinimumTLSVersionTLS10 60 cloudConfiguration := in.CloudConfiguration 61 62 /* XXX: Do we support other clouds? */ 63 switch in.CloudName { 64 case aztypes.PublicCloud: 65 minimumTLSVersion = armstorage.MinimumTLSVersionTLS12 66 case aztypes.USGovernmentCloud: 67 minimumTLSVersion = armstorage.MinimumTLSVersionTLS12 68 } 69 70 storageClientFactory, err := armstorage.NewClientFactory( 71 in.SubscriptionID, 72 in.TokenCredential, 73 &arm.ClientOptions{ 74 ClientOptions: policy.ClientOptions{ 75 Cloud: cloudConfiguration, 76 //Transport: ..., 77 }, 78 }, 79 ) 80 if err != nil { 81 return nil, fmt.Errorf("failed to get storage account factory %w", err) 82 } 83 84 sku := armstorage.SKU{ 85 Name: to.Ptr(armstorage.SKUNameStandardLRS), 86 } 87 accountCreateParameters := armstorage.AccountCreateParameters{ 88 Identity: nil, 89 Kind: to.Ptr(armstorage.KindStorageV2), 90 Location: to.Ptr(in.Region), 91 SKU: &sku, 92 Properties: &armstorage.AccountPropertiesCreateParameters{ 93 AllowBlobPublicAccess: to.Ptr(true), 94 AllowSharedKeyAccess: to.Ptr(true), 95 IsLocalUserEnabled: to.Ptr(true), 96 LargeFileSharesState: to.Ptr(armstorage.LargeFileSharesStateEnabled), 97 PublicNetworkAccess: to.Ptr(armstorage.PublicNetworkAccessEnabled), 98 MinimumTLSVersion: &minimumTLSVersion, 99 }, 100 Tags: in.Tags, 101 } 102 103 if in.CustomerManagedKey != nil && in.CustomerManagedKey.KeyVault.Name != "" { 104 // When encryption is enabled, Ignition is is stored as a page blob 105 // (and not a block blob). To support this case, `Kind` can continue to be 106 // `StorageV2` and yhe `SKU` needs to be `Premium_LRS`. 107 //https://learn.microsoft.com/en-us/azure/storage/common/storage-account-create?tabs=azure-portal 108 sku = armstorage.SKU{ 109 Name: to.Ptr(armstorage.SKUNamePremiumLRS), 110 } 111 identity := armstorage.Identity{ 112 Type: to.Ptr(armstorage.IdentityTypeUserAssigned), 113 UserAssignedIdentities: map[string]*armstorage.UserAssignedIdentity{ 114 fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ManagedIdentity/userAssignedIdentities/%s", 115 in.SubscriptionID, 116 in.CustomerManagedKey.KeyVault.ResourceGroup, 117 in.CustomerManagedKey.UserAssignedIdentityKey, 118 ): {}, 119 }, 120 } 121 logrus.Debugf("Generating Encrytption for Storage Account using Customer Managed Key") 122 encryption, err := GenerateStorageAccountEncryption( 123 ctx, 124 &CustomerManagedKeyInput{ 125 SubscriptionID: in.SubscriptionID, 126 ResourceGroupName: in.ResourceGroupName, 127 CustomerManagedKey: in.CustomerManagedKey, 128 TokenCredential: in.TokenCredential, 129 }, 130 ) 131 if err != nil { 132 return nil, fmt.Errorf("error generating encryption information for provided customer managed key: %w", err) 133 } 134 accountCreateParameters.Identity = &identity 135 accountCreateParameters.SKU = &sku 136 accountCreateParameters.Properties.Encryption = encryption 137 } 138 139 logrus.Debugf("Creating storage account") 140 accountsClient := storageClientFactory.NewAccountsClient() 141 pollerResponse, err := accountsClient.BeginCreate( 142 ctx, 143 in.ResourceGroupName, 144 in.StorageAccountName, 145 accountCreateParameters, 146 nil, 147 ) 148 if err != nil { 149 return nil, fmt.Errorf("error creating storage account %s: %w", in.StorageAccountName, err) 150 } 151 152 pollDoneResponse, err := pollerResponse.PollUntilDone(ctx, nil) 153 if err != nil { 154 return nil, fmt.Errorf("error waiting for creation of storage account %s: %w", in.StorageAccountName, err) 155 } 156 157 logrus.Debugf("Getting storage keys") 158 listKeysResponse, err := accountsClient.ListKeys(ctx, in.ResourceGroupName, in.StorageAccountName, nil) 159 if err != nil { 160 return nil, fmt.Errorf("failed to retrieve storage account keys for %s: %w", in.StorageAccountName, err) 161 } 162 163 out := &CreateStorageAccountOutput{ 164 StorageAccount: to.Ptr(pollDoneResponse.Account), 165 StorageAccountsClient: accountsClient, 166 StorageClientFactory: storageClientFactory, 167 } 168 169 for _, key := range listKeysResponse.Keys { 170 out.StorageAccountKeys = append(out.StorageAccountKeys, *key) 171 } 172 173 return out, nil 174 } 175 176 // CreateBlobContainerInput contains the input parameters used for creating a 177 // blob storage container. 178 type CreateBlobContainerInput struct { 179 SubscriptionID string 180 ResourceGroupName string 181 StorageAccountName string 182 ContainerName string 183 PublicAccess *armstorage.PublicAccess 184 StorageClientFactory *armstorage.ClientFactory 185 } 186 187 // CreateBlobContainerOutput contains the return values after creating a blob 188 // storage container. 189 type CreateBlobContainerOutput struct { 190 BlobContainer *armstorage.BlobContainer 191 } 192 193 // CreateBlobContainer creates a blob container in a storage account. 194 func CreateBlobContainer(ctx context.Context, in *CreateBlobContainerInput) (*CreateBlobContainerOutput, error) { 195 blobContainersClient := in.StorageClientFactory.NewBlobContainersClient() 196 197 logrus.Debugf("Creating blob container") 198 blobContainerResponse, err := blobContainersClient.Create( 199 ctx, 200 in.ResourceGroupName, 201 in.StorageAccountName, 202 in.ContainerName, 203 armstorage.BlobContainer{ 204 ContainerProperties: &armstorage.ContainerProperties{ 205 PublicAccess: in.PublicAccess, 206 }, 207 }, 208 nil, 209 ) 210 if err != nil { 211 return nil, fmt.Errorf("failed to create blob container %s: %w", in.ContainerName, err) 212 } 213 214 return &CreateBlobContainerOutput{ 215 BlobContainer: to.Ptr(blobContainerResponse.BlobContainer), 216 }, nil 217 } 218 219 // CreatePageBlobInput containers the input parameters used for creating a page 220 // blob. 221 type CreatePageBlobInput struct { 222 StorageURL string 223 BlobURL string 224 ImageURL string 225 StorageAccountName string 226 BootstrapIgnData []byte 227 ImageLength int64 228 StorageAccountKeys []armstorage.AccountKey 229 CloudConfiguration cloud.Configuration 230 } 231 232 // CreatePageBlobOutput contains the return values after creating a page blob. 233 type CreatePageBlobOutput struct { 234 PageBlobClient *pageblob.Client 235 SharedKeyCredential *azblob.SharedKeyCredential 236 } 237 238 // CreatePageBlob creates a blob and uploads a file from a URL to it. 239 func CreatePageBlob(ctx context.Context, in *CreatePageBlobInput) (string, error) { 240 logrus.Debugf("Getting page blob credentials") 241 242 // XXX: Should try all of them until one is successful 243 sharedKeyCredential, err := azblob.NewSharedKeyCredential(in.StorageAccountName, *in.StorageAccountKeys[0].Value) 244 if err != nil { 245 return "", fmt.Errorf("failed to get shared credentials for storage account: %w", err) 246 } 247 248 logrus.Debugf("Getting page blob client") 249 pageBlobClient, err := pageblob.NewClientWithSharedKeyCredential( 250 in.BlobURL, 251 sharedKeyCredential, 252 &pageblob.ClientOptions{ 253 ClientOptions: azcore.ClientOptions{ 254 Cloud: in.CloudConfiguration, 255 }, 256 }, 257 ) 258 if err != nil { 259 return "", fmt.Errorf("failed to get page blob client: %w", err) 260 } 261 262 logrus.Debugf("Creating Page blob and uploading image to it") 263 if in.ImageURL == "" { 264 _, err = pageBlobClient.Create(ctx, in.ImageLength, nil) 265 if err != nil { 266 return "", fmt.Errorf("failed to create page blob with image contents: %w", err) 267 } 268 // This image (example: ignition shim) needs to be uploaded from a local file. 269 err = doUploadPages(ctx, pageBlobClient, in.BootstrapIgnData, in.ImageLength) 270 if err != nil { 271 return "", fmt.Errorf("failed to upload page blob image contents: %w", err) 272 } 273 } else { 274 // This is used in terraform, not sure if it matters 275 metadata := map[string]*string{ 276 "source_uri": to.Ptr(in.ImageURL), 277 } 278 279 _, err = pageBlobClient.Create(ctx, in.ImageLength, &pageblob.CreateOptions{ 280 Metadata: metadata, 281 }) 282 if err != nil { 283 return "", fmt.Errorf("failed to create page blob with image URL: %w", err) 284 } 285 286 err = doUploadPagesFromURL(ctx, pageBlobClient, in.ImageURL, in.ImageLength) 287 if err != nil { 288 return "", fmt.Errorf("failed to upload page blob image from URL %s: %w", in.ImageURL, err) 289 } 290 } 291 292 // Is this addition OK for when CreatePageBlob() is called from InfraReady() 293 sasURL, err := pageBlobClient.GetSASURL(sas.BlobPermissions{Read: true}, time.Now().Add(time.Minute*60), &blob.GetSASURLOptions{}) 294 if err != nil { 295 return "", fmt.Errorf("failed to get Page Blob SAS URL: %w", err) 296 } 297 return sasURL, nil 298 } 299 300 func doUploadPages(ctx context.Context, pageBlobClient *pageblob.Client, imageData []byte, imageLength int64) error { 301 logrus.Debugf("Uploading to Page Blob with Image of length :%d", imageLength) 302 303 // Page blobs file size must be a multiple of 512, hence a little padding is needed to push the file. 304 // imageLength has already been adjusted to the next highest size divisible by 512. 305 // So, here we are padding the image to match this size. 306 // Bootstrap Ignition is a json file. For parsing of this file to succeed with the padding, the 307 // file needs to end with a }. 308 logrus.Debugf("Original Image length: %d", int64(len(imageData))) 309 padding := imageLength - int64(len(imageData)) 310 paddingString := strings.Repeat(" ", int(padding)) + string(imageData[len(imageData)-1]) 311 imageData = append(imageData[0:len(imageData)-1], paddingString...) 312 logrus.Debugf("New Image length (after padding): %d", int64(len(imageData))) 313 314 pageSize := int64(1024 * 1024 * 4) 315 newOffset := int64(0) 316 remainingImageLength := imageLength 317 318 for remainingImageLength > 0 { 319 if remainingImageLength < pageSize { 320 pageSize = remainingImageLength 321 } 322 323 logrus.Debugf("Uploading pages with Offset :%d and Count :%d", newOffset, pageSize) 324 325 _, err := pageBlobClient.UploadPages( 326 ctx, 327 streaming.NopCloser(bytes.NewReader(imageData)), 328 blob.HTTPRange{ 329 Offset: newOffset, 330 Count: pageSize, 331 }, 332 nil) 333 if err != nil { 334 return fmt.Errorf("failed uploading Image to page blob: %w", err) 335 } 336 newOffset += pageSize 337 remainingImageLength -= pageSize 338 logrus.Debugf("newOffset :%d and remainingImageLength :%d", newOffset, remainingImageLength) 339 } 340 return nil 341 } 342 343 func doUploadPagesFromURL(ctx context.Context, pageBlobClient *pageblob.Client, imageURL string, imageLength int64) error { 344 // Azure only allows 4MB chunks, See 345 // https://docs.microsoft.com/rest/api/storageservices/put-page-from-url 346 pageSize := int64(1024 * 1024 * 4) 347 leftOverBytes := imageLength % pageSize 348 offset := int64(0) 349 pages := int64(0) 350 351 if imageLength > pageSize { 352 pages = imageLength / pageSize 353 if imageLength%pageSize > 0 { 354 pages++ 355 } 356 } else { 357 pageSize = imageLength 358 pages = 1 359 } 360 361 threadsPerGroup := int64(64) 362 if pages < threadsPerGroup { 363 threadsPerGroup = pages 364 } 365 366 threadGroups := pages / threadsPerGroup 367 if pages%threadsPerGroup > 0 { 368 threadGroups++ 369 } 370 371 var wg sync.WaitGroup 372 var threadError error 373 var res error 374 375 pagesLeft := pages 376 for threadGroup := int64(0); threadGroup < threadGroups; threadGroup++ { 377 if pagesLeft < threadsPerGroup { 378 threadsPerGroup = pagesLeft 379 } 380 381 errors := make(chan error, 1) 382 defer close(errors) 383 384 results := make(chan int64, threadsPerGroup) 385 defer close(results) 386 387 for thread := int64(0); thread < threadsPerGroup; thread++ { 388 if offset+pageSize >= imageLength && leftOverBytes > 0 { 389 pageSize = leftOverBytes 390 leftOverBytes = 0 391 } else if offset > imageLength { 392 break 393 } 394 395 wg.Add(1) 396 go func(ctx context.Context, source string, thread, sourceOffset, destOffset, count int64, wg *sync.WaitGroup) { 397 defer wg.Done() 398 var err error 399 nretries := 3 400 for i := 0; i < nretries; i++ { 401 _, err = pageBlobClient.UploadPagesFromURL(ctx, imageURL, sourceOffset, destOffset, count, nil) 402 if err == nil { 403 break 404 } 405 } 406 errors <- err 407 results <- thread 408 }(ctx, imageURL, thread, offset, offset, pageSize, &wg) 409 410 offset += pageSize 411 } 412 pagesLeft -= threadsPerGroup 413 for thread := int64(0); thread < threadsPerGroup; thread++ { 414 threadError = <-errors 415 416 // XXX: Save first error only. Should we care about the 417 // rest? 418 if threadError != nil && res == nil { 419 res = threadError 420 } 421 <-results 422 } 423 wg.Wait() 424 if res != nil { 425 logrus.Debug("Failed to upload rhcos image") 426 break 427 } 428 429 logrus.Debugf("%d out of %d pages uploaded", pages-pagesLeft, pages) 430 } 431 432 logrus.Debugf("Done uploading") 433 return res 434 } 435 436 // CreateBlockBlobInput containers the input parameters used for creating a 437 // block blob. 438 type CreateBlockBlobInput struct { 439 StorageURL string 440 BlobURL string 441 StorageAccountName string 442 BootstrapIgnData []byte 443 StorageAccountKeys []armstorage.AccountKey 444 CloudConfiguration cloud.Configuration 445 } 446 447 // CreateBlockBlobOutput contains the return values after creating a block 448 // blob. 449 type CreateBlockBlobOutput struct { 450 PageBlobClient *pageblob.Client 451 SharedKeyCredential *azblob.SharedKeyCredential 452 } 453 454 // CreateBlockBlob creates a block blob and uploads a file from a URL to it. 455 func CreateBlockBlob(ctx context.Context, in *CreateBlockBlobInput) (string, error) { 456 logrus.Debugf("Getting block blob credentials") 457 458 // XXX: Should try all of them until one is successful 459 sharedKeyCredential, err := azblob.NewSharedKeyCredential(in.StorageAccountName, *in.StorageAccountKeys[0].Value) 460 if err != nil { 461 return "", fmt.Errorf("failed to get shared crdentials for storage account: %w", err) 462 } 463 464 logrus.Debugf("Getting block blob client") 465 blockBlobClient, err := blockblob.NewClientWithSharedKeyCredential( 466 in.BlobURL, 467 sharedKeyCredential, 468 &blockblob.ClientOptions{ 469 ClientOptions: azcore.ClientOptions{ 470 Cloud: in.CloudConfiguration, 471 }, 472 }, 473 ) 474 if err != nil { 475 return "", fmt.Errorf("failed to get page blob client: %w", err) 476 } 477 478 logrus.Debugf("Creating block blob") 479 480 accessTier := blob.AccessTierHot 481 _, err = blockBlobClient.Upload(ctx, streaming.NopCloser(bytes.NewReader(in.BootstrapIgnData)), &blockblob.UploadOptions{ 482 Tier: &accessTier, 483 }) 484 if err != nil { 485 return "", fmt.Errorf("failed to create block blob: %w", err) 486 } 487 488 sasURL, err := blockBlobClient.GetSASURL(sas.BlobPermissions{Read: true}, time.Now().Add(time.Minute*60), &blob.GetSASURLOptions{}) 489 if err != nil { 490 return "", fmt.Errorf("failed to get SAS URL: %w", err) 491 } 492 493 return sasURL, nil 494 } 495 496 // CustomerManagedKeyInput contains the input parameters for creating the 497 // customer managed key and identity. 498 type CustomerManagedKeyInput struct { 499 SubscriptionID string 500 ResourceGroupName string 501 CustomerManagedKey *aztypes.CustomerManagedKey 502 TokenCredential azcore.TokenCredential 503 } 504 505 // GenerateStorageAccountEncryption generates all the Encryption information for the Storage Account 506 // using the Customer Managed Key. 507 func GenerateStorageAccountEncryption(ctx context.Context, in *CustomerManagedKeyInput) (*armstorage.Encryption, error) { 508 logrus.Debugf("Generating Encryption for Storage Account") 509 510 if in.CustomerManagedKey == nil { 511 logrus.Debugf("No Customer Managed Key provided. So, Encryption not enabled on storage account.") 512 return &armstorage.Encryption{}, nil 513 } 514 515 keyvaultClientFactory, err := armkeyvault.NewClientFactory( 516 in.SubscriptionID, 517 in.TokenCredential, 518 nil) 519 if err != nil { 520 return nil, fmt.Errorf("failed to get key vault client factory %w", err) 521 } 522 523 keysClient = keyvaultClientFactory.NewKeysClient() 524 525 _, err = keysClient.Get( 526 ctx, 527 in.CustomerManagedKey.KeyVault.ResourceGroup, 528 in.CustomerManagedKey.KeyVault.Name, 529 in.CustomerManagedKey.KeyVault.KeyName, 530 &armkeyvault.KeysClientGetOptions{}) 531 if err != nil { 532 return nil, fmt.Errorf("failed to get customer managed key %s from key vault %s: %w", in.CustomerManagedKey.KeyVault.KeyName, in.CustomerManagedKey.KeyVault.Name, err) 533 } 534 535 vaultsClient = keyvaultClientFactory.NewVaultsClient() 536 537 keyVault, err := vaultsClient.Get( 538 ctx, 539 in.CustomerManagedKey.KeyVault.ResourceGroup, 540 in.CustomerManagedKey.KeyVault.Name, 541 &armkeyvault.VaultsClientGetOptions{}) 542 if err != nil { 543 return nil, fmt.Errorf("failed to get key vault %s which contains customer managed key: %w", in.CustomerManagedKey.KeyVault.Name, err) 544 } 545 546 encryption := &armstorage.Encryption{ 547 Services: &armstorage.EncryptionServices{ 548 Blob: &armstorage.EncryptionService{ 549 Enabled: to.Ptr(true), 550 KeyType: to.Ptr(armstorage.KeyTypeAccount), 551 }, 552 File: &armstorage.EncryptionService{ 553 Enabled: to.Ptr(true), 554 KeyType: to.Ptr(armstorage.KeyTypeAccount), 555 }, 556 }, 557 EncryptionIdentity: &armstorage.EncryptionIdentity{ 558 EncryptionUserAssignedIdentity: to.Ptr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ManagedIdentity/userAssignedIdentities/%s", 559 in.SubscriptionID, 560 in.CustomerManagedKey.KeyVault.ResourceGroup, 561 in.CustomerManagedKey.UserAssignedIdentityKey, 562 )), 563 }, 564 KeySource: to.Ptr(armstorage.KeySourceMicrosoftKeyvault), 565 KeyVaultProperties: &armstorage.KeyVaultProperties{ 566 KeyName: to.Ptr(in.CustomerManagedKey.KeyVault.KeyName), 567 KeyVersion: to.Ptr(""), 568 KeyVaultURI: keyVault.Properties.VaultURI, 569 }, 570 } 571 572 return encryption, nil 573 }