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 }