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  }