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 }