Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
lentzi90 committed Sep 29, 2023
1 parent 0b2e6cc commit 746cf67
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 39 deletions.
72 changes: 42 additions & 30 deletions pkg/clients/compute.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
uflavors "github.com/gophercloud/utils/openstack/compute/v2/flavors"

"sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics"
openstackutil "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/openstack"
)

/*
Expand All @@ -47,13 +48,13 @@ var NovaSupportedVersions = map[int]*utils.Version{
38: {ID: "2.38", Priority: 10, Suffix: "/v2.38/"}, // Maximum in Newton
53: {ID: "2.53", Priority: 20, Suffix: "/v2.53/"}, // Maximum in Pike
60: {ID: "2.60", Priority: 30, Suffix: "/v2.60/"}, // Maximum in Queens
90: {ID: "2.90", Priority: 0, Suffix: "/v2.90/"}, // Maximum in Devstack
}

// Constants for specific microversion requirement.
// The values correspond to the decimal part of the Nova version for easy comparisons.
// For example, we require 2.53 for tagging support, so NovaTagging is set to 53.
const (
NovaTagging = 53
MinimumNovaMicroversion = "2.38"
NovaTagging = "2.53"
)

// ServerExt is the base gophercloud Server with extensions used by InstanceStatus.
Expand All @@ -73,10 +74,22 @@ type ComputeClient interface {

ListAttachedInterfaces(serverID string) ([]attachinterfaces.Interface, error)
DeleteAttachedInterface(serverID, portID string) error
RequireMicroversion(decimal int) error
RequireMicroversion(required string) error
}

type computeClient struct{ client *gophercloud.ServiceClient }
func GetNovaSupportedVersions() []*utils.Version {
supportedVersions := []*utils.Version{}
for k := range NovaSupportedVersions {
supportedVersions = append(supportedVersions, NovaSupportedVersions[k])
}
return supportedVersions
}

type computeClient struct {
client *gophercloud.ServiceClient
minVersion string
maxVersion string
}

// NewComputeClient returns a new compute client.
func NewComputeClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (ComputeClient, error) {
Expand All @@ -87,19 +100,24 @@ func NewComputeClient(providerClient *gophercloud.ProviderClient, providerClient
return nil, fmt.Errorf("failed to create compute service client: %v", err)
}

// Make an initial version negotiation.
// The caller can redo this later with more restrictions via RequireMicroversion.
supportedVersions := []*utils.Version{}
for k := range NovaSupportedVersions {
supportedVersions = append(supportedVersions, NovaSupportedVersions[k])
// Find the minimum and maximum versions supported by the server
serviceMin, serviceMax, err := openstackutil.GetSupportedMicroversions(*compute)
if err != nil {
return nil, fmt.Errorf("unable to verify compatible server version: %w", err)
}
version, _, err := utils.ChooseVersion(providerClient, supportedVersions)

supported, err := openstackutil.MicroversionSupported(MinimumNovaMicroversion, serviceMin, serviceMax)
if err != nil {
return nil, fmt.Errorf("failed to negotiation compute service version: %w", err)
return nil, fmt.Errorf("unable to verify compatible server version: %w", err)
}
compute.Microversion = version.ID
if !supported {
return nil, fmt.Errorf("no compatible server version. CAPO requires %s, but min=%s and max=%s",
MinimumNovaMicroversion, serviceMin, serviceMax)
}

compute.Microversion = MinimumNovaMicroversion

return &computeClient{compute}, nil
return &computeClient{client: compute, minVersion: serviceMin, maxVersion: serviceMax}, nil
}

func (c computeClient) ListAvailabilityZones() ([]availabilityzones.AvailabilityZone, error) {
Expand Down Expand Up @@ -173,23 +191,17 @@ func (c computeClient) DeleteAttachedInterface(serverID, portID string) error {
return mc.ObserveRequestIgnoreNotFoundorConflict(err)
}

// RequireMicroversion negotiates the Nova microversion for the ComputeClient while taking into account
// the version requirement given. The microversion will be set to minimum `2.requiredDecimal`,
// or an error will be returned.
func (c computeClient) RequireMicroversion(requiredDecimal int) error {
supportedMicroversions := []*utils.Version{}
for decimal := range NovaSupportedVersions {
if decimal < requiredDecimal {
continue
}
supportedMicroversions = append(supportedMicroversions, NovaSupportedVersions[decimal])
}
chosen, _, err := utils.ChooseVersion(c.client.ProviderClient, supportedMicroversions)
// RequireMicroversion checks that the required Nova microversion is supported and sets it for
// the ComputeClient.
func (c computeClient) RequireMicroversion(required string) error {
supported, err := openstackutil.MicroversionSupported(required, c.minVersion, c.maxVersion)
if err != nil {
return fmt.Errorf("failed to negotiate compute client version: %v", err)
return err
}
if !supported {
return fmt.Errorf("microversion %s not supported. Min=%s, max=%s", required, c.minVersion, c.maxVersion)
}
fmt.Println("TODO: Add proper logging. Chose microversion %s", chosen.ID)
c.client.Microversion = chosen.ID
c.client.Microversion = required
return nil
}

Expand Down Expand Up @@ -232,6 +244,6 @@ func (e computeErrorClient) DeleteAttachedInterface(_, _ string) error {
return e.error
}

func (e computeErrorClient) RequireMicroversion(_ int) error {
func (e computeErrorClient) RequireMicroversion(_ string) error {
return e.error
}
4 changes: 2 additions & 2 deletions pkg/clients/mock/compute.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

170 changes: 170 additions & 0 deletions pkg/utils/openstack/microversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package openstack

import (
"fmt"
"strconv"
"strings"

"github.com/gophercloud/gophercloud"
)

// type linkResp struct {
// Href string `json:"href"`
// Rel string `json:"rel"`
// }

// type valueResp struct {
// ID string `json:"id"`
// Status string `json:"status"`
// Version string `json:"version"`
// MinVersion string `json:"min_version"`
// Links []linkResp `json:"links"`
// }

// type response struct {
// Version valueResp `json:"version"`
// }

// ChooseVersion queries the base endpoint of an API to choose the most recent non-experimental alternative from a service's
// published versions.
// It returns the highest-Priority Version among the alternatives that are provided.
// func ChooseVersion(client *gophercloud.ServiceClient, recognized []*utils.Version) (*utils.Version, error) {
// log := ctrl.LoggerFrom(context.Background())
// log.Info("Choosing among versions", "num recognized", len(recognized))

// // If a full endpoint is specified, check version suffixes for a match first.
// for _, v := range recognized {
// log.Info("Recognized version", "ID", v.ID)
// if strings.HasSuffix(client.Endpoint, v.Suffix) {
// return v, nil
// }
// }

// var resp response
// log.Info("Getting available versions", "endpoint", client.Endpoint)
// _, err := client.Request("GET", client.Endpoint, &gophercloud.RequestOpts{
// JSONResponse: &resp,
// OkCodes: []int{200, 300},
// })

// if err != nil {
// return nil, err
// }

// log.Info("Found available versions", "versions", resp.Version)

// var minVersion, maxVersion int

// // Parse the min and max versions. We deal only with the decimal part, so 2.53 -> 53.
// minVersion, err = parseMicroversion(resp.Version.MinVersion)
// if err != nil {
// return nil, err
// }
// maxVersion, err = parseMicroversion(resp.Version.Version)
// if err != nil {
// return nil, err
// }

// // TODO: Sort by priority so we can return at first match
// for _, v := range recognized {
// version, err := parseMicroversion(v.ID)
// if err != nil {
// return nil, err
// }

// // Acceptable version
// if (version <= maxVersion) && (version >= minVersion) {
// return v, nil
// }
// }

// return nil, fmt.Errorf("no supported version available from endpoint %s", client.Endpoint)
// }

// GetSupportedMicroversions returns the minimum and maximum microversion that is supported by the ServiceClient Endpoint.
func GetSupportedMicroversions(client gophercloud.ServiceClient) (string, string, error) {
type valueResp struct {
ID string `json:"id"`
Status string `json:"status"`
Version string `json:"version"`
MinVersion string `json:"min_version"`
}

type response struct {
Version valueResp `json:"version"`
}
var resp response
_, err := client.Request("GET", client.Endpoint, &gophercloud.RequestOpts{
JSONResponse: &resp,
OkCodes: []int{200, 300},
})
if err != nil {
return "", "", err
}

return resp.Version.MinVersion, resp.Version.Version, nil
}

// MicroversionSupported checks if a microversion falls in the supported interval.
// It returns true if the version is within the interval and false otherwise.
func MicroversionSupported(version string, minVersion string, maxVersion string) (bool, error) {
// Parse the version X.Y into X and Y integers that are easier to compare.
vMajor, v, err := parseMicroversion(version)
if err != nil {
return false, err
}
minMajor, min, err := parseMicroversion(minVersion)
if err != nil {
return false, err
}
maxMajor, max, err := parseMicroversion(maxVersion)
if err != nil {
return false, err
}

// Check that the major version number is supported.
if (vMajor < minMajor) || (vMajor > maxMajor) {
return false, err
}

// Check that the minor version number is supported
if (v <= max) && (v >= min) {
return true, nil
}

return false, nil
}

// parseMicroversion parses the version X.Y into separate integers X and Y.
// For example, "2.53" becomes 2 and 53.
func parseMicroversion(version string) (int, int, error) {
parts := strings.Split(version, ".")
if len(parts) != 2 {
return 0, 0, fmt.Errorf("invalid microversion format: %q", version)
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return 0, 0, err
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return 0, 0, err
}
return major, minor, nil
}
13 changes: 6 additions & 7 deletions test/e2e/shared/openstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import (
"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
"github.com/gophercloud/gophercloud/openstack/networking/v2/ports"
"github.com/gophercloud/gophercloud/openstack/networking/v2/subnets"
"github.com/gophercloud/gophercloud/openstack/utils"
"github.com/gophercloud/utils/openstack/clientconfig"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
Expand Down Expand Up @@ -231,17 +230,17 @@ func DumpOpenStackServers(e2eCtx *E2EContext, filter servers.ListOpts) ([]server
return nil, nil
}

chosen, _, err := utils.ChooseVersion(providerClient, clients.NovaSupportedVersions)
if err != nil {
return nil, fmt.Errorf("failed to negotiate compute client version: %v", err)
}

computeClient, err := openstack.NewComputeV2(providerClient, gophercloud.EndpointOpts{Region: clientOpts.RegionName})
if err != nil {
return nil, fmt.Errorf("error creating compute client: %v", err)
}

computeClient.Microversion = chosen.ID
// TODO: We have a ServieClient here (not ComputeClient), which means we do not have access to `RequireMicroversion`.
// Maybe we can fix it by implementing `RequireMicroversion` in gophercloud?
if filter.Tags != "" || filter.TagsAny != "" || filter.NotTags != "" || filter.NotTagsAny != "" {
computeClient.Microversion = clients.NovaTagging
}

allPages, err := servers.List(computeClient, filter).AllPages()
if err != nil {
return nil, fmt.Errorf("error listing servers: %v", err)
Expand Down

0 comments on commit 746cf67

Please sign in to comment.