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 }