github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/nutanix/nutanix.go (about)

     1  // Package nutanix collects Nutanix-specific configuration.
     2  package nutanix
     3  
     4  import (
     5  	"context"
     6  	"fmt"
     7  	"sort"
     8  	"strconv"
     9  	"time"
    10  
    11  	"github.com/AlecAivazis/survey/v2"
    12  	nutanixclient "github.com/nutanix-cloud-native/prism-go-client"
    13  	nutanixclientv3 "github.com/nutanix-cloud-native/prism-go-client/v3"
    14  	"github.com/pkg/errors"
    15  	"github.com/sirupsen/logrus"
    16  
    17  	"github.com/openshift/installer/pkg/types/nutanix"
    18  	nutanixtypes "github.com/openshift/installer/pkg/types/nutanix"
    19  	"github.com/openshift/installer/pkg/validate"
    20  )
    21  
    22  // PrismCentralClient wraps a Nutanix V3 client
    23  type PrismCentralClient struct {
    24  	PrismCentral string
    25  	Username     string
    26  	Password     string
    27  	Port         string
    28  	V3Client     *nutanixclientv3.Client
    29  }
    30  
    31  // Platform collects Nutanix-specific configuration.
    32  func Platform() (*nutanix.Platform, error) {
    33  	nutanixClient, err := getClients()
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  
    38  	portNum, err := strconv.Atoi(nutanixClient.Port)
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  
    43  	pc := nutanixtypes.PrismCentral{
    44  		Endpoint: nutanix.PrismEndpoint{
    45  			Address: nutanixClient.PrismCentral,
    46  			Port:    int32(portNum),
    47  		},
    48  		Username: nutanixClient.Username,
    49  		Password: nutanixClient.Password,
    50  	}
    51  
    52  	ctx := context.TODO()
    53  	v3Client := nutanixClient.V3Client
    54  	pe, err := getPrismElement(ctx, v3Client)
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  	pe.Endpoint.Port = int32(portNum)
    59  
    60  	subnetUUID, err := getSubnet(ctx, v3Client, pe.UUID)
    61  	if err != nil {
    62  		return nil, err
    63  	}
    64  
    65  	apiVIP, ingressVIP, err := getVIPs()
    66  	if err != nil {
    67  		return nil, errors.Wrap(err, "failed to get VIPs")
    68  	}
    69  
    70  	platform := &nutanix.Platform{
    71  		PrismCentral:  pc,
    72  		PrismElements: []nutanixtypes.PrismElement{*pe},
    73  		SubnetUUIDs:   []string{subnetUUID},
    74  		APIVIPs:       []string{apiVIP},
    75  		IngressVIPs:   []string{ingressVIP},
    76  	}
    77  	return platform, nil
    78  
    79  }
    80  
    81  // getClients() surveys the user for username, password, port & prism central.
    82  // Validation on the three fields is performed by creating a client.
    83  // If creating the client fails, an error is returned.
    84  func getClients() (*PrismCentralClient, error) {
    85  	var prismCentral, port, username, password string
    86  	if err := survey.Ask([]*survey.Question{
    87  		{
    88  			Prompt: &survey.Input{
    89  				Message: "Prism Central",
    90  				Help:    "The domain name or IP address of the Prism Central to be used for installation.",
    91  			},
    92  			Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error {
    93  				return validate.Host(ans.(string))
    94  			}),
    95  		},
    96  	}, &prismCentral); err != nil {
    97  		return nil, errors.Wrap(err, "failed UserInput")
    98  	}
    99  
   100  	if err := survey.Ask([]*survey.Question{
   101  		{
   102  			Prompt: &survey.Input{
   103  				Message: "Port",
   104  				Help:    "The port used to login to Prism Central.",
   105  				Default: "9440",
   106  			},
   107  			Validate: survey.Required,
   108  		},
   109  	}, &port); err != nil {
   110  		return nil, errors.Wrap(err, "failed UserInput")
   111  	}
   112  
   113  	if err := survey.Ask([]*survey.Question{
   114  		{
   115  			Prompt: &survey.Input{
   116  				Message: "Username",
   117  				Help:    "The username to login to the Prism Central.",
   118  			},
   119  			Validate: survey.Required,
   120  		},
   121  	}, &username); err != nil {
   122  		return nil, errors.Wrap(err, "failed UserInput")
   123  	}
   124  
   125  	if err := survey.Ask([]*survey.Question{
   126  		{
   127  			Prompt: &survey.Password{
   128  				Message: "Password",
   129  				Help:    "The password to login to Prism Central.",
   130  			},
   131  			Validate: survey.Required,
   132  		},
   133  	}, &password); err != nil {
   134  		return nil, errors.Wrap(err, "failed UserInput")
   135  	}
   136  
   137  	// There is a noticeable delay when creating the client, so let the user know what's going on.
   138  	logrus.Infof("Connecting to Prism Central %s", prismCentral)
   139  	clientV3, err := nutanixtypes.CreateNutanixClient(context.TODO(),
   140  		prismCentral,
   141  		port,
   142  		username,
   143  		password,
   144  	)
   145  
   146  	if err != nil {
   147  		return nil, errors.Wrapf(err, "unable to connect to Prism Central %s. Ensure provided information is correct", prismCentral)
   148  	}
   149  
   150  	return &PrismCentralClient{
   151  		PrismCentral: prismCentral,
   152  		Username:     username,
   153  		Password:     password,
   154  		Port:         port,
   155  		V3Client:     clientV3,
   156  	}, nil
   157  }
   158  
   159  func getPrismElement(ctx context.Context, client *nutanixclientv3.Client) (*nutanixtypes.PrismElement, error) {
   160  	ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
   161  	defer cancel()
   162  
   163  	pe := &nutanixtypes.PrismElement{}
   164  	emptyFilter := ""
   165  	pesAll, err := client.V3.ListAllCluster(ctx, emptyFilter)
   166  	if err != nil {
   167  		return nil, errors.Wrap(err, "unable to list prism element clusters")
   168  	}
   169  	pes := pesAll.Entities
   170  
   171  	if len(pes) == 0 {
   172  		return nil, errors.New("did not find any prism element clusters")
   173  	}
   174  
   175  	if len(pes) == 1 {
   176  		pe.UUID = *pes[0].Metadata.UUID
   177  		pe.Endpoint.Address = *pes[0].Spec.Resources.Network.ExternalIP
   178  		logrus.Infof("Defaulting to only available prism element (cluster): %s", *pes[0].Spec.Name)
   179  		return pe, nil
   180  	}
   181  
   182  	pesMap := make(map[string]*nutanixclientv3.ClusterIntentResponse)
   183  	var peChoices []string
   184  	for _, p := range pes {
   185  		n := *p.Spec.Name
   186  		pesMap[n] = p
   187  		peChoices = append(peChoices, n)
   188  	}
   189  
   190  	var selectedPe string
   191  	if err := survey.Ask([]*survey.Question{
   192  		{
   193  			Prompt: &survey.Select{
   194  				Message: "Prism Element",
   195  				Options: peChoices,
   196  				Help:    "The Prism Element to be used for installation.",
   197  			},
   198  			Validate: survey.Required,
   199  		},
   200  	}, &selectedPe); err != nil {
   201  		return nil, errors.Wrap(err, "failed UserInput")
   202  	}
   203  
   204  	pe.UUID = *pesMap[selectedPe].Metadata.UUID
   205  	pe.Endpoint.Address = *pesMap[selectedPe].Spec.Resources.Network.ExternalIP
   206  	return pe, nil
   207  
   208  }
   209  
   210  func getSubnet(ctx context.Context, client *nutanixclientv3.Client, peUUID string) (string, error) {
   211  	ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
   212  	defer cancel()
   213  
   214  	emptyFilter := ""
   215  	emptyClientFilters := make([]*nutanixclient.AdditionalFilter, 0)
   216  	subnetsAll, err := client.V3.ListAllSubnet(ctx, emptyFilter, emptyClientFilters)
   217  	if err != nil {
   218  		return "", errors.Wrap(err, "unable to list subnets")
   219  	}
   220  
   221  	subnets := subnetsAll.Entities
   222  
   223  	// API returns an error when no results, but let's leave this in to be defensive.
   224  	if len(subnets) == 0 {
   225  		return "", errors.New("did not find any subnets")
   226  	}
   227  	if len(subnets) == 1 {
   228  		n := *subnets[0].Spec.Name
   229  		u := *subnets[0].Metadata.UUID
   230  		logrus.Infof("Defaulting to only available network: %s", n)
   231  		return u, nil
   232  	}
   233  
   234  	subnetUUIDs := make(map[string]string)
   235  	var subnetChoices []string
   236  	for _, subnet := range subnets {
   237  		// some subnet types (e.g. VPC overlays) do not come with a cluster reference; we don't need to check them
   238  		if subnet.Spec.ClusterReference == nil || (subnet.Spec.ClusterReference.UUID != nil && *subnet.Spec.ClusterReference.UUID == peUUID) {
   239  			n := *subnet.Spec.Name
   240  			subnetUUIDs[n] = *subnet.Metadata.UUID
   241  			subnetChoices = append(subnetChoices, n)
   242  		}
   243  	}
   244  	if len(subnetChoices) == 0 {
   245  		return "", errors.New(fmt.Sprintf("could not find any subnets linked to Prism Element with UUID %s", peUUID))
   246  	}
   247  	sort.Strings(subnetChoices)
   248  
   249  	var selectedSubnet string
   250  	if err := survey.Ask([]*survey.Question{
   251  		{
   252  			Prompt: &survey.Select{
   253  				Message: "Subnet",
   254  				Options: subnetChoices,
   255  				Help:    "The subnet to be used for installation.",
   256  			},
   257  			Validate: survey.Required,
   258  		},
   259  	}, &selectedSubnet); err != nil {
   260  		return "", errors.Wrap(err, "failed UserInput")
   261  	}
   262  
   263  	return subnetUUIDs[selectedSubnet], nil
   264  }
   265  
   266  func getVIPs() (string, string, error) {
   267  	var apiVIP, ingressVIP string
   268  
   269  	//TODO: Add support to specify multiple VIPs (-> dual-stack)
   270  	if err := survey.Ask([]*survey.Question{
   271  		{
   272  			Prompt: &survey.Input{
   273  				Message: "Virtual IP Address for API",
   274  				Help:    "The VIP to be used for the OpenShift API.",
   275  			},
   276  			Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error {
   277  				return validate.IP((ans).(string))
   278  			}),
   279  		},
   280  	}, &apiVIP); err != nil {
   281  		return "", "", errors.Wrap(err, "failed UserInput")
   282  	}
   283  
   284  	if err := survey.Ask([]*survey.Question{
   285  		{
   286  			Prompt: &survey.Input{
   287  				Message: "Virtual IP Address for Ingress",
   288  				Help:    "The VIP to be used for ingress to the cluster.",
   289  			},
   290  			Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error {
   291  				if apiVIP == (ans.(string)) {
   292  					return fmt.Errorf("%q should not be equal to the Virtual IP address for the API", ans.(string))
   293  				}
   294  				return validate.IP((ans).(string))
   295  			}),
   296  		},
   297  	}, &ingressVIP); err != nil {
   298  		return "", "", errors.Wrap(err, "failed UserInput")
   299  	}
   300  
   301  	return apiVIP, ingressVIP, nil
   302  }