github.com/openshift/installer@v1.4.17/pkg/infrastructure/nutanix/clusterapi/clusterapi.go (about) 1 package clusterapi 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 nutanixclientv3 "github.com/nutanix-cloud-native/prism-go-client/v3" 10 "github.com/sirupsen/logrus" 11 "k8s.io/apimachinery/pkg/util/wait" 12 "k8s.io/utils/ptr" 13 14 infracapi "github.com/openshift/installer/pkg/infrastructure/clusterapi" 15 nutanixtypes "github.com/openshift/installer/pkg/types/nutanix" 16 ) 17 18 // Provider is the Nutanix implementation of the clusterapi InfraProvider. 19 type Provider struct{} 20 21 var _ infracapi.PreProvider = Provider{} 22 var _ infracapi.IgnitionProvider = Provider{} 23 24 // Name returns the Nutanix provider name. 25 func (p Provider) Name() string { 26 return nutanixtypes.Name 27 } 28 29 // PublicGatherEndpoint indicates that machine ready checks should NOT wait for an ExternalIP 30 // in the status when declaring machines ready. 31 func (Provider) PublicGatherEndpoint() infracapi.GatherEndpoint { return infracapi.InternalIP } 32 33 // PreProvision creates the resources required prior to running capi nutanix controller. 34 func (p Provider) PreProvision(ctx context.Context, in infracapi.PreProvisionInput) error { 35 // create categories with name "kubernetes-io-cluster-<cluster_id>" and values ["owned", "shared"]. 36 // load the rhcos image to prism_central. 37 38 ic := in.InstallConfig.Config 39 nutanixCl, err := nutanixtypes.CreateNutanixClientFromPlatform(ic.Platform.Nutanix) 40 if err != nil { 41 return fmt.Errorf("failed to create nutanix client: %w", err) 42 } 43 44 // create the category key. 45 categoryKey := nutanixtypes.CategoryKey(in.InfraID) 46 keyBody := &nutanixclientv3.CategoryKey{ 47 APIVersion: ptr.To("3.1.0"), 48 Description: ptr.To("Openshift Cluster Category Key"), 49 Name: ptr.To(categoryKey), 50 } 51 respk, err := nutanixCl.V3.CreateOrUpdateCategoryKey(ctx, keyBody) 52 if err != nil { 53 return fmt.Errorf("failed to create the category key %q: %w", categoryKey, err) 54 } 55 logrus.Infof("created the category key %q", *respk.Name) 56 57 // create the category value "owned". 58 valBody := &nutanixclientv3.CategoryValue{ 59 Description: ptr.To("Openshift Cluster Category Value: resources owned by the cluster"), 60 Value: ptr.To(nutanixtypes.CategoryValueOwned), 61 } 62 respv, err := nutanixCl.V3.CreateOrUpdateCategoryValue(ctx, categoryKey, valBody) 63 if err != nil { 64 return fmt.Errorf("failed to create the category value %q with category key %q: %w", nutanixtypes.CategoryValueOwned, categoryKey, err) 65 } 66 logrus.Infof("created the category value %q with name %q", *respv.Value, *respv.Name) 67 68 // create the category value "shared". 69 valBody = &nutanixclientv3.CategoryValue{ 70 Description: ptr.To("Openshift Cluster Category Value: resources used but not owned by the cluster"), 71 Value: ptr.To(nutanixtypes.CategoryValueShared), 72 } 73 respv, err = nutanixCl.V3.CreateOrUpdateCategoryValue(ctx, categoryKey, valBody) 74 if err != nil { 75 return fmt.Errorf("failed to create the category value %q with category key %q: %w", nutanixtypes.CategoryValueShared, categoryKey, err) 76 } 77 logrus.Infof("created the category value %q with name %q", *respv.Value, *respv.Name) 78 79 // upload the rhcos image. 80 imgName := nutanixtypes.RHCOSImageName(in.InfraID) 81 imgURI := in.RhcosImage.ControlPlane 82 imgReq := &nutanixclientv3.ImageIntentInput{} 83 imgSpec := &nutanixclientv3.Image{ 84 Name: &imgName, 85 Description: ptr.To("Created By OpenShift Installer"), 86 Resources: &nutanixclientv3.ImageResources{ 87 ImageType: ptr.To("DISK_IMAGE"), 88 SourceURI: &imgURI, 89 }, 90 } 91 imgReq.Spec = imgSpec 92 imgMeta := &nutanixclientv3.Metadata{ 93 Kind: ptr.To("image"), 94 Categories: map[string]string{categoryKey: nutanixtypes.CategoryValueOwned}, 95 } 96 imgReq.Metadata = imgMeta 97 respi, err := nutanixCl.V3.CreateImage(ctx, imgReq) 98 if err != nil { 99 return fmt.Errorf("failed to create the rhcos image %q: %w", imgName, err) 100 } 101 imgUUID := *respi.Metadata.UUID 102 logrus.Infof("creating the rhcos image %s (uuid: %s).", imgName, imgUUID) 103 104 if taskUUID, ok := respi.Status.ExecutionContext.TaskUUID.(string); ok { 105 logrus.Infof("waiting the image data uploading from %s, taskUUID: %s.", imgURI, taskUUID) 106 107 // Wait till the image creation task is successed. 108 if err = nutanixtypes.WaitForTask(nutanixCl.V3, taskUUID); err != nil { 109 e1 := fmt.Errorf("failed to create the rhcos image %q: %w", imgName, err) 110 logrus.Error(e1) 111 return e1 112 } 113 logrus.Infof("created and uploaded the rhcos image data %s (uuid: %s)", imgName, imgUUID) 114 } else { 115 err = fmt.Errorf("failed to convert the task UUID %v to string", respi.Status.ExecutionContext.TaskUUID) 116 logrus.Errorf(err.Error()) 117 return err 118 } 119 120 return nil 121 } 122 123 // Ignition handles preconditions for bootstrap ignition and 124 // generates ignition data for the CAPI bootstrap ignition secret. 125 // Load the ignition iso image to prism_central. 126 func (p Provider) Ignition(ctx context.Context, in infracapi.IgnitionInput) ([]byte, error) { 127 ic := in.InstallConfig.Config 128 nutanixCl, err := nutanixtypes.CreateNutanixClientFromPlatform(ic.Platform.Nutanix) 129 if err != nil { 130 return nil, fmt.Errorf("failed to create nutanix client: %w", err) 131 } 132 133 imgName := nutanixtypes.BootISOImageName(in.InfraID) 134 imgPath, err := nutanixtypes.CreateBootstrapISO(in.InfraID, string(in.BootstrapIgnData)) 135 if err != nil { 136 return nil, fmt.Errorf("failed to create bootstrap ignition iso: %w", err) 137 } 138 139 // upload the bootstrap image. 140 imgReq := &nutanixclientv3.ImageIntentInput{} 141 imgSpec := &nutanixclientv3.Image{ 142 Name: &imgName, 143 Description: ptr.To("Created By OpenShift Installer"), 144 Resources: &nutanixclientv3.ImageResources{ 145 ImageType: ptr.To("ISO_IMAGE"), 146 }, 147 } 148 imgReq.Spec = imgSpec 149 categoryKey := nutanixtypes.CategoryKey(in.InfraID) 150 imgMeta := &nutanixclientv3.Metadata{ 151 Kind: ptr.To("image"), 152 Categories: map[string]string{categoryKey: nutanixtypes.CategoryValueOwned}, 153 } 154 imgReq.Metadata = imgMeta 155 156 // Wait for successful creation of the bootstrap image object and upload the image data to it in PC. 157 // Put both createImage() and uploadImageData() in the same wait.ExponentialBackoffWithContext(). 158 // Because if createImage() succeeds but uploadImageData() fails, we need to delete the image object 159 // and retry to call both createImage() and uploadImageData() again. The old-version prism-api server sometimes 160 // returns error for the uploadImage call and does not allow to retry the uploadImage call to the same image object. 161 timeout := 20 * time.Minute 162 if err = wait.ExponentialBackoffWithContext(ctx, wait.Backoff{ 163 Duration: time.Minute * 4, 164 Factor: float64(1.0), 165 Steps: 5, 166 Cap: timeout, 167 }, func(ctx context.Context) (bool, error) { 168 // create the bootstrap image object in PC 169 imgUUID, err1 := createImage(ctx, nutanixCl, imgReq, imgName) 170 if err1 != nil { 171 logrus.Errorf("failed to create the bootstrap image object %s in PC: %v", imgName, err1) 172 // no need to retry if the error code is 401 or 403 173 if strings.Contains(err1.Error(), `"code": 401`) || strings.Contains(err1.Error(), `"code": 403`) { 174 return false, err1 175 } 176 177 // delete the image object if uuid is not empty 178 if imgUUID != "" { 179 if e2 := deleteImage(ctx, nutanixCl, imgUUID); e2 != nil { 180 logrus.Errorf("failed to delete image object %s (uuid: %s): %v", imgName, imgUUID, e2) 181 } 182 } 183 return false, nil 184 } 185 186 // upload the image data to the bootstrap image object in PC 187 err2 := uploadImageData(ctx, nutanixCl, imgName, imgUUID, imgPath) 188 if err2 != nil { 189 logrus.Errorf("failed to upload the bootstrap image %s data: %v", imgName, err2) 190 // no need to retry if the error code is 401 or 403 191 if strings.Contains(err2.Error(), `"code": 401`) || strings.Contains(err2.Error(), `"code": 403`) { 192 return false, err2 193 } 194 195 // delete the image object 196 if e2 := deleteImage(ctx, nutanixCl, imgUUID); e2 != nil { 197 logrus.Errorf("failed to delete image object %s (uuid: %s): %v", imgName, imgUUID, e2) 198 } 199 return false, nil 200 } 201 202 return true, nil 203 }); err != nil { 204 if wait.Interrupted(err) { 205 err = fmt.Errorf("timeout/interrupt to create/upload the bootstrap image object %s in PC within %v: %w", imgName, timeout, err) 206 } else { 207 err = fmt.Errorf("failed to create/upload the bootstrap image object %s in PC: %w", imgName, err) 208 } 209 210 return in.BootstrapIgnData, err 211 } 212 logrus.Infof("Successfully created the bootstrap image object %s and uploaded its image data", imgName) 213 214 return in.BootstrapIgnData, nil 215 } 216 217 // createImage creates the image object in PC, with the provided request input. 218 // Returns the imageUUID if the image is created. 219 func createImage(ctx context.Context, nutanixCl *nutanixclientv3.Client, imgReq *nutanixclientv3.ImageIntentInput, imgName string) (string, error) { 220 t1 := time.Now() 221 222 // create the image object. 223 respi, err := nutanixCl.V3.CreateImage(ctx, imgReq) 224 if err != nil { 225 return "", fmt.Errorf("failed to create the image %q: %w", imgName, err) 226 } 227 imgUUID := *respi.Metadata.UUID 228 229 if taskUUID, ok := respi.Status.ExecutionContext.TaskUUID.(string); ok { 230 logrus.Infof("creating the image %s (uuid: %s), taskUUID: %s", imgName, imgUUID, taskUUID) 231 232 // Wait for the image creation task 233 if err = nutanixtypes.WaitForTask(nutanixCl.V3, taskUUID); err != nil { 234 err = fmt.Errorf("failed to create the image %s (uuid: %s), taskUUID: %s: %w", imgName, imgUUID, taskUUID, err) 235 } else { 236 logrus.Infof("created the image %s (uuid: %s). used_time %v", imgName, imgUUID, time.Since(t1)) 237 } 238 } else { 239 err = fmt.Errorf("failed to convert the task UUID %v to string", respi.Status.ExecutionContext.TaskUUID) 240 } 241 242 return imgUUID, err 243 } 244 245 // uploadImageData upload the image data from the specified file path to the image object in PC. 246 func uploadImageData(ctx context.Context, nutanixCl *nutanixclientv3.Client, imgName, imgUUID, imgPath string) error { 247 // upload the image data. 248 logrus.Infof("preparing to upload the image %s (uuid: %s) data from file %s", imgName, imgUUID, imgPath) 249 t1 := time.Now() 250 err := nutanixCl.V3.UploadImage(ctx, imgUUID, imgPath) 251 if err != nil { 252 return fmt.Errorf("failed to upload the image data %q from filepath %s: %w used_time %v", imgName, imgPath, err, time.Since(t1)) 253 } 254 logrus.Infof("uploading the image %s data. used_time %v", imgName, time.Since(t1)) 255 256 // wait for the image data uploading task to complete. 257 respb, err := nutanixCl.V3.GetImage(ctx, imgUUID) 258 if err != nil { 259 return fmt.Errorf("failed to get the image %q. %w", imgName, err) 260 } 261 262 if taskUUIDs, ok := respb.Status.ExecutionContext.TaskUUID.([]interface{}); ok { 263 tUUIDs := []string{} 264 for _, tUUID := range taskUUIDs { 265 if tUUIDstr, ok := tUUID.(string); ok { 266 tUUIDs = append(tUUIDs, tUUIDstr) 267 } 268 } 269 logrus.Infof("waiting for the image data uploading task to complete, taskUUIDs: %v", tUUIDs) 270 if err = nutanixtypes.WaitForTasks(nutanixCl.V3, tUUIDs); err != nil { 271 return fmt.Errorf("failed to upload the bootstrap image data %q from filepath %s: %w", imgName, imgPath, err) 272 } 273 } else { 274 return fmt.Errorf("failed to convert the taskUUIDs %v to array", respb.Status.ExecutionContext.TaskUUID) 275 } 276 277 return nil 278 } 279 280 // deleteImage deletes the image object with the given uuid in PC. 281 func deleteImage(ctx context.Context, nutanixCl *nutanixclientv3.Client, imgUUID string) error { 282 logrus.Infof("preparing to delete the image with uuid %s", imgUUID) 283 284 respd, err := nutanixCl.V3.DeleteImage(ctx, imgUUID) 285 if err != nil { 286 return fmt.Errorf("failed to delete the image with uuid %s: %w", imgUUID, err) 287 } 288 289 if taskUUID, ok := respd.Status.ExecutionContext.TaskUUID.(string); ok { 290 logrus.Infof("deleting the image with uuid %s, taskUUID: %s", imgUUID, taskUUID) 291 292 // Wait till the image deletion task is successed. 293 if err = nutanixtypes.WaitForTask(nutanixCl.V3, taskUUID); err != nil { 294 return fmt.Errorf("failed to delete the image with uuid: %s, taskUUID: %s: %w", imgUUID, taskUUID, err) 295 } 296 } else { 297 return fmt.Errorf("failed to convert the task UUID %v to string", respd.Status.ExecutionContext.TaskUUID) 298 } 299 300 return nil 301 }