github.com/openshift/installer@v1.4.17/pkg/gather/azure/azure.go (about)

     1  package azure
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
    15  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
    16  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v4"
    17  	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage"
    18  	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
    19  	"github.com/pkg/errors"
    20  	"github.com/sirupsen/logrus"
    21  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    22  
    23  	azuresession "github.com/openshift/installer/pkg/asset/installconfig/azure"
    24  	"github.com/openshift/installer/pkg/gather"
    25  	"github.com/openshift/installer/pkg/gather/providers"
    26  	"github.com/openshift/installer/pkg/types"
    27  	"github.com/openshift/installer/pkg/types/azure"
    28  )
    29  
    30  // Gather holds options for resources we want to gather.
    31  type Gather struct {
    32  	resourceGroupName     string
    33  	logger                logrus.FieldLogger
    34  	serialLogBundle       string
    35  	directory             string
    36  	virtualMachinesClient *armcompute.VirtualMachinesClient
    37  	accountsClient        *armstorage.AccountsClient
    38  }
    39  
    40  // New returns a Azure Gather from ClusterMetadata.
    41  func New(logger logrus.FieldLogger, serialLogBundle string, bootstrap string, masters []string, metadata *types.ClusterMetadata) (providers.Gather, error) {
    42  	cloudName := metadata.Azure.CloudName
    43  	if cloudName == "" {
    44  		cloudName = azure.PublicCloud
    45  	}
    46  
    47  	resourceGroupName := metadata.Azure.ResourceGroupName
    48  	if resourceGroupName == "" {
    49  		resourceGroupName = metadata.InfraID + "-rg"
    50  	}
    51  
    52  	session, err := azuresession.GetSession(cloudName, metadata.Azure.ARMEndpoint)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	accountClientOptions := arm.ClientOptions{
    58  		ClientOptions: policy.ClientOptions{
    59  			// NOTE: the api version must support AzureStack
    60  			APIVersion: "2019-04-01",
    61  			Cloud:      session.CloudConfig,
    62  		},
    63  	}
    64  	accountsClient, err := armstorage.NewAccountsClient(session.Credentials.SubscriptionID, session.TokenCreds, &accountClientOptions)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	vmClientOptions := arm.ClientOptions{
    70  		ClientOptions: policy.ClientOptions{
    71  			// NOTE: the api version must both support AzureStack and BootDignosticsData
    72  			APIVersion: "2020-06-01",
    73  			Cloud:      session.CloudConfig,
    74  		},
    75  	}
    76  	virtualMachinesClient, err := armcompute.NewVirtualMachinesClient(session.Credentials.SubscriptionID, session.TokenCreds, &vmClientOptions)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	gather := &Gather{
    82  		resourceGroupName:     resourceGroupName,
    83  		logger:                logger,
    84  		serialLogBundle:       serialLogBundle,
    85  		directory:             filepath.Dir(serialLogBundle),
    86  		accountsClient:        accountsClient,
    87  		virtualMachinesClient: virtualMachinesClient,
    88  	}
    89  
    90  	return gather, nil
    91  }
    92  
    93  // Run is the entrypoint to start the gather process.
    94  func (g *Gather) Run() error {
    95  	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
    96  	defer cancel()
    97  
    98  	accountList, err := getAccounts(ctx, g)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	sharedKeyCredentials, err := getSharedKeyCredentials(ctx, accountList, g)
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	virtualMachines, err := getVirtualMachines(ctx, g)
   109  	if err != nil {
   110  		return err
   111  	}
   112  
   113  	// We can only get the serial log from VM's with boot diagnostics enabled
   114  	bootDiagnostics := getBootDiagnostics(ctx, virtualMachines, g)
   115  	if len(bootDiagnostics) == 0 {
   116  		g.logger.Debug("No boot logs found")
   117  		return nil
   118  	}
   119  
   120  	err = downloadFiles(ctx, bootDiagnostics, accountList, sharedKeyCredentials, g)
   121  	if err != nil {
   122  		return err
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  func getAccounts(ctx context.Context, g *Gather) ([]*armstorage.Account, error) {
   129  	var accounts []*armstorage.Account
   130  	pager := g.accountsClient.NewListByResourceGroupPager(g.resourceGroupName, nil)
   131  	for pager.More() {
   132  		accountListResult, err := pager.NextPage(ctx)
   133  		if err != nil {
   134  			return nil, errors.Wrap(err, "could not find any storage accounts")
   135  		}
   136  		accounts = append(accounts, accountListResult.Value...)
   137  	}
   138  	return accounts, nil
   139  }
   140  
   141  func getSharedKeyCredentials(ctx context.Context, accounts []*armstorage.Account, g *Gather) ([]*azblob.SharedKeyCredential, error) {
   142  	var sharedKeyCredentials []*azblob.SharedKeyCredential
   143  	for _, account := range accounts {
   144  		keyResults, err := g.accountsClient.ListKeys(ctx, g.resourceGroupName, *account.Name, nil)
   145  		if err != nil {
   146  			g.logger.Debugf("Failed to list keys: %s", err.Error())
   147  			continue
   148  		}
   149  		if keyResults.Keys != nil {
   150  			for _, key := range keyResults.Keys {
   151  				if key.Value != nil {
   152  					sharedKeyCredential, err := azblob.NewSharedKeyCredential(*account.Name, *key.Value)
   153  					if err != nil {
   154  						g.logger.Debugf("Failed to get shared key: %s", err.Error())
   155  						continue
   156  					}
   157  					sharedKeyCredentials = append(sharedKeyCredentials, sharedKeyCredential)
   158  				}
   159  			}
   160  		}
   161  	}
   162  
   163  	return sharedKeyCredentials, nil
   164  }
   165  
   166  func getVirtualMachines(ctx context.Context, g *Gather) ([]*armcompute.VirtualMachine, error) {
   167  	vmsPager := g.virtualMachinesClient.NewListPager(g.resourceGroupName, nil)
   168  
   169  	var virtualMachines []*armcompute.VirtualMachine
   170  	for vmsPager.More() {
   171  		vmsPage, err := vmsPager.NextPage(ctx)
   172  		if err != nil {
   173  			g.logger.Debugf("Failed to get vm: %s", err.Error())
   174  			return nil, err
   175  		}
   176  		virtualMachines = append(virtualMachines, vmsPage.Value...)
   177  	}
   178  
   179  	return virtualMachines, nil
   180  }
   181  
   182  func getBootDiagnostics(ctx context.Context, virtualMachines []*armcompute.VirtualMachine, g *Gather) []string {
   183  	var bootDiagnostics []string
   184  	for _, vm := range virtualMachines {
   185  		if vm.Properties.DiagnosticsProfile == nil ||
   186  			vm.Properties.DiagnosticsProfile.BootDiagnostics == nil ||
   187  			vm.Properties.DiagnosticsProfile.BootDiagnostics.Enabled == nil ||
   188  			!*vm.Properties.DiagnosticsProfile.BootDiagnostics.Enabled {
   189  			g.logger.Debugf("No boot logs or boot diagnostics disabled for %s", *vm.Name)
   190  			continue
   191  		}
   192  		instanceView, err := g.virtualMachinesClient.InstanceView(ctx, g.resourceGroupName, *vm.Name, nil)
   193  		if err != nil {
   194  			g.logger.Debugf("Failed to get instance view: %v", err)
   195  			continue
   196  		}
   197  		var sshotURI *string
   198  		var slogURI *string
   199  		if instanceView.BootDiagnostics != nil {
   200  			sshotURI = instanceView.BootDiagnostics.ConsoleScreenshotBlobURI
   201  			slogURI = instanceView.BootDiagnostics.SerialConsoleLogBlobURI
   202  		}
   203  
   204  		// Boot logs might be in managed account
   205  		if sshotURI == nil && slogURI == nil {
   206  			bootData, err := g.virtualMachinesClient.RetrieveBootDiagnosticsData(ctx, g.resourceGroupName, *vm.Name, nil)
   207  			if err != nil {
   208  				g.logger.Debugf("Failed to get boot diagnostics data: %v", err)
   209  				continue
   210  			}
   211  			sshotURI = bootData.ConsoleScreenshotBlobURI
   212  			slogURI = bootData.SerialConsoleLogBlobURI
   213  		}
   214  
   215  		if sshotURI != nil {
   216  			bootDiagnostics = append(bootDiagnostics, *sshotURI)
   217  		}
   218  		if slogURI != nil {
   219  			bootDiagnostics = append(bootDiagnostics, *slogURI)
   220  		}
   221  	}
   222  
   223  	return bootDiagnostics
   224  }
   225  
   226  func downloadFiles(ctx context.Context, fileURIs []string, accounts []*armstorage.Account, sharedKeyCredentials []*azblob.SharedKeyCredential, g *Gather) error {
   227  	var errs []error
   228  
   229  	serialLogBundleDir := filepath.Join(g.directory, strings.TrimSuffix(filepath.Base(g.serialLogBundle), ".tar.gz"))
   230  
   231  	err := os.MkdirAll(serialLogBundleDir, 0o755)
   232  	if err != nil && !errors.Is(err, os.ErrExist) {
   233  		return err
   234  	}
   235  
   236  	files := make([]string, 0, len(fileURIs))
   237  	for _, fileURI := range fileURIs {
   238  		var filePath string
   239  		var err error
   240  		if isBlobInManagedAccount(fileURI, accounts) {
   241  			filePath, err = downloadFile(ctx, fileURI, serialLogBundleDir, g)
   242  		} else {
   243  			filePath, err = downloadBlob(ctx, fileURI, serialLogBundleDir, sharedKeyCredentials, g)
   244  		}
   245  		if err != nil {
   246  			errs = append(errs, err)
   247  			continue
   248  		}
   249  		files = append(files, filePath)
   250  	}
   251  
   252  	if len(files) > 0 {
   253  		err := gather.CreateArchive(files, g.serialLogBundle)
   254  		if err != nil {
   255  			g.logger.Debugf("Failed to create archive: %s", err.Error())
   256  			errs = append(errs, err)
   257  		}
   258  	}
   259  
   260  	err = gather.DeleteArchiveDirectory(serialLogBundleDir)
   261  	if err != nil {
   262  		g.logger.Debugf("Failed to remove archive directory: %v", err)
   263  	}
   264  
   265  	return utilerrors.NewAggregate(errs)
   266  }
   267  
   268  func downloadBlob(ctx context.Context, fileURI string, filePathDir string, sharedKeyCredentials []*azblob.SharedKeyCredential, g *Gather) (string, error) {
   269  	g.logger.Debugf("Attemping to download %s", fileURI)
   270  
   271  	uri, err := url.ParseRequestURI(fileURI)
   272  	if err != nil {
   273  		return "", err
   274  	}
   275  	uriParts := strings.Split(uri.Path, "/")
   276  	containerName := uriParts[len(uriParts)-2]
   277  	blobName := uriParts[len(uriParts)-1]
   278  	filePath := filepath.Join(filePathDir, blobName)
   279  
   280  	accountURL := fmt.Sprintf("%s://%s/", uri.Scheme, uri.Host)
   281  	for _, credential := range sharedKeyCredentials {
   282  		if !strings.HasPrefix(uri.Host, credential.AccountName()) {
   283  			continue
   284  		}
   285  		blobClient, err := azblob.NewClientWithSharedKeyCredential(accountURL, credential, nil)
   286  		if err != nil {
   287  			g.logger.Debugf("Failed to create blob client: %s", err.Error())
   288  			continue
   289  		}
   290  
   291  		file, err := os.Create(filePath)
   292  		if err != nil {
   293  			g.logger.Debugf("Failed to create file: %s", err.Error())
   294  			return "", err
   295  		}
   296  		defer file.Close()
   297  
   298  		_, err = blobClient.DownloadFile(ctx, containerName, blobName, file, nil)
   299  		if err != nil {
   300  			return "", err
   301  		}
   302  
   303  		return filePath, nil
   304  	}
   305  
   306  	return "", errors.Errorf("unable to download file: %s", filePath)
   307  }
   308  
   309  func downloadFile(ctx context.Context, fileURI string, filePathDir string, g *Gather) (string, error) {
   310  	g.logger.Debug("Attempting to download file from managed account")
   311  
   312  	uri, err := url.ParseRequestURI(fileURI)
   313  	if err != nil {
   314  		return "", err
   315  	}
   316  	filePath := filepath.Join(filePathDir, filepath.Base(uri.Path))
   317  
   318  	req, err := http.NewRequestWithContext(ctx, "GET", fileURI, nil)
   319  	if err != nil {
   320  		return "", err
   321  	}
   322  	resp, err := http.DefaultClient.Do(req)
   323  	if err != nil {
   324  		return "", err
   325  	}
   326  	defer resp.Body.Close()
   327  
   328  	if resp.StatusCode != http.StatusOK {
   329  		return "", fmt.Errorf("unable to download file: %s", filePath)
   330  	}
   331  
   332  	file, err := os.Create(filePath)
   333  	if err != nil {
   334  		return "", err
   335  	}
   336  	defer file.Close()
   337  
   338  	_, err = io.Copy(file, resp.Body)
   339  	if err != nil {
   340  		return "", err
   341  	}
   342  
   343  	return filePath, nil
   344  }
   345  
   346  func isBlobInManagedAccount(blobURI string, accounts []*armstorage.Account) bool {
   347  	for _, account := range accounts {
   348  		if account.Properties != nil &&
   349  			account.Properties.PrimaryEndpoints != nil &&
   350  			account.Properties.PrimaryEndpoints.Blob != nil &&
   351  			strings.HasPrefix(blobURI, *account.Properties.PrimaryEndpoints.Blob) {
   352  			return false
   353  		}
   354  	}
   355  	return true
   356  }