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

     1  // Package vsphere collects vSphere-specific configuration.
     2  package vsphere
     3  
     4  import (
     5  	"context"
     6  	"fmt"
     7  	"sort"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/AlecAivazis/survey/v2"
    12  	"github.com/pkg/errors"
    13  	"github.com/sirupsen/logrus"
    14  	"github.com/vmware/govmomi/vapi/rest"
    15  	"github.com/vmware/govmomi/vim25"
    16  	"k8s.io/apimachinery/pkg/util/sets"
    17  
    18  	"github.com/openshift/installer/pkg/types/vsphere"
    19  	"github.com/openshift/installer/pkg/validate"
    20  )
    21  
    22  const root = "/..."
    23  
    24  // vCenterClient contains the login info/creds and client for the vCenter.
    25  // They are contained in a single struct to facilitate client creation
    26  // serving as validation of the vCenter, username, and password fields.
    27  type vCenterClient struct {
    28  	VCenter    string
    29  	Username   string
    30  	Password   string
    31  	Client     *vim25.Client
    32  	RestClient *rest.Client
    33  	Logout     ClientLogout
    34  }
    35  
    36  // Platform collects vSphere-specific configuration.
    37  func Platform() (*vsphere.Platform, error) {
    38  	vCenter, err := getClients()
    39  	if err != nil {
    40  		return nil, err
    41  	}
    42  	defer vCenter.Logout()
    43  
    44  	finder := NewFinder(vCenter.Client)
    45  	ctx := context.TODO()
    46  
    47  	dc, dcPath, err := getDataCenter(ctx, finder, vCenter.Client)
    48  	if err != nil {
    49  		return nil, err
    50  	}
    51  
    52  	cluster, err := getCluster(ctx, dcPath, finder, vCenter.Client)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	datastore, err := getDataStore(ctx, dcPath, finder, vCenter.Client)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  
    62  	network, err := getNetwork(ctx, dc, cluster, finder, vCenter.Client)
    63  	if err != nil {
    64  		return nil, err
    65  	}
    66  
    67  	apiVIP, ingressVIP, err := getVIPs()
    68  	if err != nil {
    69  		return nil, errors.Wrap(err, "failed to get VIPs")
    70  	}
    71  
    72  	failureDomain := vsphere.FailureDomain{
    73  		Name:   "generated-failure-domain",
    74  		Zone:   "generated-zone",
    75  		Region: "generated-region",
    76  		Server: vCenter.VCenter,
    77  		Topology: vsphere.Topology{
    78  			Datacenter:     dc,
    79  			ComputeCluster: cluster,
    80  			Datastore:      datastore,
    81  			Networks:       []string{network},
    82  		},
    83  	}
    84  
    85  	vcenter := vsphere.VCenter{
    86  		Server:      vCenter.VCenter,
    87  		Port:        443,
    88  		Username:    vCenter.Username,
    89  		Password:    vCenter.Password,
    90  		Datacenters: []string{dc},
    91  	}
    92  
    93  	platform := &vsphere.Platform{
    94  		VCenters:       []vsphere.VCenter{vcenter},
    95  		FailureDomains: []vsphere.FailureDomain{failureDomain},
    96  		APIVIPs:        []string{apiVIP},
    97  		IngressVIPs:    []string{ingressVIP},
    98  	}
    99  
   100  	return platform, nil
   101  }
   102  
   103  // getClients() surveys the user for username, password, & vcenter.
   104  // Validation on the three fields is performed by creating a client.
   105  // If creating the client fails, an error is returned.
   106  func getClients() (*vCenterClient, error) {
   107  	var vcenter, username, password string
   108  
   109  	if err := survey.Ask([]*survey.Question{
   110  		{
   111  			Prompt: &survey.Input{
   112  				Message: "vCenter",
   113  				Help:    "The domain name or IP address of the vCenter to be used for installation.",
   114  			},
   115  			Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error {
   116  				return validate.Host(ans.(string))
   117  			}),
   118  		},
   119  	}, &vcenter); err != nil {
   120  		return nil, errors.Wrap(err, "failed UserInput")
   121  	}
   122  
   123  	if err := survey.Ask([]*survey.Question{
   124  		{
   125  			Prompt: &survey.Input{
   126  				Message: "Username",
   127  				Help:    "The username to login to the vCenter.",
   128  			},
   129  			Validate: survey.Required,
   130  		},
   131  	}, &username); err != nil {
   132  		return nil, errors.Wrap(err, "failed UserInput")
   133  	}
   134  
   135  	if err := survey.Ask([]*survey.Question{
   136  		{
   137  			Prompt: &survey.Password{
   138  				Message: "Password",
   139  				Help:    "The password to login to the vCenter.",
   140  			},
   141  			Validate: survey.Required,
   142  		},
   143  	}, &password); err != nil {
   144  		return nil, errors.Wrap(err, "failed UserInput")
   145  	}
   146  
   147  	// There is a noticeable delay when creating the client, so let the user know what's going on.
   148  	logrus.Infof("Connecting to vCenter %s", vcenter)
   149  	vim25Client, restClient, logoutFunction, err := CreateVSphereClients(context.TODO(),
   150  		vcenter,
   151  		username,
   152  		password)
   153  
   154  	// Survey does not allow validation of groups of input
   155  	// so we perform our own validation.
   156  	if err != nil {
   157  		return nil, errors.Wrapf(err, "unable to connect to vCenter %s. Ensure provided information is correct and client certs have been added to system trust", vcenter)
   158  	}
   159  
   160  	return &vCenterClient{
   161  		VCenter:    vcenter,
   162  		Username:   username,
   163  		Password:   password,
   164  		Client:     vim25Client,
   165  		RestClient: restClient,
   166  		Logout:     logoutFunction,
   167  	}, nil
   168  }
   169  
   170  // getDataCenter searches the root for all datacenters and, if there is more than one, lets the user select
   171  // one to use for installation. Returns the name and path of the selected datacenter. The name is used
   172  // to generate the install config and the path is used to determine the options for cluster, datastore and network.
   173  func getDataCenter(ctx context.Context, finder Finder, client *vim25.Client) (string, string, error) {
   174  	ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
   175  	defer cancel()
   176  
   177  	dataCenters, err := finder.DatacenterList(ctx, root)
   178  	if err != nil {
   179  		return "", "", errors.Wrap(err, "unable to list datacenters")
   180  	}
   181  
   182  	// API returns an error when no results, but let's leave this in to be defensive.
   183  	if len(dataCenters) == 0 {
   184  		return "", "", errors.New("did not find any datacenters")
   185  	}
   186  	if len(dataCenters) == 1 {
   187  		name := strings.TrimPrefix(dataCenters[0].InventoryPath, "/")
   188  		logrus.Infof("Defaulting to only available datacenter: %s", name)
   189  		return name, dataCenters[0].InventoryPath, nil
   190  	}
   191  
   192  	dataCenterPaths := make(map[string]string)
   193  	dataCenterChoices := make([]string, 0, len(dataCenters))
   194  	for _, dc := range dataCenters {
   195  		name := strings.TrimPrefix(dc.InventoryPath, "/")
   196  		dataCenterPaths[name] = dc.InventoryPath
   197  		dataCenterChoices = append(dataCenterChoices, name)
   198  	}
   199  	sort.Strings(dataCenterChoices)
   200  
   201  	var selectedDataCenter string
   202  	if err := survey.Ask([]*survey.Question{
   203  		{
   204  			Prompt: &survey.Select{
   205  				Message: "Datacenter",
   206  				Options: dataCenterChoices,
   207  				Help:    "The Datacenter to be used for installation.",
   208  			},
   209  			Validate: survey.Required,
   210  		},
   211  	}, &selectedDataCenter); err != nil {
   212  		return "", "", errors.Wrap(err, "failed UserInput")
   213  	}
   214  
   215  	return selectedDataCenter, dataCenterPaths[selectedDataCenter], nil
   216  }
   217  
   218  func getCluster(ctx context.Context, path string, finder Finder, client *vim25.Client) (string, error) {
   219  	ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
   220  	defer cancel()
   221  
   222  	clusters, err := finder.ClusterComputeResourceList(ctx, formatPath(path))
   223  	if err != nil {
   224  		return "", errors.Wrap(err, "unable to list clusters")
   225  	}
   226  
   227  	// API returns an error when no results, but let's leave this in to be defensive.
   228  	if len(clusters) == 0 {
   229  		return "", errors.New("did not find any clusters")
   230  	}
   231  	if len(clusters) == 1 {
   232  		logrus.Infof("Defaulting to only available cluster: %s", clusters[0].InventoryPath)
   233  		return clusters[0].InventoryPath, nil
   234  	}
   235  
   236  	clusterChoices := make([]string, 0, len(clusters))
   237  	for _, c := range clusters {
   238  		clusterChoices = append(clusterChoices, c.InventoryPath)
   239  	}
   240  	sort.Strings(clusterChoices)
   241  
   242  	var selectedcluster string
   243  	if err := survey.Ask([]*survey.Question{
   244  		{
   245  			Prompt: &survey.Select{
   246  				Message: "Cluster",
   247  				Options: clusterChoices,
   248  				Help:    "The cluster to be used for installation.",
   249  			},
   250  			Validate: survey.Required,
   251  		},
   252  	}, &selectedcluster); err != nil {
   253  		return "", errors.Wrap(err, "failed UserInput")
   254  	}
   255  
   256  	return selectedcluster, nil
   257  }
   258  
   259  func getDataStore(ctx context.Context, path string, finder Finder, client *vim25.Client) (string, error) {
   260  	ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
   261  	defer cancel()
   262  
   263  	dataStores, err := finder.DatastoreList(ctx, formatPath(path))
   264  	if err != nil {
   265  		return "", errors.Wrap(err, "unable to list datastores")
   266  	}
   267  
   268  	// API returns an error when no results, but let's leave this in to be defensive.
   269  	if len(dataStores) == 0 {
   270  		return "", errors.New("did not find any datastores")
   271  	}
   272  	if len(dataStores) == 1 {
   273  		logrus.Infof("Defaulting to only available datastore: %s", dataStores[0].InventoryPath)
   274  		return dataStores[0].InventoryPath, nil
   275  	}
   276  
   277  	dataStoreChoices := make([]string, 0, len(dataStores))
   278  	for _, ds := range dataStores {
   279  		dataStoreChoices = append(dataStoreChoices, ds.InventoryPath)
   280  	}
   281  	sort.Strings(dataStoreChoices)
   282  
   283  	var selectedDataStore string
   284  	if err := survey.Ask([]*survey.Question{
   285  		{
   286  			Prompt: &survey.Select{
   287  				Message: "Default Datastore",
   288  				Options: dataStoreChoices,
   289  				Help:    "The default datastore to be used for installation.",
   290  			},
   291  			Validate: survey.Required,
   292  		},
   293  	}, &selectedDataStore); err != nil {
   294  		return "", errors.Wrap(err, "failed UserInput")
   295  	}
   296  
   297  	return selectedDataStore, nil
   298  }
   299  
   300  func getNetwork(ctx context.Context, datacenter string, cluster string, finder Finder, client *vim25.Client) (string, error) {
   301  	ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
   302  	defer cancel()
   303  
   304  	// Get a list of networks from the previously selected Datacenter and Cluster
   305  	networks, err := GetClusterNetworks(ctx, finder, datacenter, cluster)
   306  	if err != nil {
   307  		return "", errors.Wrap(err, "unable to list networks")
   308  	}
   309  
   310  	// API returns an error when no results, but let's leave this in to be defensive.
   311  	if len(networks) == 0 {
   312  		return "", errors.New("did not find any networks")
   313  	}
   314  	if len(networks) == 1 {
   315  		n, err := GetNetworkName(ctx, client, networks[0])
   316  		if err != nil {
   317  			return "", errors.Wrap(err, "unable to get network name")
   318  		}
   319  		logrus.Infof("Defaulting to only available network: %s", n)
   320  		return n, nil
   321  	}
   322  
   323  	validNetworkTypes := sets.NewString(
   324  		"DistributedVirtualPortgroup",
   325  		"Network",
   326  		"OpaqueNetwork",
   327  	)
   328  
   329  	var networkChoices []string
   330  	for _, network := range networks {
   331  		if validNetworkTypes.Has(network.Reference().Type) {
   332  			// Below results in an API call. Can it be eliminated somehow?
   333  			n, err := GetNetworkName(ctx, client, network)
   334  			if err != nil {
   335  				return "", errors.Wrap(err, "unable to get network name")
   336  			}
   337  			networkChoices = append(networkChoices, n)
   338  		}
   339  	}
   340  	if len(networkChoices) == 0 {
   341  		return "", errors.New("could not find any networks of the type DistributedVirtualPortgroup or Network")
   342  	}
   343  	sort.Strings(networkChoices)
   344  
   345  	var selectednetwork string
   346  	if err := survey.Ask([]*survey.Question{
   347  		{
   348  			Prompt: &survey.Select{
   349  				Message: "Network",
   350  				Options: networkChoices,
   351  				Help:    "The network to be used for installation.",
   352  			},
   353  			Validate: survey.Required,
   354  		},
   355  	}, &selectednetwork); err != nil {
   356  		return "", errors.Wrap(err, "failed UserInput")
   357  	}
   358  
   359  	return selectednetwork, nil
   360  }
   361  
   362  func getVIPs() (string, string, error) {
   363  	var apiVIP, ingressVIP string
   364  
   365  	if err := survey.Ask([]*survey.Question{
   366  		{
   367  			Prompt: &survey.Input{
   368  				Message: "Virtual IP Address for API",
   369  				Help:    "The VIP to be used for the OpenShift API.",
   370  			},
   371  			Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error {
   372  				return validate.IP((ans).(string))
   373  			}),
   374  		},
   375  	}, &apiVIP); err != nil {
   376  		return "", "", errors.Wrap(err, "failed UserInput")
   377  	}
   378  
   379  	if err := survey.Ask([]*survey.Question{
   380  		{
   381  			Prompt: &survey.Input{
   382  				Message: "Virtual IP Address for Ingress",
   383  				Help:    "The VIP to be used for ingress to the cluster.",
   384  			},
   385  			Validate: survey.ComposeValidators(survey.Required, func(ans interface{}) error {
   386  				if apiVIP == (ans.(string)) {
   387  					return fmt.Errorf("%q should not be equal to the Virtual IP address for the API", ans.(string))
   388  				}
   389  				return validate.IP((ans).(string))
   390  			}),
   391  		},
   392  	}, &ingressVIP); err != nil {
   393  		return "", "", errors.Wrap(err, "failed UserInput")
   394  	}
   395  
   396  	return apiVIP, ingressVIP, nil
   397  }
   398  
   399  // formatPath is a helper function that appends "/..." to enable recursive
   400  // find in a root object. For details, see the introduction at:
   401  // https://godoc.org/github.com/vmware/govmomi/find
   402  func formatPath(rootObject string) string {
   403  	return fmt.Sprintf("%s/...", rootObject)
   404  }