github.com/openshift/installer@v1.4.17/pkg/tfvars/openstack/openstack.go (about) 1 // Package openstack contains OpenStack-specific Terraform-variable logic. 2 package openstack 3 4 import ( 5 "context" 6 "encoding/json" 7 "fmt" 8 9 "github.com/gophercloud/gophercloud/v2" 10 "github.com/gophercloud/gophercloud/v2/openstack" 11 "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" 12 13 configv1 "github.com/openshift/api/config/v1" 14 machinev1alpha1 "github.com/openshift/api/machine/v1alpha1" 15 "github.com/openshift/installer/pkg/asset/installconfig" 16 "github.com/openshift/installer/pkg/asset/machines" 17 "github.com/openshift/installer/pkg/rhcos" 18 types_openstack "github.com/openshift/installer/pkg/types/openstack" 19 openstackdefaults "github.com/openshift/installer/pkg/types/openstack/defaults" 20 ) 21 22 // TFVars generates OpenStack-specific Terraform variables. 23 func TFVars( 24 ctx context.Context, 25 installConfig *installconfig.InstallConfig, 26 mastersAsset *machines.Master, 27 workersAsset *machines.Worker, 28 baseImage string, 29 clusterID *installconfig.ClusterID, 30 bootstrapIgn string, 31 ) ([]byte, error) { 32 var ( 33 cloud = installConfig.Config.Platform.OpenStack.Cloud 34 mastermpool = installConfig.Config.ControlPlane.Platform.OpenStack 35 defaultmpool = installConfig.Config.OpenStack.DefaultMachinePlatform 36 ) 37 38 conn, err := openstackdefaults.NewServiceClient(ctx, "network", openstackdefaults.DefaultClientOpts(cloud)) 39 if err != nil { 40 return nil, fmt.Errorf("failed to build an OpenStack service client: %w", err) 41 } 42 43 var masterSpecs []*machinev1alpha1.OpenstackProviderSpec 44 { 45 masters, err := mastersAsset.Machines() 46 if err != nil { 47 return nil, err 48 } 49 50 for _, master := range masters { 51 masterSpecs = append(masterSpecs, master.Spec.ProviderSpec.Value.Object.(*machinev1alpha1.OpenstackProviderSpec)) 52 } 53 } 54 55 var workerSpecs []*machinev1alpha1.OpenstackProviderSpec 56 { 57 workers, err := workersAsset.MachineSets() 58 if err != nil { 59 return nil, err 60 } 61 62 for _, worker := range workers { 63 workerSpecs = append(workerSpecs, worker.Spec.Template.Spec.ProviderSpec.Value.Object.(*machinev1alpha1.OpenstackProviderSpec)) 64 } 65 } 66 67 var workermpool *types_openstack.MachinePool 68 if len(installConfig.Config.Compute) > 0 { 69 // Only considering the first Compute machinepool here, because 70 // the current Installer implementation allows for one only. 71 // 72 // This validation code[1] errors if the pool is not named 73 // "worker", and also errors in case of duplicate names, 74 // factually rendering impossible to have two machinepools in 75 // the install-config YAML array. 76 // 77 // [1]: https://github.com/openshift/installer/blob/252facf5e6e1238ee60b5f78607214e8691a3eab/pkg/types/validation/installconfig.go#L404-L410 78 if len(installConfig.Config.Compute) > 1 { 79 panic("Multiple machine-pools are currently not supported by the OpenShift installer on OpenStack platform") 80 } 81 workermpool = installConfig.Config.Compute[0].Platform.OpenStack 82 } 83 84 var userManagedLoadBalancer bool 85 if lb := installConfig.Config.Platform.OpenStack.LoadBalancer; lb != nil && lb.Type == configv1.LoadBalancerTypeUserManaged { 86 userManagedLoadBalancer = true 87 } 88 89 // computeAvailabilityZones is a slice where each index targets a master. 90 computeAvailabilityZones := make([]string, len(masterSpecs)) 91 for i := range computeAvailabilityZones { 92 computeAvailabilityZones[i] = masterSpecs[i].AvailabilityZone 93 } 94 95 // storageAvailabilityZones is a slice where each index targets a master. 96 storageAvailabilityZones := make([]string, len(masterSpecs)) 97 for i := range storageAvailabilityZones { 98 if masterSpecs[i].RootVolume != nil { 99 storageAvailabilityZones[i] = masterSpecs[i].RootVolume.Zone 100 } 101 } 102 103 // storageVolumeTypes is a slice where each index targets a master. 104 storageVolumeTypes := make([]string, len(masterSpecs)) 105 for i := range storageVolumeTypes { 106 if masterSpecs[i].RootVolume != nil { 107 storageVolumeTypes[i] = masterSpecs[i].RootVolume.VolumeType 108 } 109 } 110 111 // If baseImage is a URL, the corresponding image will be uploaded to 112 // Glance in the PreTerraform hook of the Cluster asset. 113 imageName, _ := rhcos.GenerateOpenStackImageName(baseImage, clusterID.InfraID) 114 115 serviceCatalog, err := getServiceCatalog(ctx, cloud) 116 if err != nil { 117 return nil, fmt.Errorf("could not retrieve service catalog: %w", err) 118 } 119 120 octaviaSupport, err := isOctaviaSupported(serviceCatalog) 121 if err != nil { 122 return nil, err 123 } 124 125 var rootVolumeSize int 126 if rootVolume := masterSpecs[0].RootVolume; rootVolume != nil { 127 rootVolumeSize = rootVolume.Size 128 } 129 130 masterServerGroupPolicy := GetServerGroupPolicy(mastermpool, defaultmpool) 131 masterServerGroupName := masterSpecs[0].ServerGroupName 132 if masterSpecs[0].ServerGroupID != "" { 133 return nil, fmt.Errorf("the field ServerGroupID is not implemented in the Installer. Please use ServerGroupName for automatic creation of the Control Plane server group") 134 } 135 136 workerServerGroupPolicy := GetServerGroupPolicy(workermpool, defaultmpool) 137 var workerServerGroupNames []string 138 { 139 for _, workerConfig := range workerSpecs { 140 workerServerGroupNames = append(workerServerGroupNames, workerConfig.ServerGroupName) 141 if workerConfig.ServerGroupID != "" { 142 return nil, fmt.Errorf("the field ServerGroupID is not implemented in the Installer. Please use ServerGroupName for automatic creation of the Compute server group") 143 } 144 } 145 } 146 147 var additionalNetworkIDs []string 148 if mastermpool != nil { 149 additionalNetworkIDs = mastermpool.AdditionalNetworkIDs 150 } 151 152 // defaultMachinesPort carries the machine subnets and the network. 153 var defaultMachinesPort *terraformPort 154 if controlPlanePort := installConfig.Config.Platform.OpenStack.ControlPlanePort; controlPlanePort != nil { 155 port, err := portTargetToTerraformPort(ctx, conn, *controlPlanePort) 156 if err != nil { 157 return nil, fmt.Errorf("failed to resolve portTarget :%w", err) 158 } 159 defaultMachinesPort = &port 160 } 161 162 // machinesPorts defines the primary port for a master. A nil value 163 // signals Terraform to fill in the blank with the network it creates. 164 // Each slice index targets a master. 165 machinesPorts := make([]*terraformPort, len(masterSpecs)) 166 167 // additionalPorts translates non-control-plane 168 // `failureDomain.portTarget` information in Terraform-understandable 169 // syntax. Each slice index targets a master. 170 additionalPorts := make([][]terraformPort, len(masterSpecs)) 171 for i := range masterSpecs { 172 // Assign a slice to each master's index, no matter what. 173 // Terraform expects each master to get an array, empty or otherwise. 174 additionalPorts[i] = []terraformPort{} 175 machinesPorts[i] = defaultMachinesPort 176 } 177 178 var additionalSecurityGroupIDs []string 179 if mastermpool != nil { 180 additionalSecurityGroupIDs = mastermpool.AdditionalSecurityGroupIDs 181 } 182 183 return json.MarshalIndent(struct { 184 BaseImageName string `json:"openstack_base_image_name,omitempty"` 185 ExternalNetwork string `json:"openstack_external_network,omitempty"` 186 Cloud string `json:"openstack_credentials_cloud,omitempty"` 187 FlavorName string `json:"openstack_master_flavor_name,omitempty"` 188 APIFloatingIP string `json:"openstack_api_floating_ip,omitempty"` 189 IngressFloatingIP string `json:"openstack_ingress_floating_ip,omitempty"` 190 APIVIPs []string `json:"openstack_api_int_ips,omitempty"` 191 IngressVIPs []string `json:"openstack_ingress_ips,omitempty"` 192 OctaviaSupport bool `json:"openstack_octavia_support,omitempty"` 193 RootVolumeSize int `json:"openstack_master_root_volume_size,omitempty"` 194 BootstrapShim string `json:"openstack_bootstrap_shim_ignition,omitempty"` 195 ExternalDNS []string `json:"openstack_external_dns,omitempty"` 196 MasterServerGroupName string `json:"openstack_master_server_group_name,omitempty"` 197 MasterServerGroupPolicy types_openstack.ServerGroupPolicy `json:"openstack_master_server_group_policy"` 198 WorkerServerGroupNames []string `json:"openstack_worker_server_group_names,omitempty"` 199 WorkerServerGroupPolicy types_openstack.ServerGroupPolicy `json:"openstack_worker_server_group_policy"` 200 AdditionalNetworkIDs []string `json:"openstack_additional_network_ids,omitempty"` 201 AdditionalPorts [][]terraformPort `json:"openstack_additional_ports"` 202 AdditionalSecurityGroupIDs []string `json:"openstack_master_extra_sg_ids,omitempty"` 203 DefaultMachinesPort *terraformPort `json:"openstack_default_machines_port,omitempty"` 204 MachinesPorts []*terraformPort `json:"openstack_machines_ports"` 205 MasterAvailabilityZones []string `json:"openstack_master_availability_zones,omitempty"` 206 MasterRootVolumeAvailabilityZones []string `json:"openstack_master_root_volume_availability_zones,omitempty"` 207 MasterRootVolumeTypes []string `json:"openstack_master_root_volume_types,omitempty"` 208 UserManagedLoadBalancer bool `json:"openstack_user_managed_load_balancer"` 209 }{ 210 BaseImageName: imageName, 211 ExternalNetwork: installConfig.Config.Platform.OpenStack.ExternalNetwork, 212 Cloud: cloud, 213 FlavorName: masterSpecs[0].Flavor, 214 APIFloatingIP: installConfig.Config.Platform.OpenStack.APIFloatingIP, 215 IngressFloatingIP: installConfig.Config.Platform.OpenStack.IngressFloatingIP, 216 APIVIPs: installConfig.Config.Platform.OpenStack.APIVIPs, 217 IngressVIPs: installConfig.Config.Platform.OpenStack.IngressVIPs, 218 OctaviaSupport: octaviaSupport, 219 RootVolumeSize: rootVolumeSize, 220 BootstrapShim: bootstrapIgn, 221 ExternalDNS: installConfig.Config.Platform.OpenStack.ExternalDNS, 222 MasterServerGroupName: masterServerGroupName, 223 MasterServerGroupPolicy: masterServerGroupPolicy, 224 WorkerServerGroupNames: workerServerGroupNames, 225 WorkerServerGroupPolicy: workerServerGroupPolicy, 226 AdditionalNetworkIDs: additionalNetworkIDs, 227 AdditionalPorts: additionalPorts, 228 AdditionalSecurityGroupIDs: additionalSecurityGroupIDs, 229 DefaultMachinesPort: defaultMachinesPort, 230 MachinesPorts: machinesPorts, 231 MasterAvailabilityZones: computeAvailabilityZones, 232 MasterRootVolumeAvailabilityZones: storageAvailabilityZones, 233 MasterRootVolumeTypes: storageVolumeTypes, 234 UserManagedLoadBalancer: userManagedLoadBalancer, 235 }, "", " ") 236 } 237 238 // getServiceCatalog fetches OpenStack service catalog with service endpoints 239 func getServiceCatalog(ctx context.Context, cloud string) (*tokens.ServiceCatalog, error) { 240 conn, err := openstackdefaults.NewServiceClient(ctx, "identity", openstackdefaults.DefaultClientOpts(cloud)) 241 if err != nil { 242 return nil, err 243 } 244 245 authResult := conn.GetAuthResult() 246 auth, ok := authResult.(tokens.CreateResult) 247 if !ok { 248 return nil, fmt.Errorf("unable to extract service catalog") 249 } 250 251 serviceCatalog, err := auth.ExtractServiceCatalog() 252 if err != nil { 253 return nil, err 254 } 255 256 return serviceCatalog, nil 257 } 258 259 func isOctaviaSupported(serviceCatalog *tokens.ServiceCatalog) (bool, error) { 260 _, err := openstack.V3EndpointURL(serviceCatalog, gophercloud.EndpointOpts{ 261 Type: "load-balancer", 262 Name: "octavia", 263 Availability: gophercloud.AvailabilityPublic, 264 }) 265 if err != nil { 266 if _, ok := err.(*gophercloud.ErrEndpointNotFound); ok { 267 return false, nil 268 } 269 return false, err 270 } 271 272 return true, nil 273 } 274 275 // GetServerGroupPolicy returns the server group policy set in the given machine-pool, or in the default one, or falls back to soft-anti-affinity. 276 func GetServerGroupPolicy(machinePool, defaultMachinePool *types_openstack.MachinePool) types_openstack.ServerGroupPolicy { 277 if machinePool != nil && machinePool.ServerGroupPolicy.IsSet() { 278 return machinePool.ServerGroupPolicy 279 } 280 if defaultMachinePool != nil && defaultMachinePool.ServerGroupPolicy.IsSet() { 281 return defaultMachinePool.ServerGroupPolicy 282 } 283 return types_openstack.SGPolicySoftAntiAffinity 284 }