github.com/openshift/installer@v1.4.17/pkg/types/nutanix/helpers.go (about)

     1  package nutanix
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/google/uuid"
    13  	"github.com/kdomanski/iso9660"
    14  	"github.com/nutanix-cloud-native/prism-go-client/utils"
    15  	nutanixclientv3 "github.com/nutanix-cloud-native/prism-go-client/v3"
    16  	"github.com/pkg/errors"
    17  
    18  	machinev1 "github.com/openshift/api/machine/v1"
    19  )
    20  
    21  const (
    22  	diskLabel        = "config-2"
    23  	isoFile          = "bootstrap-ign.iso"
    24  	metadataFilePath = "openstack/latest/meta_data.json"
    25  	userDataFilePath = "openstack/latest/user_data"
    26  	sleepTime        = 10 * time.Second
    27  	timeout          = 5 * time.Minute
    28  
    29  	// Category Key format: "kubernetes-io-cluster-<cluster-id>".
    30  	categoryKeyPrefix = "kubernetes-io-cluster-"
    31  	// CategoryValueOwned is the category value representing owned by the cluster.
    32  	CategoryValueOwned = "owned"
    33  	// CategoryValueShared is the category value representing shared by the cluster.
    34  	CategoryValueShared = "shared"
    35  )
    36  
    37  type metadataCloudInit struct {
    38  	UUID string `json:"uuid"`
    39  }
    40  
    41  // BootISOImageName is the image name for Bootstrap node for a given infraID.
    42  func BootISOImageName(infraID string) string {
    43  	return fmt.Sprintf("%s-%s", infraID, isoFile)
    44  }
    45  
    46  // BootISOImagePath is the image path for Bootstrap node for a given infraID and path.
    47  func BootISOImagePath(path, infraID string) string {
    48  	imgName := BootISOImageName(infraID)
    49  	application := "openshift-installer"
    50  	subdir := "image_cache"
    51  	fullISOFile := filepath.Join(path, application, subdir, imgName)
    52  	return fullISOFile
    53  }
    54  
    55  // CreateBootstrapISO creates a ISO for the bootstrap node.
    56  func CreateBootstrapISO(infraID, userData string) (string, error) {
    57  	id := uuid.New()
    58  	metaObj := &metadataCloudInit{
    59  		UUID: id.String(),
    60  	}
    61  	metadata, err := json.Marshal(metaObj)
    62  	if err != nil {
    63  		return "", errors.Wrap(err, fmt.Sprintf("failed to marshal metadata struct to json"))
    64  	}
    65  
    66  	writer, err := iso9660.NewWriter()
    67  	if err != nil {
    68  		return "", errors.Wrap(err, fmt.Sprintf("failed to create writer: %s", err))
    69  	}
    70  
    71  	defer writer.Cleanup()
    72  
    73  	userDataReader := strings.NewReader(userData)
    74  	err = writer.AddFile(userDataReader, userDataFilePath)
    75  	if err != nil {
    76  		return "", errors.Wrap(err, fmt.Sprintf("failed to add file: %s", err))
    77  	}
    78  
    79  	metadataReader := strings.NewReader(string(metadata))
    80  	err = writer.AddFile(metadataReader, metadataFilePath)
    81  	if err != nil {
    82  		return "", errors.Wrap(err, fmt.Sprintf("failed to add file: %s", err))
    83  	}
    84  
    85  	cacheDir, err := os.UserCacheDir()
    86  	if err != nil {
    87  		return "", errors.Wrap(err, "unable to fetch user cache dir")
    88  	}
    89  
    90  	fullISOFile := BootISOImagePath(cacheDir, infraID)
    91  	fullISOFileDir, err := filepath.Abs(filepath.Dir(fullISOFile))
    92  	if err != nil {
    93  		return "", errors.Wrap(err, "unable to extract parent directory from bootstrap iso filepath")
    94  	}
    95  
    96  	_, err = os.Stat(fullISOFileDir)
    97  	if err != nil {
    98  		if os.IsNotExist(err) {
    99  			err = os.MkdirAll(fullISOFileDir, 0755)
   100  			if err != nil {
   101  				return "", errors.Wrap(err, fmt.Sprintf("failed to create %s", fullISOFileDir))
   102  			}
   103  		} else {
   104  			return "", errors.Wrap(err, fmt.Sprintf("cannot access %s", fullISOFileDir))
   105  		}
   106  	}
   107  
   108  	outputFile, err := os.OpenFile(fullISOFile, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644)
   109  	if err != nil {
   110  		return "", errors.Wrap(err, fmt.Sprintf("failed to create file: %s", err))
   111  	}
   112  
   113  	err = writer.WriteTo(outputFile, diskLabel)
   114  	if err != nil {
   115  		return "", errors.Wrap(err, fmt.Sprintf("failed to write ISO image: %s", err))
   116  	}
   117  
   118  	err = outputFile.Close()
   119  	if err != nil {
   120  		return "", errors.Wrap(err, fmt.Sprintf("failed to close output file: %s", err))
   121  	}
   122  
   123  	return fullISOFile, nil
   124  }
   125  
   126  // WaitForTasks is a wrapper for WaitForTask.
   127  func WaitForTasks(clientV3 nutanixclientv3.Service, taskUUIDs []string) error {
   128  	for _, t := range taskUUIDs {
   129  		err := WaitForTask(clientV3, t)
   130  		if err != nil {
   131  			return err
   132  		}
   133  	}
   134  	return nil
   135  }
   136  
   137  // WaitForTask waits until a queued task has been finished or timeout has been reached.
   138  func WaitForTask(clientV3 nutanixclientv3.Service, taskUUID string) error {
   139  	finished := false
   140  	var err error
   141  	for start := time.Now(); time.Since(start) < timeout; {
   142  		finished, err = isTaskFinished(clientV3, taskUUID)
   143  		if err != nil {
   144  			return err
   145  		}
   146  		if finished {
   147  			break
   148  		}
   149  		time.Sleep(sleepTime)
   150  	}
   151  	if !finished {
   152  		return errors.Errorf("timeout while waiting for task UUID: %s", taskUUID)
   153  	}
   154  
   155  	return nil
   156  }
   157  
   158  func isTaskFinished(clientV3 nutanixclientv3.Service, taskUUID string) (bool, error) {
   159  	isFinished := map[string]bool{
   160  		"QUEUED":    false,
   161  		"RUNNING":   false,
   162  		"SUCCEEDED": true,
   163  	}
   164  	status, err := getTaskStatus(clientV3, taskUUID)
   165  	if err != nil {
   166  		return false, err
   167  	}
   168  	if val, ok := isFinished[status]; ok {
   169  		return val, nil
   170  	}
   171  	return false, errors.Errorf("retrieved unexpected task status: %s", status)
   172  }
   173  
   174  func getTaskStatus(clientV3 nutanixclientv3.Service, taskUUID string) (string, error) {
   175  	ctx, cancel := context.WithTimeout(context.TODO(), 60*time.Second)
   176  	defer cancel()
   177  	v, err := clientV3.GetTask(ctx, taskUUID)
   178  
   179  	if err != nil {
   180  		return "", err
   181  	}
   182  
   183  	if *v.Status == "INVALID_UUID" || *v.Status == "FAILED" {
   184  		return *v.Status, errors.Errorf("error_detail: %s, progress_message: %s", utils.StringValue(v.ErrorDetail), utils.StringValue(v.ProgressMessage))
   185  	}
   186  	return *v.Status, nil
   187  }
   188  
   189  // RHCOSImageName is the unique image name for a given cluster.
   190  func RHCOSImageName(infraID string) string {
   191  	return fmt.Sprintf("%s-rhcos", infraID)
   192  }
   193  
   194  // CategoryKey returns the cluster specific category key name.
   195  func CategoryKey(infraID string) string {
   196  	categoryKey := fmt.Sprintf("%s%s", categoryKeyPrefix, infraID)
   197  	return categoryKey
   198  }
   199  
   200  // GetGPUList returns a list of VMGpus for the given list of GPU identifiers in the Prism Element (uuid).
   201  func GetGPUList(ctx context.Context, client *nutanixclientv3.Client, gpus []machinev1.NutanixGPU, peUUID string) ([]*nutanixclientv3.VMGpu, error) {
   202  	vmGPUs := make([]*nutanixclientv3.VMGpu, 0)
   203  
   204  	if len(gpus) == 0 {
   205  		return vmGPUs, nil
   206  	}
   207  
   208  	peGPUs, err := GetGPUsForPE(ctx, client, peUUID)
   209  	if err != nil {
   210  		return nil, fmt.Errorf("failed to retrieve GPUs of the Prism Element cluster (uuid: %v): %w", peUUID, err)
   211  	}
   212  	if len(peGPUs) == 0 {
   213  		return nil, fmt.Errorf("no available GPUs found in Prism Element cluster (uuid: %s)", peUUID)
   214  	}
   215  
   216  	for _, gpu := range gpus {
   217  		foundGPU, err := GetGPUFromList(ctx, client, gpu, peGPUs)
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  		vmGPUs = append(vmGPUs, foundGPU)
   222  	}
   223  
   224  	return vmGPUs, nil
   225  }
   226  
   227  // GetGPUFromList returns the VMGpu matching the input reqirements from the provided list of GPU devices.
   228  func GetGPUFromList(ctx context.Context, client *nutanixclientv3.Client, gpu machinev1.NutanixGPU, gpuDevices []*nutanixclientv3.GPU) (*nutanixclientv3.VMGpu, error) {
   229  	for _, gd := range gpuDevices {
   230  		if gd.Status != "UNUSED" {
   231  			continue
   232  		}
   233  
   234  		if (gpu.Type == machinev1.NutanixGPUIdentifierDeviceID && gd.DeviceID != nil && *gpu.DeviceID == int32(*gd.DeviceID)) ||
   235  			(gpu.Type == machinev1.NutanixGPUIdentifierName && *gpu.Name == gd.Name) {
   236  			return &nutanixclientv3.VMGpu{
   237  				DeviceID: gd.DeviceID,
   238  				Mode:     &gd.Mode,
   239  				Vendor:   &gd.Vendor,
   240  			}, nil
   241  		}
   242  	}
   243  
   244  	return nil, fmt.Errorf("no available GPU found that matches required GPU inputs")
   245  }
   246  
   247  // GetGPUsForPE returns all the GPU devices for the given Prism Element (uuid).
   248  func GetGPUsForPE(ctx context.Context, client *nutanixclientv3.Client, peUUID string) ([]*nutanixclientv3.GPU, error) {
   249  	gpus := make([]*nutanixclientv3.GPU, 0)
   250  	hosts, err := client.V3.ListAllHost(ctx)
   251  	if err != nil {
   252  		return gpus, fmt.Errorf("failed to get hosts from Prism Central: %w", err)
   253  	}
   254  
   255  	for _, host := range hosts.Entities {
   256  		if host == nil ||
   257  			host.Status == nil ||
   258  			host.Status.ClusterReference == nil ||
   259  			host.Status.ClusterReference.UUID != peUUID ||
   260  			host.Status.Resources == nil ||
   261  			len(host.Status.Resources.GPUList) == 0 {
   262  			continue
   263  		}
   264  
   265  		for _, peGpu := range host.Status.Resources.GPUList {
   266  			if peGpu != nil {
   267  				gpus = append(gpus, peGpu)
   268  			}
   269  		}
   270  	}
   271  
   272  	return gpus, nil
   273  }
   274  
   275  // FindImageUUIDByName retrieves the image resource uuid by the given image name from PC.
   276  func FindImageUUIDByName(ctx context.Context, ntnxclient *nutanixclientv3.Client, imageName string) (*string, error) {
   277  	res, err := ntnxclient.V3.ListImage(ctx, &nutanixclientv3.DSMetadata{
   278  		Filter: utils.StringPtr(fmt.Sprintf("name==%s", imageName)),
   279  	})
   280  	if err != nil || len(res.Entities) == 0 {
   281  		return nil, fmt.Errorf("failed to find image by name %q. err: %w", imageName, err)
   282  	}
   283  
   284  	if len(res.Entities) > 1 {
   285  		return nil, fmt.Errorf("found more than one (%v) images with name %q", len(res.Entities), imageName)
   286  	}
   287  
   288  	return res.Entities[0].Metadata.UUID, nil
   289  }