1
0
mirror of https://github.com/openshift/installer.git synced 2026-02-05 06:46:36 +01:00
Files
installer/pkg/tfvars/openstack/openstack.go
2023-02-21 09:29:03 -05:00

352 lines
15 KiB
Go

// Package openstack contains OpenStack-specific Terraform-variable logic.
package openstack
import (
"encoding/json"
"fmt"
"os"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
"github.com/gophercloud/utils/openstack/clientconfig"
configv1 "github.com/openshift/api/config/v1"
machinev1alpha1 "github.com/openshift/api/machine/v1alpha1"
"github.com/openshift/installer/pkg/asset/installconfig"
installconfig_openstack "github.com/openshift/installer/pkg/asset/installconfig/openstack"
"github.com/openshift/installer/pkg/asset/machines"
"github.com/openshift/installer/pkg/rhcos"
"github.com/openshift/installer/pkg/types"
types_openstack "github.com/openshift/installer/pkg/types/openstack"
openstackdefaults "github.com/openshift/installer/pkg/types/openstack/defaults"
)
// TFVars generates OpenStack-specific Terraform variables.
func TFVars(
installConfig *installconfig.InstallConfig,
mastersAsset *machines.Master,
workersAsset *machines.Worker,
baseImage string,
clusterID *installconfig.ClusterID,
bootstrapIgn string,
) ([]byte, error) {
var (
cloud = installConfig.Config.Platform.OpenStack.Cloud
mastermpool = installConfig.Config.ControlPlane.Platform.OpenStack
defaultmpool = installConfig.Config.OpenStack.DefaultMachinePlatform
machinesSubnet = installConfig.Config.Platform.OpenStack.MachinesSubnet
)
var userCA string
{
cloud, err := installconfig_openstack.GetSession(installConfig.Config.Platform.OpenStack.Cloud)
if err != nil {
return nil, fmt.Errorf("failed to get cloud config for openstack: %w", err)
}
// Get the ca-cert-bundle key if there is a value for cacert in clouds.yaml
if caPath := cloud.CloudConfig.CACertFile; caPath != "" {
caFile, err := os.ReadFile(caPath)
if err != nil {
return nil, fmt.Errorf("failed to read clouds.yaml ca-cert from disk: %w", err)
}
userCA = string(caFile)
}
}
var masterSpecs []*machinev1alpha1.OpenstackProviderSpec
{
masters, err := mastersAsset.Machines()
if err != nil {
return nil, err
}
for _, master := range masters {
masterSpecs = append(masterSpecs, master.Spec.ProviderSpec.Value.Object.(*machinev1alpha1.OpenstackProviderSpec))
}
}
var workerSpecs []*machinev1alpha1.OpenstackProviderSpec
{
workers, err := workersAsset.MachineSets()
if err != nil {
return nil, err
}
for _, worker := range workers {
workerSpecs = append(workerSpecs, worker.Spec.Template.Spec.ProviderSpec.Value.Object.(*machinev1alpha1.OpenstackProviderSpec))
}
}
var workermpool *types_openstack.MachinePool
if len(installConfig.Config.Compute) > 0 {
// Only considering the first Compute machinepool here, because
// the current Installer implementation allows for one only.
//
// This validation code[1] errors if the pool is not named
// "worker", and also errors in case of duplicate names,
// factually rendering impossible to have two machinepools in
// the install-config YAML array.
//
// [1]: https://github.com/openshift/installer/blob/252facf5e6e1238ee60b5f78607214e8691a3eab/pkg/types/validation/installconfig.go#L404-L410
if len(installConfig.Config.Compute) > 1 {
panic("Multiple machine-pools are currently not supported by the OpenShift installer on OpenStack platform")
}
workermpool = installConfig.Config.Compute[0].Platform.OpenStack
}
var userManagedLoadBalancer bool
if lb := installConfig.Config.Platform.OpenStack.LoadBalancer; lb != nil && lb.Type == configv1.LoadBalancerTypeUserManaged {
userManagedLoadBalancer = true
}
var zones []string
{
seen := make(map[string]struct{})
for _, config := range masterSpecs {
if _, ok := seen[config.AvailabilityZone]; !ok {
zones = append(zones, config.AvailabilityZone)
seen[config.AvailabilityZone] = struct{}{}
}
}
}
var masterRootVolumeAvailabilityZones []string
{
if defaultmpool != nil && defaultmpool.RootVolume != nil {
masterRootVolumeAvailabilityZones = defaultmpool.RootVolume.Zones
}
if mastermpool != nil && mastermpool.RootVolume != nil && mastermpool.RootVolume.Zones != nil {
masterRootVolumeAvailabilityZones = mastermpool.RootVolume.Zones
}
}
// Normally baseImage contains a URL that we will use to create a new Glance image, but for testing
// purposes we also allow to set a custom Glance image name to skip the uploading. Here we check
// whether baseImage is a URL or not. If this is the first case, it means that the image should be
// created by the installer from the URL. Otherwise, it means that we are given the name of the pre-created
// Glance image, which we should use for instances.
imageName, isURL := rhcos.GenerateOpenStackImageName(baseImage, clusterID.InfraID)
if isURL {
// Valid URL -> use baseImage as a URL that will be used to create new Glance image with name "<infraID>-rhcos".
if err := uploadBaseImage(cloud, baseImage, imageName, clusterID.InfraID, installConfig.Config.Platform.OpenStack.ClusterOSImageProperties); err != nil {
return nil, err
}
}
serviceCatalog, err := getServiceCatalog(cloud)
if err != nil {
return nil, fmt.Errorf("could not retrieve service catalog: %w", err)
}
bootstrapShim, err := getBootstrapShim(cloud, clusterID.InfraID, serviceCatalog, installConfig.Config.Proxy, bootstrapIgn, userCA)
if err != nil {
return nil, err
}
octaviaSupport, err := isOctaviaSupported(serviceCatalog)
if err != nil {
return nil, err
}
var rootVolumeSize int
var rootVolumeType string
if rootVolume := masterSpecs[0].RootVolume; rootVolume != nil {
rootVolumeSize = rootVolume.Size
rootVolumeType = rootVolume.VolumeType
}
masterServerGroupPolicy := getServerGroupPolicy(mastermpool, defaultmpool, types_openstack.SGPolicySoftAntiAffinity)
masterServerGroupName := masterSpecs[0].ServerGroupName
if masterSpecs[0].ServerGroupID != "" {
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")
}
workerServerGroupPolicy := getServerGroupPolicy(workermpool, defaultmpool, types_openstack.SGPolicySoftAntiAffinity)
var workerServerGroupNames []string
{
for _, workerConfig := range workerSpecs {
workerServerGroupNames = append(workerServerGroupNames, workerConfig.ServerGroupName)
if workerConfig.ServerGroupID != "" {
return nil, fmt.Errorf("the field ServerGroupID is not implemented in the Installer. Please use ServerGroupName for automatic creation of the Compute server group")
}
}
}
var additionalNetworkIDs []string
if mastermpool != nil {
additionalNetworkIDs = mastermpool.AdditionalNetworkIDs
}
var additionalSecurityGroupIDs []string
if mastermpool != nil {
additionalSecurityGroupIDs = mastermpool.AdditionalSecurityGroupIDs
}
var machinesNetwork string
if machinesSubnet != "" {
machinesNetwork, err = getNetworkFromSubnet(cloud, machinesSubnet)
if err != nil {
return nil, err
}
}
return json.MarshalIndent(struct {
BaseImageName string `json:"openstack_base_image_name,omitempty"`
ExternalNetwork string `json:"openstack_external_network,omitempty"`
Cloud string `json:"openstack_credentials_cloud,omitempty"`
FlavorName string `json:"openstack_master_flavor_name,omitempty"`
APIFloatingIP string `json:"openstack_api_floating_ip,omitempty"`
IngressFloatingIP string `json:"openstack_ingress_floating_ip,omitempty"`
APIVIP string `json:"openstack_api_int_ip,omitempty"`
IngressVIP string `json:"openstack_ingress_ip,omitempty"`
TrunkSupport bool `json:"openstack_trunk_support,omitempty"`
OctaviaSupport bool `json:"openstack_octavia_support,omitempty"`
RootVolumeSize int `json:"openstack_master_root_volume_size,omitempty"`
RootVolumeType string `json:"openstack_master_root_volume_type,omitempty"`
BootstrapShim string `json:"openstack_bootstrap_shim_ignition,omitempty"`
ExternalDNS []string `json:"openstack_external_dns,omitempty"`
MasterServerGroupName string `json:"openstack_master_server_group_name,omitempty"`
MasterServerGroupPolicy types_openstack.ServerGroupPolicy `json:"openstack_master_server_group_policy"`
WorkerServerGroupNames []string `json:"openstack_worker_server_group_names,omitempty"`
WorkerServerGroupPolicy types_openstack.ServerGroupPolicy `json:"openstack_worker_server_group_policy"`
AdditionalNetworkIDs []string `json:"openstack_additional_network_ids,omitempty"`
AdditionalSecurityGroupIDs []string `json:"openstack_master_extra_sg_ids,omitempty"`
MachinesSubnet string `json:"openstack_machines_subnet_id,omitempty"`
MachinesNetwork string `json:"openstack_machines_network_id,omitempty"`
MasterAvailabilityZones []string `json:"openstack_master_availability_zones,omitempty"`
MasterRootVolumeAvailabilityZones []string `json:"openstack_master_root_volume_availability_zones,omitempty"`
UserManagedLoadBalancer bool `json:"openstack_user_managed_load_balancer"`
}{
BaseImageName: imageName,
ExternalNetwork: installConfig.Config.Platform.OpenStack.ExternalNetwork,
Cloud: cloud,
FlavorName: masterSpecs[0].Flavor,
APIFloatingIP: installConfig.Config.Platform.OpenStack.APIFloatingIP,
IngressFloatingIP: installConfig.Config.Platform.OpenStack.IngressFloatingIP,
APIVIP: installConfig.Config.Platform.OpenStack.APIVIPs[0],
IngressVIP: installConfig.Config.Platform.OpenStack.IngressVIPs[0],
TrunkSupport: masterSpecs[0].Trunk,
OctaviaSupport: octaviaSupport,
RootVolumeSize: rootVolumeSize,
RootVolumeType: rootVolumeType,
BootstrapShim: bootstrapShim,
ExternalDNS: installConfig.Config.Platform.OpenStack.ExternalDNS,
MasterServerGroupName: masterServerGroupName,
MasterServerGroupPolicy: masterServerGroupPolicy,
WorkerServerGroupNames: workerServerGroupNames,
WorkerServerGroupPolicy: workerServerGroupPolicy,
AdditionalNetworkIDs: additionalNetworkIDs,
AdditionalSecurityGroupIDs: additionalSecurityGroupIDs,
MachinesSubnet: machinesSubnet,
MachinesNetwork: machinesNetwork,
MasterAvailabilityZones: zones,
MasterRootVolumeAvailabilityZones: masterRootVolumeAvailabilityZones,
UserManagedLoadBalancer: userManagedLoadBalancer,
}, "", " ")
}
// getServiceCatalog fetches OpenStack service catalog with service endpoints
func getServiceCatalog(cloud string) (*tokens.ServiceCatalog, error) {
conn, err := clientconfig.NewServiceClient("identity", openstackdefaults.DefaultClientOpts(cloud))
if err != nil {
return nil, err
}
authResult := conn.GetAuthResult()
auth, ok := authResult.(tokens.CreateResult)
if !ok {
return nil, fmt.Errorf("unable to extract service catalog")
}
serviceCatalog, err := auth.ExtractServiceCatalog()
if err != nil {
return nil, err
}
return serviceCatalog, nil
}
// getNetworkFromSubnet looks up a subnet in openstack and returns the ID of the network it's a part of
func getNetworkFromSubnet(cloud string, subnetID string) (string, error) {
networkClient, err := clientconfig.NewServiceClient("network", openstackdefaults.DefaultClientOpts(cloud))
if err != nil {
return "", err
}
subnet, err := subnets.Get(networkClient, subnetID).Extract()
if err != nil {
return "", err
}
return subnet.NetworkID, nil
}
func isOctaviaSupported(serviceCatalog *tokens.ServiceCatalog) (bool, error) {
_, err := openstack.V3EndpointURL(serviceCatalog, gophercloud.EndpointOpts{
Type: "load-balancer",
Name: "octavia",
Availability: gophercloud.AvailabilityPublic,
})
if err != nil {
if _, ok := err.(*gophercloud.ErrEndpointNotFound); ok {
return false, nil
}
return false, err
}
return true, nil
}
// We need to obtain Glance public endpoint that will be used by Ignition to download bootstrap ignition files.
// By design this should be done by using https://www.terraform.io/docs/providers/openstack/d/identity_endpoint_v3.html
// but OpenStack default policies forbid to use this API for regular users.
// On the other hand when a user authenticates in OpenStack (i.e. gets a token), it includes the whole service
// catalog in the output json. So we are able to parse the data and get the endpoint from there
// https://docs.openstack.org/api-ref/identity/v3/?expanded=token-authentication-with-scoped-authorization-detail#token-authentication-with-scoped-authorization
// Unfortunately this feature is not currently supported by Terraform, so we had to implement it here.
// We do next:
// 1. In "getServiceCatalog" we authenticate in OpenStack (tokens.Create(..)),
// parse the token and extract the service catalog: (ExtractServiceCatalog())
// 2. In getGlancePublicURL we iterate through the catalog and find "public" endpoint for "image".
func getBootstrapShim(cloud string, infraID string, serviceCatalog *tokens.ServiceCatalog, proxy *types.Proxy, bootstrapIgn string, userCA string) (string, error) {
clientConfigCloud, err := clientconfig.GetCloudFromYAML(openstackdefaults.DefaultClientOpts(cloud))
if err != nil {
return "", err
}
regionName := clientConfigCloud.RegionName
glancePublicURL, err := openstack.V3EndpointURL(serviceCatalog, gophercloud.EndpointOpts{
Type: "image",
Availability: gophercloud.AvailabilityPublic,
Region: regionName,
})
if err != nil {
return "", fmt.Errorf("cannot retrieve Glance URL from the service catalog: %w", err)
}
configLocation, err := uploadBootstrapConfig(cloud, bootstrapIgn, infraID)
if err != nil {
return "", err
}
tokenID, err := getAuthToken(cloud)
if err != nil {
return "", err
}
bootstrapConfigURL := fmt.Sprintf("%s%s", glancePublicURL, configLocation)
return generateIgnitionShim(userCA, infraID, bootstrapConfigURL, tokenID, proxy)
}
func getServerGroupPolicy(machinePool, defaultMachinePool *types_openstack.MachinePool, defaultPolicy types_openstack.ServerGroupPolicy) types_openstack.ServerGroupPolicy {
if machinePool != nil && machinePool.ServerGroupPolicy.IsSet() {
return machinePool.ServerGroupPolicy
}
if defaultMachinePool != nil && defaultMachinePool.ServerGroupPolicy.IsSet() {
return defaultMachinePool.ServerGroupPolicy
}
return defaultPolicy
}