From 4e05854b1a8137b725547496783d018841f582ae Mon Sep 17 00:00:00 2001 From: daturece Date: Sat, 12 Jul 2025 14:15:21 +0200 Subject: [PATCH 1/8] Implement OpenStack hibernation support - openstack_actuator.go - hibernation based on snapshots/restoring - openstack/client.go - minimal OpenstackClient to support "hibernation" operations --- go.mod | 5 + go.sum | 27 + .../hibernation/openstack_actuator.go | 738 ++++++++++++++++++ pkg/openstackclient/client.go | 429 ++++++++++ 4 files changed, 1199 insertions(+) create mode 100644 pkg/controller/hibernation/openstack_actuator.go create mode 100644 pkg/openstackclient/client.go diff --git a/go.mod b/go.mod index afa4f61bfad..c77b2b10a70 100644 --- a/go.mod +++ b/go.mod @@ -128,7 +128,12 @@ require ( ) require ( +<<<<<<< Updated upstream github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb +======= + github.com/gophercloud/gophercloud v1.7.0 + github.com/gophercloud/utils/v2 v2.0.0-20240701101423-2401526caee5 +>>>>>>> Stashed changes gopkg.in/evanphx/json-patch.v4 v4.12.0 ) diff --git a/go.sum b/go.sum index 203b24b1c3d..9c4d2a53da9 100644 --- a/go.sum +++ b/go.sum @@ -506,6 +506,7 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +<<<<<<< Updated upstream github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= @@ -514,6 +515,27 @@ github.com/gophercloud/gophercloud/v2 v2.5.0 h1:DubPfC43gsZiGZ9LT1IJflVMm+0rck0e github.com/gophercloud/gophercloud/v2 v2.5.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb h1:TQTXVYXL3d0zRAybRUKKboO0z/XAsXEfU6Oax8n00kc= github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb/go.mod h1:tIUw/gFHOB6lFV9LhzNZg5jfCLYMxI2lC1dZUa7NlHM= +======= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= +github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gophercloud/gophercloud v1.7.0 h1:fyJGKh0LBvIZKLvBWvQdIgkaV5yTM3Jh9EYUh+UNCAs= +github.com/gophercloud/gophercloud v1.7.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/gophercloud/gophercloud/v2 v2.0.0 h1:iH0x0Ji79a/ULzmq95fvOBAyie7+M+wUAEu+JrRMsCk= +github.com/gophercloud/gophercloud/v2 v2.0.0/go.mod h1:ZKbcGNjxFTSaP5wlvtLDdsppllD/UGGvXBPqcjeqA8Y= +github.com/gophercloud/utils/v2 v2.0.0-20240701101423-2401526caee5 h1:/mLIQMTyjIVfiwQkknJS9XxEPLFuB70ss+ZrofChBf8= +github.com/gophercloud/utils/v2 v2.0.0-20240701101423-2401526caee5/go.mod h1:3tI9DoiOJFBkqbOeAPqPns/QUnMCiflwYBvgR6KJdM4= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +>>>>>>> Stashed changes github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -1077,6 +1099,11 @@ golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +<<<<<<< Updated upstream +======= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +>>>>>>> Stashed changes golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= diff --git a/pkg/controller/hibernation/openstack_actuator.go b/pkg/controller/hibernation/openstack_actuator.go new file mode 100644 index 00000000000..84a6923f815 --- /dev/null +++ b/pkg/controller/hibernation/openstack_actuator.go @@ -0,0 +1,738 @@ +package hibernation + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + hivev1 "github.com/openshift/hive/apis/hive/v1" + "github.com/openshift/hive/pkg/openstackclient" + + log "github.com/sirupsen/logrus" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func init() { + RegisterActuator(&openstackActuator{openstackClientFn: getOpenStackClient}) +} + +// openstackActuator implements HibernationActuator for OpenStack +type openstackActuator struct { + openstackClientFn func(*hivev1.ClusterDeployment, client.Client, log.FieldLogger) (openstackclient.Client, error) +} + +func getOpenStackClient(cd *hivev1.ClusterDeployment, c client.Client, logger log.FieldLogger) (openstackclient.Client, error) { + ctx := context.Background() + + if cd.Spec.Platform.OpenStack == nil || cd.Spec.Platform.OpenStack.CredentialsSecretRef.Name == "" { + return nil, fmt.Errorf("no OpenStack credentials secret reference found in ClusterDeployment") + } + + secretName := cd.Spec.Platform.OpenStack.CredentialsSecretRef.Name + secretNamespace := cd.Namespace + + secret := &corev1.Secret{} + err := c.Get(ctx, types.NamespacedName{ + Name: secretName, + Namespace: secretNamespace, + }, secret) + if err != nil { + return nil, fmt.Errorf("failed to get credentials secret %s/%s: %v", secretNamespace, secretName, err) + } + + return openstackclient.NewClientFromSecret(secret) +} + +// CanHandle returns true if this actuator can handle the given ClusterDeployment +func (a *openstackActuator) CanHandle(cd *hivev1.ClusterDeployment) bool { + return cd.Spec.Platform.OpenStack != nil +} + +// StopMachines creates snapshots and saves configuration, then stops machines +func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { + logger = logger.WithField("cloud", "openstack") + logger.Info("stopping machines and creating snapshots") + + _, err := a.loadHibernationConfigFromSecret(cd, hiveClient, logger) + if err == nil { + logger.Info("Hibernation config already exists - checking if hibernation completed") + + openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) + if err != nil { + return fmt.Errorf("failed to create OpenStack client: %v", err) + } + + infraID := cd.Spec.ClusterMetadata.InfraID + matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + if err != nil { + return fmt.Errorf("error finding instances: %v", err) + } + + if len(matchingServers) == 0 { + logger.Info("Hibernation already completed - config exists and no instances found") + return nil + } + + logger.Info("Hibernation config exists but instances still found - proceeding with cleanup") + } + + logger = logger.WithField("cloud", "openstack") + logger.Info("stopping machines and creating snapshots") + + openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) + if err != nil { + return fmt.Errorf("failed to create OpenStack client: %v", err) + } + + infraID := cd.Spec.ClusterMetadata.InfraID + + matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + if err != nil { + return fmt.Errorf("error finding instances: %v", err) + } + + if len(matchingServers) == 0 { + logger.Info("no instances found - cluster already hibernated") + return nil + } + + logger.Infof("found %d instances to hibernate", len(matchingServers)) + + // Validate instance states before snapshotting + err = a.validateInstanceStates(openstackClient, matchingServers, logger) + if err != nil { + return err + } + + // Create snapshots for each instance + snapshotIDs, err := a.createSnapshots(openstackClient, matchingServers, logger) + if err != nil { + return err + } + + // Wait for all snapshots to complete + err = a.waitForSnapshots(openstackClient, snapshotIDs, matchingServers, logger) + if err != nil { + return err + } + + // Save configuration to Secret + err = a.saveInstanceConfigurationToSecret(cd, hiveClient, openstackClient, matchingServers, snapshotIDs, logger) + if err != nil { + return fmt.Errorf("error saving configuration: %v", err) + } + + // Delete the instances + err = a.deleteInstances(openstackClient, matchingServers, logger) + if err != nil { + return err + } + + logger.Info("waiting for OpenStack to clean up deleted instances...") + time.Sleep(30 * time.Second) + + logger.Info("hibernation completed successfully") + return nil +} + +// validateInstanceStates checks if instances are in valid states for hibernation +func (a *openstackActuator) validateInstanceStates(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) error { + ctx := context.Background() + + for _, server := range servers { + serverDetails, err := openstackClient.GetServer(ctx, server.ID) + if err != nil { + return fmt.Errorf("failed to get server %s details: %v", server.Name, err) + } + + logger.Infof("instance %s status: %s", server.Name, serverDetails.Status) + + // Check for deleting states that would cause conflicts + if strings.Contains(strings.ToLower(serverDetails.Status), "delet") { + return fmt.Errorf("cannot hibernate: instance %s is being deleted by another process", server.Name) + } + + if serverDetails.Status != "ACTIVE" { + logger.Warnf("instance %s status is %s (not ACTIVE) - snapshot may fail", server.Name, serverDetails.Status) + } + } + return nil +} + +// Create snapshots for all instances +func (a *openstackActuator) createSnapshots(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) ([]string, error) { + ctx := context.Background() + snapshotIDs := make([]string, 0, len(servers)) + + for i, server := range servers { + logger.Infof("creating snapshot %d/%d for instance %s", i+1, len(servers), server.Name) + + snapshotID, err := openstackClient.CreateServerSnapshot(ctx, server.ID, server.Name) + if err != nil { + // Enhanced error handling for conflicts + if strings.Contains(err.Error(), "task_state deleting") || strings.Contains(err.Error(), "409") { + return nil, fmt.Errorf("hibernation conflict: instance %s is being modified by another process", server.Name) + } + return nil, fmt.Errorf("failed to create snapshot for %s: %v", server.Name, err) + } + + snapshotIDs = append(snapshotIDs, snapshotID) + logger.Infof("snapshot created for %s (ID: %s)", server.Name, snapshotID) + } + return snapshotIDs, nil +} + +// waitForSnapshots waits for all snapshots to complete +func (a *openstackActuator) waitForSnapshots(openstackClient openstackclient.Client, snapshotIDs []string, servers []ServerInfo, logger log.FieldLogger) error { + for i, snapshotID := range snapshotIDs { + serverName := servers[i].Name + logger.Infof("waiting for snapshot %s to complete for %s", snapshotID, serverName) + + err := a.waitForSnapshotCompletion(openstackClient, snapshotID, serverName, logger) + if err != nil { + return fmt.Errorf("failed to wait for snapshot %s: %v", snapshotID, err) + } + } + return nil +} + +// deleteInstances deletes all instances +func (a *openstackActuator) deleteInstances(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) error { + ctx := context.Background() + + for i, server := range servers { + logger.Infof("deleting instance %d/%d: %s", i+1, len(servers), server.Name) + + err := openstackClient.DeleteServer(ctx, server.ID) + if err != nil { + return fmt.Errorf("failed to delete %s: %v", server.Name, err) + } + } + return nil +} + +// waitForSnapshotCompletion waits for a snapshot to reach ACTIVE state +func (a *openstackActuator) waitForSnapshotCompletion(openstackClient openstackclient.Client, snapshotID, serverName string, logger log.FieldLogger) error { + ctx := context.Background() + maxWaitTime := 30 * time.Minute + checkInterval := 10 * time.Second + timeout := time.After(maxWaitTime) + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + + for { + select { + case <-timeout: + return fmt.Errorf("timeout waiting for snapshot %s to complete after %v", snapshotID, maxWaitTime) + case <-ticker.C: + image, err := openstackClient.GetImage(ctx, snapshotID) + if err != nil { + logger.Warnf("error checking snapshot %s status: %v", snapshotID, err) + continue + } + + logger.Infof("snapshot %s for %s status: %s", snapshotID, serverName, image.Status) + + switch image.Status { + case "active": + return nil + case "queued", "saving": + continue + case "killed", "deleted", "deactivated": + return fmt.Errorf("snapshot %s failed with status: %s", snapshotID, image.Status) + default: + logger.Warnf("unknown snapshot status %s for %s, continuing to wait", image.Status, snapshotID) + continue + } + } + } +} + +// StartMachines recreates instances from snapshots using saved configuration +func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { + logger = logger.WithField("cloud", "openstack") + logger.Info("starting machines from snapshots") + + // Only proceed if PowerState is Running + if cd.Spec.PowerState != hivev1.ClusterPowerStateRunning { + logger.Infof("PowerState is %s, not Running - refusing to start machines", cd.Spec.PowerState) + return nil + } + + openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) + if err != nil { + return fmt.Errorf("failed to create OpenStack client: %v", err) + } + + // Check for existing instances + infraID := cd.Spec.ClusterMetadata.InfraID + existingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + if err != nil { + logger.Warnf("could not check existing instances: %v", err) + } + + if len(existingServers) > 0 { + logger.Info("instances already exist - clearing hibernation config") + _ = a.deleteHibernationConfigSecret(cd, hiveClient, logger) // Best effort cleanup + return nil + } + + // Load hibernation config and restore + instances, err := a.loadHibernationConfigFromSecret(cd, hiveClient, logger) + if err != nil { + logger.Warnf("no hibernation config found: %v", err) + logger.Warn("cannot recreate instances without hibernation snapshots") + return nil // Don't fail - let controller handle this gracefully + } + + logger.Infof("restoring %d instances from hibernation snapshots", len(instances)) + return a.restoreFromHibernationConfig(cd, hiveClient, openstackClient, instances, logger) +} + +// restoreFromHibernationConfig recreates instances from hibernation configuration +func (a *openstackActuator) restoreFromHibernationConfig(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { + ctx := context.Background() + + logger.Infof("restoring %d instances from hibernation", len(instances)) + + // Recreate each instance using saved configuration + createdServerIDs := make([]string, 0, len(instances)) + + for i, instance := range instances { + logger.Infof("creating instance %d/%d: %s", i+1, len(instances), instance.Name) + + // Validate snapshot exists + _, err := openstackClient.GetImage(ctx, instance.SnapshotID) + if err != nil { + return fmt.Errorf("snapshot %s not found: %v", instance.SnapshotID, err) + } + + // Build server creation options + createOpts := &openstackclient.ServerCreateOpts{ + Name: instance.Name, + ImageRef: instance.SnapshotID, + FlavorRef: instance.Flavor, + NetworkID: instance.NetworkID, + PortID: instance.PortID, + SecurityGroups: instance.SecurityGroups, + Metadata: map[string]string{ + "openshiftClusterID": instance.OpenshiftClusterID, + }, + } + + newServer, err := openstackClient.CreateServerFromOpts(ctx, createOpts) + if err != nil { + return fmt.Errorf("failed to create instance %s: %v", instance.Name, err) + } + + createdServerIDs = append(createdServerIDs, newServer.ID) + logger.Infof("created instance %s (ID: %s)", instance.Name, newServer.ID) + } + + // Wait for instances to become ACTIVE + logger.Info("waiting for instances to become active") + for i, serverID := range createdServerIDs { + instanceName := instances[i].Name + err := a.waitForServerActive(openstackClient, serverID, instanceName, logger) + if err != nil { + return fmt.Errorf("failed to wait for instance %s to become active: %v", instanceName, err) + } + } + + // Clear hibernation config since restoration is complete + logger.Info("restoration completed - clearing hibernation configuration") + err := a.deleteHibernationConfigSecret(cd, hiveClient, logger) + if err != nil { + logger.Warnf("could not clear hibernation config: %v", err) + } + + logger.Info("restoration completed successfully") + return nil +} + +// waitForServerActive waits for a server to reach ACTIVE state +func (a *openstackActuator) waitForServerActive(openstackClient openstackclient.Client, serverID, serverName string, logger log.FieldLogger) error { + ctx := context.Background() + maxWaitTime := 30 * time.Minute + checkInterval := 15 * time.Second + timeout := time.After(maxWaitTime) + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + + for { + select { + case <-timeout: + return fmt.Errorf("timeout waiting for server %s to become ACTIVE after %v", serverID, maxWaitTime) + case <-ticker.C: + server, err := openstackClient.GetServer(ctx, serverID) + if err != nil { + logger.Warnf("error checking server %s status: %v", serverID, err) + continue + } + + logger.Infof("server %s (%s) status: %s", serverID, serverName, server.Status) + + switch server.Status { + case "ACTIVE": + return nil + case "BUILD", "REBUILD": + continue + case "ERROR", "DELETED": + return fmt.Errorf("server %s failed with status: %s", serverID, server.Status) + default: + continue + } + } + } +} + +// MachinesRunning checks if machines are running +func (a *openstackActuator) MachinesRunning(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) (bool, []string, error) { + logger = logger.WithField("cloud", "openstack") + logger.Info("checking if machines are running") + + openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) + if err != nil { + return false, nil, fmt.Errorf("failed to create OpenStack client: %v", err) + } + + infraID := cd.Spec.ClusterMetadata.InfraID + matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + if err != nil { + return false, nil, fmt.Errorf("error finding instances: %v", err) + } + + logger.Infof("found %d instances with prefix '%s'", len(matchingServers), infraID) + + if len(matchingServers) == 0 { + logger.Info("no instances found - machines are not running") + return false, []string{"no instances found"}, nil + } + + // OpenStack-specific: Check actual instance states + runningCount, deletingInstances := a.categorizeInstanceStates(openstackClient, matchingServers, logger) + + // If instances are being deleted, hibernation is in progress + if len(deletingInstances) > 0 && runningCount == 0 { + logger.Infof("all instances are being deleted (%v) - hibernation in progress", deletingInstances) + return false, []string{"instances-being-deleted"}, nil + } + + return runningCount > 0, []string{}, nil +} + +// categorizeInstanceStates checks the actual state of instances in OpenStack +func (a *openstackActuator) categorizeInstanceStates(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) (int, []string) { + ctx := context.Background() + runningCount := 0 + var deletingInstances []string + + for _, server := range servers { + serverDetails, err := openstackClient.GetServer(ctx, server.ID) + if err != nil { + logger.Warnf("could not get server %s details: %v", server.Name, err) + runningCount++ // Assume running if we can't check + continue + } + + status := strings.ToLower(serverDetails.Status) + if strings.Contains(status, "delet") || status == "shutoff" || status == "error" { + logger.Infof("instance %s is being deleted/stopped (status: %s)", server.Name, serverDetails.Status) + deletingInstances = append(deletingInstances, server.Name) + } else { + logger.Infof("instance %s is running (status: %s)", server.Name, serverDetails.Status) + runningCount++ + } + } + + return runningCount, deletingInstances +} + +// MachinesStopped checks if machines are stopped +func (a *openstackActuator) MachinesStopped(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) (bool, []string, error) { + logger = logger.WithField("cloud", "openstack") + logger.Info("checking if machines are stopped") + + openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) + if err != nil { + return false, nil, fmt.Errorf("failed to create OpenStack client: %v", err) + } + + infraID := cd.Spec.ClusterMetadata.InfraID + matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + if err != nil { + return false, nil, fmt.Errorf("error finding instances: %v", err) + } + + if len(matchingServers) == 0 { + logger.Info("no instances found - machines are stopped") + return true, nil, nil + } + + var notStopped []string + for _, server := range matchingServers { + notStopped = append(notStopped, server.Name) + } + + logger.Infof("found %d instances still running", len(notStopped)) + return false, notStopped, nil +} + +// ServerInfo holds basic server information +type ServerInfo struct { + ID string + Name string +} + +type OpenStackInstanceConfig struct { + Name string `json:"name"` + Flavor string `json:"flavor"` + PortID string `json:"portID"` + SnapshotID string `json:"snapshotID"` + SecurityGroups []string `json:"securityGroups"` + ClusterID string `json:"clusterID"` + NetworkID string `json:"networkID"` + OpenshiftClusterID string `json:"openshiftClusterID"` +} + +// findInstancesByPrefix returns servers that match the infraID prefix +func (a *openstackActuator) findInstancesByPrefix(openstackClient openstackclient.Client, prefix string) ([]ServerInfo, error) { + ctx := context.Background() + + servers, err := openstackClient.ListServers(ctx, nil) + if err != nil { + return nil, fmt.Errorf("error listing servers: %v", err) + } + + var matchingServers []ServerInfo + for _, server := range servers { + if strings.HasPrefix(server.Name, prefix) { + matchingServers = append(matchingServers, ServerInfo{ + ID: server.ID, + Name: server.Name, + }) + } + } + + return matchingServers, nil +} + +// Configuration persistence methods +func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, matchingServers []ServerInfo, snapshotIDs []string, logger log.FieldLogger) error { + ctx := context.Background() + + if len(matchingServers) == 0 { + return nil + } + + if len(snapshotIDs) != len(matchingServers) { + return fmt.Errorf("mismatch between servers (%d) and snapshot IDs (%d)", len(matchingServers), len(snapshotIDs)) + } + + // Use infraID directly instead of extracting from instance name + infraID := cd.Spec.ClusterMetadata.InfraID + + // Get shared configuration + networkID, err := a.getNetworkIDForCluster(openstackClient, infraID) + if err != nil { + return fmt.Errorf("error getting network ID: %v", err) + } + + // Get openshiftClusterID from first instance + server, err := openstackClient.GetServer(ctx, matchingServers[0].ID) + if err != nil { + return fmt.Errorf("error getting server details: %v", err) + } + + var openshiftClusterID string + if server.Metadata != nil { + if id, exists := server.Metadata["openshiftClusterID"]; exists { + openshiftClusterID = id + } + } + + // Get all ports once for efficiency + allPorts, err := openstackClient.ListPorts(ctx) + if err != nil { + return fmt.Errorf("error listing ports: %v", err) + } + + // Build configuration for each instance + var instanceConfigs []OpenStackInstanceConfig + for i, serverInfo := range matchingServers { + serverDetails, err := openstackClient.GetServer(ctx, serverInfo.ID) + if err != nil { + return fmt.Errorf("error getting server details for %s: %v", serverInfo.Name, err) + } + + // Get flavor ID + var flavorID string + if serverDetails.Flavor != nil { + if id, ok := serverDetails.Flavor["id"].(string); ok { + flavorID = id + } else { + return fmt.Errorf("could not extract flavor ID for %s", serverInfo.Name) + } + } else { + return fmt.Errorf("no flavor information found for %s", serverInfo.Name) + } + + // Find matching port + var portID string + for _, port := range allPorts { + if port.Name == serverInfo.Name || port.Name == serverInfo.Name+"-0" { + portID = port.ID + break + } + } + + if portID == "" { + return fmt.Errorf("no port found for instance %s", serverInfo.Name) + } + + // Get security groups + secGroups, err := openstackClient.GetServerSecurityGroups(ctx, serverInfo.ID) + if err != nil { + return fmt.Errorf("error getting security groups for %s: %v", serverInfo.Name, err) + } + + instanceConfigs = append(instanceConfigs, OpenStackInstanceConfig{ + Name: serverInfo.Name, + Flavor: flavorID, + PortID: portID, + SnapshotID: snapshotIDs[i], + SecurityGroups: secGroups, + ClusterID: infraID, // Use infraID directly + NetworkID: networkID, + OpenshiftClusterID: openshiftClusterID, + }) + } + + return a.saveHibernationConfigToSecret(cd, hiveClient, instanceConfigs, logger) +} + +func (a *openstackActuator) saveHibernationConfigToSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, instanceConfigs []OpenStackInstanceConfig, logger log.FieldLogger) error { + ctx := context.Background() + + configData, err := json.Marshal(instanceConfigs) + if err != nil { + return fmt.Errorf("failed to marshal hibernation config: %v", err) + } + + secretName := fmt.Sprintf("%s-hibernation-config", cd.Name) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: cd.Namespace, + Labels: map[string]string{ + "hive.openshift.io/cluster-deployment": cd.Name, + "hive.openshift.io/hibernation-config": "openstack", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: cd.APIVersion, + Kind: cd.Kind, + Name: cd.Name, + UID: cd.UID, + }, + }, + }, + Data: map[string][]byte{ + "hibernation-config": configData, + }, + } + + err = hiveClient.Create(ctx, secret) + if err != nil { + if !apierrors.IsAlreadyExists(err) { + return fmt.Errorf("failed to create hibernation config secret: %v", err) + } + + // Update existing secret + existingSecret := &corev1.Secret{} + err = hiveClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: cd.Namespace}, existingSecret) + if err != nil { + return fmt.Errorf("failed to get existing hibernation config secret: %v", err) + } + + existingSecret.Data = secret.Data + err = hiveClient.Update(ctx, existingSecret) + if err != nil { + return fmt.Errorf("failed to update hibernation config secret: %v", err) + } + } + + logger.Infof("saved hibernation configuration to secret %s", secretName) + return nil +} + +func (a *openstackActuator) loadHibernationConfigFromSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) ([]OpenStackInstanceConfig, error) { + ctx := context.Background() + secretName := fmt.Sprintf("%s-hibernation-config", cd.Name) + + secret := &corev1.Secret{} + err := hiveClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: cd.Namespace}, secret) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, fmt.Errorf("hibernation config secret not found") + } + return nil, fmt.Errorf("failed to get hibernation config secret: %v", err) + } + + configData, exists := secret.Data["hibernation-config"] + if !exists { + return nil, fmt.Errorf("hibernation config not found in secret") + } + + var instanceConfigs []OpenStackInstanceConfig + err = json.Unmarshal(configData, &instanceConfigs) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal hibernation config: %v", err) + } + + logger.Infof("loaded hibernation configuration from secret %s (%d instances)", secretName, len(instanceConfigs)) + return instanceConfigs, nil +} + +func (a *openstackActuator) deleteHibernationConfigSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { + ctx := context.Background() + secretName := fmt.Sprintf("%s-hibernation-config", cd.Name) + + secret := &corev1.Secret{} + err := hiveClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: cd.Namespace}, secret) + if err != nil { + if apierrors.IsNotFound(err) { + logger.Info("hibernation config secret already deleted") + return nil + } + return fmt.Errorf("failed to get hibernation config secret: %v", err) + } + + err = hiveClient.Delete(ctx, secret) + if err != nil { + return fmt.Errorf("failed to delete hibernation config secret: %v", err) + } + + logger.Infof("deleted hibernation config secret %s", secretName) + return nil +} + +// getNetworkIDForCluster finds the network ID for a specific cluster using infraID +func (a *openstackActuator) getNetworkIDForCluster(openstackClient openstackclient.Client, infraID string) (string, error) { + ctx := context.Background() + networkName := fmt.Sprintf("%s-openshift", infraID) + + network, err := openstackClient.GetNetworkByName(ctx, networkName) + if err != nil { + return "", fmt.Errorf("failed to find network '%s': %w", networkName, err) + } + + return network.ID, nil +} diff --git a/pkg/openstackclient/client.go b/pkg/openstackclient/client.go new file mode 100644 index 00000000000..0d4b01b1b2c --- /dev/null +++ b/pkg/openstackclient/client.go @@ -0,0 +1,429 @@ +package openstackclient + +import ( + "context" + "encoding/json" + "fmt" + + "gopkg.in/yaml.v2" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + corev1 "k8s.io/api/core/v1" +) + +//go:generate mockgen -source=./client.go -destination=./mock/client_generated.go -package=mock + +// Client is a wrapper object for actual OpenStack libraries to allow for easier mocking/testing. +type Client interface { + // Servers - only what you actually use + ListServers(ctx context.Context, opts *servers.ListOpts) ([]servers.Server, error) + GetServer(ctx context.Context, serverID string) (*servers.Server, error) + DeleteServer(ctx context.Context, serverID string) error + CreateServerSnapshot(ctx context.Context, serverID, snapshotName string) (string, error) + CreateServerFromOpts(ctx context.Context, opts *ServerCreateOpts) (*servers.Server, error) + + // Images - only snapshot checking + GetImage(ctx context.Context, imageID string) (*images.Image, error) + + // Networks - only what you need + GetNetworkByName(ctx context.Context, networkName string) (*Network, error) + ListPorts(ctx context.Context) ([]Port, error) + + // Security Groups - only reading + GetServerSecurityGroups(ctx context.Context, serverID string) ([]string, error) +} + +// Network represents an OpenStack network +type Network struct { + ID string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` +} + +// Port represents an OpenStack port +type Port struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// ServerCreateOpts contains options for creating a server +type ServerCreateOpts struct { + Name string `json:"name"` + ImageRef string `json:"imageRef"` + FlavorRef string `json:"flavorRef"` + NetworkID string `json:"networkID"` + PortID string `json:"portID"` + SecurityGroups []string `json:"securityGroups"` + Metadata map[string]string `json:"metadata"` +} + +// OpenStack credentials structure - matches clouds.yaml format +type Credentials struct { + AuthURL string `json:"auth_url"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + UserID string `json:"user_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + ProjectName string `json:"project_name,omitempty"` + UserDomainName string `json:"user_domain_name,omitempty"` + UserDomainID string `json:"user_domain_id,omitempty"` + ProjectDomainName string `json:"project_domain_name,omitempty"` + ProjectDomainID string `json:"project_domain_id,omitempty"` + RegionName string `json:"region_name,omitempty"` + Interface string `json:"interface,omitempty"` + IdentityAPIVersion string `json:"identity_api_version,omitempty"` + + // Legacy support + TenantID string `json:"tenant_id,omitempty"` + TenantName string `json:"tenant_name,omitempty"` + DomainID string `json:"domain_id,omitempty"` + DomainName string `json:"domain_name,omitempty"` + Region string `json:"region,omitempty"` +} + +type CloudsYAML struct { + Clouds map[string]CloudConfig `yaml:"clouds"` +} + +type CloudConfig struct { + Auth CloudAuth `yaml:"auth"` + Region string `yaml:"region_name"` + Interface string `yaml:"interface"` + Version string `yaml:"identity_api_version"` +} + +type CloudAuth struct { + AuthURL string `yaml:"auth_url"` + Username string `yaml:"username"` + Password string `yaml:"password"` + ProjectID string `yaml:"project_id"` + ProjectName string `yaml:"project_name"` + UserDomainName string `yaml:"user_domain_name"` + ProjectDomainName string `yaml:"project_domain_name"` + UserDomainID string `yaml:"user_domain_id"` + ProjectDomainID string `yaml:"project_domain_id"` +} + +type openstackClient struct { + provider *gophercloud.ProviderClient + computeClient *gophercloud.ServiceClient + imageClient *gophercloud.ServiceClient + networkClient *gophercloud.ServiceClient + credentials *Credentials +} + +// Implementation of server methods +func (c *openstackClient) ListServers(ctx context.Context, opts *servers.ListOpts) ([]servers.Server, error) { + if opts == nil { + opts = &servers.ListOpts{} + } + allPages, err := servers.List(c.computeClient, opts).AllPages() + if err != nil { + return nil, err + } + return servers.ExtractServers(allPages) +} + +func (c *openstackClient) GetServer(ctx context.Context, serverID string) (*servers.Server, error) { + server, err := servers.Get(c.computeClient, serverID).Extract() + return server, err +} + +func (c *openstackClient) DeleteServer(ctx context.Context, serverID string) error { + return servers.Delete(c.computeClient, serverID).ExtractErr() +} + +// CreateServerSnapshot creates a snapshot (image) of the specified server +func (c *openstackClient) CreateServerSnapshot(ctx context.Context, serverID, snapshotName string) (string, error) { + // Create image options + createImageOpts := servers.CreateImageOpts{ + Name: snapshotName, + Metadata: map[string]string{ + "snapshot_type": "server_snapshot", + "source_server": serverID, + }, + } + + // Create the snapshot/image + result := servers.CreateImage(c.computeClient, serverID, createImageOpts) + imageID, err := result.ExtractImageID() + if err != nil { + return "", fmt.Errorf("failed to create snapshot: %w", err) + } + + return imageID, nil +} + +// CreateServerFromOpts creates a new server using our custom ServerCreateOpts +func (c *openstackClient) CreateServerFromOpts(ctx context.Context, opts *ServerCreateOpts) (*servers.Server, error) { + // Build networks slice for the NIC + networks := []servers.Network{ + { + UUID: opts.NetworkID, + Port: opts.PortID, + }, + } + + // Convert our custom options to Gophercloud options + createOpts := &servers.CreateOpts{ + Name: opts.Name, + ImageRef: opts.ImageRef, + FlavorRef: opts.FlavorRef, + Networks: networks, + SecurityGroups: opts.SecurityGroups, + Metadata: opts.Metadata, + } + + server, err := servers.Create(c.computeClient, createOpts).Extract() + if err != nil { + return nil, fmt.Errorf("failed to create server: %w", err) + } + + return server, nil +} + +// Implementation of image methods +func (c *openstackClient) GetImage(ctx context.Context, imageID string) (*images.Image, error) { + image, err := images.Get(c.imageClient, imageID).Extract() + return image, err +} + +// Port implementations +func (c *openstackClient) ListPorts(ctx context.Context) ([]Port, error) { + allPages, err := ports.List(c.networkClient, nil).AllPages() + if err != nil { + return nil, err + } + + portList, err := ports.ExtractPorts(allPages) + if err != nil { + return nil, err + } + + var result []Port + for _, port := range portList { + result = append(result, Port{ + ID: port.ID, + Name: port.Name, + }) + } + + return result, nil +} + +func (c *openstackClient) GetNetworkByName(ctx context.Context, networkName string) (*Network, error) { + listOpts := networks.ListOpts{ + Name: networkName, + } + + allPages, err := networks.List(c.networkClient, listOpts).AllPages() + if err != nil { + return nil, err + } + + networkList, err := networks.ExtractNetworks(allPages) + if err != nil { + return nil, err + } + + if len(networkList) == 0 { + return nil, fmt.Errorf("network with name '%s' not found", networkName) + } + + // Return the first match + net := networkList[0] + return &Network{ + ID: net.ID, + Name: net.Name, + Status: net.Status, + }, nil +} + +// GetServerSecurityGroups gets the security group names for a specific server +func (c *openstackClient) GetServerSecurityGroups(ctx context.Context, serverID string) ([]string, error) { + serverSecGroups, err := secgroups.ListByServer(c.computeClient, serverID).AllPages() + if err != nil { + return nil, err + } + + secGroupList, err := secgroups.ExtractSecurityGroups(serverSecGroups) + if err != nil { + return nil, err + } + + var secGroupNames []string + for _, secGroup := range secGroupList { + secGroupNames = append(secGroupNames, secGroup.Name) + } + + return secGroupNames, nil +} + +// NewClientFromSecret creates our client wrapper object for interacting with OpenStack. +// The OpenStack creds are read from the specified secret. +func NewClientFromSecret(secret *corev1.Secret) (Client, error) { + // Check if it's a clouds.yaml format + if cloudsYaml, ok := secret.Data["clouds.yaml"]; ok { + return newClientFromCloudsYAML(cloudsYaml) + } + + // Handle JSON credentials format directly + authJSON, ok := secret.Data["credentials"] + if !ok { + return nil, fmt.Errorf("secret does not contain \"credentials\" or \"clouds.yaml\" data") + } + + var creds Credentials + if err := json.Unmarshal(authJSON, &creds); err != nil { + return nil, fmt.Errorf("failed to unmarshal credentials: %w", err) + } + + return newClientFromStruct(&creds) +} + +// newClientFromCloudsYAML creates a client from clouds.yaml data +func newClientFromCloudsYAML(cloudsYamlData []byte) (Client, error) { + var clouds CloudsYAML + if err := yaml.Unmarshal(cloudsYamlData, &clouds); err != nil { + return nil, fmt.Errorf("failed to parse clouds.yaml: %w", err) + } + + // Get the "openstack" cloud config + openstackCloud, ok := clouds.Clouds["openstack"] + if !ok { + return nil, fmt.Errorf("no 'openstack' cloud found in clouds.yaml") + } + + // Convert to Credentials struct + creds := &Credentials{ + AuthURL: openstackCloud.Auth.AuthURL, + Username: openstackCloud.Auth.Username, + Password: openstackCloud.Auth.Password, + ProjectID: openstackCloud.Auth.ProjectID, + ProjectName: openstackCloud.Auth.ProjectName, + UserDomainName: openstackCloud.Auth.UserDomainName, + ProjectDomainName: openstackCloud.Auth.ProjectDomainName, + UserDomainID: openstackCloud.Auth.UserDomainID, + ProjectDomainID: openstackCloud.Auth.ProjectDomainID, + RegionName: openstackCloud.Region, + Interface: openstackCloud.Interface, + IdentityAPIVersion: openstackCloud.Version, + } + + return newClientFromStruct(creds) +} + +func authJSONFromSecretSource(secret *corev1.Secret) func() ([]byte, error) { + return func() ([]byte, error) { + authJSON, ok := secret.Data["credentials"] // adjust key name as needed + if !ok { + return nil, fmt.Errorf("creds secret does not contain \"credentials\" data") + } + return authJSON, nil + } +} + +// newClientFromStruct creates a client directly from a Credentials struct (no JSON conversion needed) +func newClientFromStruct(creds *Credentials) (*openstackClient, error) { + // Validate required credentials + if creds.AuthURL == "" { + return nil, fmt.Errorf("missing auth_url in credentials") + } + + // Create authentication options + authOpts := gophercloud.AuthOptions{ + IdentityEndpoint: creds.AuthURL, + Username: creds.Username, + UserID: creds.UserID, + Password: creds.Password, + TenantID: creds.ProjectID, // Use ProjectID as TenantID + TenantName: creds.ProjectName, + DomainName: creds.UserDomainName, + } + + // Handle legacy fields for backwards compatibility + if creds.TenantID != "" { + authOpts.TenantID = creds.TenantID + } + if creds.TenantName != "" { + authOpts.TenantName = creds.TenantName + } + if creds.DomainID != "" { + authOpts.DomainID = creds.DomainID + } + if creds.DomainName != "" { + authOpts.DomainName = creds.DomainName + } + if creds.UserDomainID != "" { + authOpts.DomainID = creds.UserDomainID + } + if creds.ProjectDomainID != "" { + authOpts.DomainID = creds.ProjectDomainID + } + if creds.ProjectDomainName != "" { + authOpts.DomainName = creds.ProjectDomainName + } + + // Authenticate and get provider client + provider, err := openstack.AuthenticatedClient(authOpts) + if err != nil { + return nil, fmt.Errorf("failed to authenticate with OpenStack: %w", err) + } + + // Set region - prefer new format over legacy + region := creds.RegionName + if region == "" { + region = creds.Region + } + if region == "" { + region = "RegionOne" // default region + } + + // Set interface preference (public, internal, admin) + interfaceType := gophercloud.AvailabilityPublic // Start with public as default + if creds.Interface != "" { + switch creds.Interface { + case "public": + interfaceType = gophercloud.AvailabilityPublic + case "internal": + interfaceType = gophercloud.AvailabilityInternal + case "admin": + interfaceType = gophercloud.AvailabilityAdmin + } + } + + endpointOpts := gophercloud.EndpointOpts{ + Region: region, + Availability: interfaceType, + } + + // Create service clients + computeClient, err := openstack.NewComputeV2(provider, endpointOpts) + if err != nil { + return nil, fmt.Errorf("failed to create compute client: %w", err) + } + + imageClient, err := openstack.NewImageServiceV2(provider, endpointOpts) + if err != nil { + return nil, fmt.Errorf("failed to create image client: %w", err) + } + + networkClient, err := openstack.NewNetworkV2(provider, endpointOpts) + if err != nil { + return nil, fmt.Errorf("failed to create network client: %w", err) + } + + return &openstackClient{ + provider: provider, + computeClient: computeClient, + imageClient: imageClient, + networkClient: networkClient, + credentials: creds, + }, nil +} From d177558bfaa9ce0a26809a5cdef7dfb1a1bb63ae Mon Sep 17 00:00:00 2001 From: daturece Date: Mon, 28 Jul 2025 09:51:06 +0200 Subject: [PATCH 2/8] Add tests, better error handling, resource checking, finalize scripts --- go.mod | 6 +- go.sum | 28 +- .../hibernation/openstack_actuator.go | 412 ++++++++++++--- .../hibernation/openstack_actuator_test.go | 492 ++++++++++++++++++ pkg/openstackclient/client.go | 206 ++++++-- pkg/openstackclient/mock/client_generated.go | 246 +++++++++ .../clusterdeployment/clusterdeployment.go | 1 + 7 files changed, 1252 insertions(+), 139 deletions(-) create mode 100644 pkg/controller/hibernation/openstack_actuator_test.go create mode 100644 pkg/openstackclient/mock/client_generated.go diff --git a/go.mod b/go.mod index c77b2b10a70..7907e96cc53 100644 --- a/go.mod +++ b/go.mod @@ -128,12 +128,8 @@ require ( ) require ( -<<<<<<< Updated upstream - github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb -======= github.com/gophercloud/gophercloud v1.7.0 - github.com/gophercloud/utils/v2 v2.0.0-20240701101423-2401526caee5 ->>>>>>> Stashed changes + github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb gopkg.in/evanphx/json-patch.v4 v4.12.0 ) diff --git a/go.sum b/go.sum index 9c4d2a53da9..c4f343a52a4 100644 --- a/go.sum +++ b/go.sum @@ -506,36 +506,16 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -<<<<<<< Updated upstream github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/gophercloud/gophercloud v1.7.0 h1:fyJGKh0LBvIZKLvBWvQdIgkaV5yTM3Jh9EYUh+UNCAs= +github.com/gophercloud/gophercloud v1.7.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/gophercloud/gophercloud/v2 v2.5.0 h1:DubPfC43gsZiGZ9LT1IJflVMm+0rck0ejoPsH8D5rqk= github.com/gophercloud/gophercloud/v2 v2.5.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb h1:TQTXVYXL3d0zRAybRUKKboO0z/XAsXEfU6Oax8n00kc= github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb/go.mod h1:tIUw/gFHOB6lFV9LhzNZg5jfCLYMxI2lC1dZUa7NlHM= -======= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s= -github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gophercloud/gophercloud v1.7.0 h1:fyJGKh0LBvIZKLvBWvQdIgkaV5yTM3Jh9EYUh+UNCAs= -github.com/gophercloud/gophercloud v1.7.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= -github.com/gophercloud/gophercloud/v2 v2.0.0 h1:iH0x0Ji79a/ULzmq95fvOBAyie7+M+wUAEu+JrRMsCk= -github.com/gophercloud/gophercloud/v2 v2.0.0/go.mod h1:ZKbcGNjxFTSaP5wlvtLDdsppllD/UGGvXBPqcjeqA8Y= -github.com/gophercloud/utils/v2 v2.0.0-20240701101423-2401526caee5 h1:/mLIQMTyjIVfiwQkknJS9XxEPLFuB70ss+ZrofChBf8= -github.com/gophercloud/utils/v2 v2.0.0-20240701101423-2401526caee5/go.mod h1:3tI9DoiOJFBkqbOeAPqPns/QUnMCiflwYBvgR6KJdM4= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= ->>>>>>> Stashed changes github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= @@ -1099,11 +1079,7 @@ golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -<<<<<<< Updated upstream -======= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= ->>>>>>> Stashed changes golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= diff --git a/pkg/controller/hibernation/openstack_actuator.go b/pkg/controller/hibernation/openstack_actuator.go index 84a6923f815..9d510cf7538 100644 --- a/pkg/controller/hibernation/openstack_actuator.go +++ b/pkg/controller/hibernation/openstack_actuator.go @@ -27,6 +27,7 @@ type openstackActuator struct { openstackClientFn func(*hivev1.ClusterDeployment, client.Client, log.FieldLogger) (openstackclient.Client, error) } +// Create API client func getOpenStackClient(cd *hivev1.ClusterDeployment, c client.Client, logger log.FieldLogger) (openstackclient.Client, error) { ctx := context.Background() @@ -49,12 +50,12 @@ func getOpenStackClient(cd *hivev1.ClusterDeployment, c client.Client, logger lo return openstackclient.NewClientFromSecret(secret) } -// CanHandle returns true if this actuator can handle the given ClusterDeployment +// Return true if this actuator can handle the given ClusterDeployment func (a *openstackActuator) CanHandle(cd *hivev1.ClusterDeployment) bool { return cd.Spec.Platform.OpenStack != nil } -// StopMachines creates snapshots and saves configuration, then stops machines +// Create snapshots and saves configuration, then stops machines func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { logger = logger.WithField("cloud", "openstack") logger.Info("stopping machines and creating snapshots") @@ -134,14 +135,39 @@ func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClien return err } - logger.Info("waiting for OpenStack to clean up deleted instances...") - time.Sleep(30 * time.Second) + err = a.waitForInstanceCleanup(openstackClient, infraID, logger) + if err != nil { + return err + } logger.Info("hibernation completed successfully") return nil } -// validateInstanceStates checks if instances are in valid states for hibernation +// Find which instances are missing +func (a *openstackActuator) findMissingInstances(expectedInstances []OpenStackInstanceConfig, existingServers []ServerInfo, logger log.FieldLogger) []OpenStackInstanceConfig { + var missingInstances []OpenStackInstanceConfig + + // Create a map of existing instance names for quick lookup + existingNames := make(map[string]bool) + for _, server := range existingServers { + existingNames[server.Name] = true + } + + // Check which expected instances are missing + for _, expected := range expectedInstances { + if !existingNames[expected.Name] { + logger.Infof("instance %s is missing - needs to be created", expected.Name) + missingInstances = append(missingInstances, expected) + } else { + logger.Infof("instance %s already exists", expected.Name) + } + } + + return missingInstances +} + +// Check if instances are in valid states for hibernation func (a *openstackActuator) validateInstanceStates(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) error { ctx := context.Background() @@ -188,7 +214,7 @@ func (a *openstackActuator) createSnapshots(openstackClient openstackclient.Clie return snapshotIDs, nil } -// waitForSnapshots waits for all snapshots to complete +// Wait for all snapshots to complete func (a *openstackActuator) waitForSnapshots(openstackClient openstackclient.Client, snapshotIDs []string, servers []ServerInfo, logger log.FieldLogger) error { for i, snapshotID := range snapshotIDs { serverName := servers[i].Name @@ -202,7 +228,7 @@ func (a *openstackActuator) waitForSnapshots(openstackClient openstackclient.Cli return nil } -// deleteInstances deletes all instances +// Delete all instances func (a *openstackActuator) deleteInstances(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) error { ctx := context.Background() @@ -217,7 +243,7 @@ func (a *openstackActuator) deleteInstances(openstackClient openstackclient.Clie return nil } -// waitForSnapshotCompletion waits for a snapshot to reach ACTIVE state +// Wait for snapshot to reach ACTIVE state func (a *openstackActuator) waitForSnapshotCompletion(openstackClient openstackclient.Client, snapshotID, serverName string, logger log.FieldLogger) error { ctx := context.Background() maxWaitTime := 30 * time.Minute @@ -254,7 +280,7 @@ func (a *openstackActuator) waitForSnapshotCompletion(openstackClient openstackc } } -// StartMachines recreates instances from snapshots using saved configuration +// Recreate instances from snapshots using saved configuration func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { logger = logger.WithField("cloud", "openstack") logger.Info("starting machines from snapshots") @@ -270,6 +296,25 @@ func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClie return fmt.Errorf("failed to create OpenStack client: %v", err) } + // Load hibernation config first to know how many instances we should have + instances, err := a.loadHibernationConfigFromSecret(cd, hiveClient, logger) + if err != nil { + logger.Warnf("no hibernation config found: %v", err) + + // Check if we have existing instances but no hibernation config + infraID := cd.Spec.ClusterMetadata.InfraID + existingServers, checkErr := a.findInstancesByPrefix(openstackClient, infraID) + if checkErr != nil { + logger.Warnf("could not check existing instances: %v", checkErr) + } else if len(existingServers) > 0 { + logger.Info("instances exist but no hibernation config - clearing any hibernation state") + _ = a.deleteHibernationConfigSecret(cd, hiveClient, logger) + } else { + logger.Warn("cannot recreate instances without hibernation snapshots") + } + return nil + } + // Check for existing instances infraID := cd.Spec.ClusterMetadata.InfraID existingServers, err := a.findInstancesByPrefix(openstackClient, infraID) @@ -277,43 +322,177 @@ func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClie logger.Warnf("could not check existing instances: %v", err) } - if len(existingServers) > 0 { - logger.Info("instances already exist - clearing hibernation config") + // Check if we already have all the instances we need + if len(existingServers) >= len(instances) { + logger.Info("sufficient instances already exist - clearing hibernation config") _ = a.deleteHibernationConfigSecret(cd, hiveClient, logger) // Best effort cleanup return nil } - // Load hibernation config and restore - instances, err := a.loadHibernationConfigFromSecret(cd, hiveClient, logger) + logger.Infof("restoring %d instances from hibernation snapshots (currently have %d)", len(instances), len(existingServers)) + + return a.restoreFromHibernationConfig(cd, hiveClient, openstackClient, instances, logger) +} + +// Validate project resource quotas prior to the restoration +func (a *openstackActuator) validateRestoreResources(openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { + ctx := context.Background() + + logger.Info("checking OpenStack quotas before restoration...") + + // Get current quotas and usage + quotas, err := openstackClient.GetComputeQuotas(ctx) if err != nil { - logger.Warnf("no hibernation config found: %v", err) - logger.Warn("cannot recreate instances without hibernation snapshots") - return nil // Don't fail - let controller handle this gracefully + return fmt.Errorf("failed to get compute quotas: %v", err) } - logger.Infof("restoring %d instances from hibernation snapshots", len(instances)) - return a.restoreFromHibernationConfig(cd, hiveClient, openstackClient, instances, logger) + usage, err := openstackClient.GetComputeUsage(ctx) + if err != nil { + return fmt.Errorf("failed to get compute usage: %v", err) + } + + // Calculate requirements for restoration + requirements, err := a.calculateResourceRequirements(openstackClient, instances, logger) + if err != nil { + return fmt.Errorf("failed to calculate resource requirements: %v", err) + } + + logger.Infof("restoration requires: %d instances, %d vCPUs, %d MB RAM", + requirements.Instances, requirements.VCPUs, requirements.RAM) + + // Check available resources + availableInstances := quotas.Instances - usage.InstancesUsed + availableVCPUs := quotas.Cores - usage.CoresUsed + availableRAM := quotas.RAM - usage.RAMUsed + + logger.Infof("available resources: %d instances, %d vCPUs, %d MB RAM", + availableInstances, availableVCPUs, availableRAM) + + // Validate sufficient resources + var errors []string + + if requirements.Instances > availableInstances { + errors = append(errors, fmt.Sprintf("insufficient instances: need %d, have %d available", + requirements.Instances, availableInstances)) + } + + if requirements.VCPUs > availableVCPUs { + errors = append(errors, fmt.Sprintf("insufficient vCPUs: need %d, have %d available", + requirements.VCPUs, availableVCPUs)) + } + + if requirements.RAM > availableRAM { + errors = append(errors, fmt.Sprintf("insufficient RAM: need %d MB, have %d MB available", + requirements.RAM, availableRAM)) + } + + if len(errors) > 0 { + return fmt.Errorf("insufficient OpenStack resources for restoration: %s", + strings.Join(errors, "; ")) + } + + logger.Info("sufficient resources available for restoration") + return nil } -// restoreFromHibernationConfig recreates instances from hibernation configuration -func (a *openstackActuator) restoreFromHibernationConfig(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { +// Calculate required project resources +func (a *openstackActuator) calculateResourceRequirements(openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) (*openstackclient.ResourceRequirements, error) { ctx := context.Background() + requirements := &openstackclient.ResourceRequirements{} + + // Track unique flavors to avoid duplicate API calls + flavorCache := make(map[string]*openstackclient.FlavorDetails) + + for _, instance := range instances { + requirements.Instances++ + + // Get flavor details + var flavor *openstackclient.FlavorDetails + if cached, exists := flavorCache[instance.Flavor]; exists { + flavor = cached + } else { + var err error + flavor, err = openstackClient.GetFlavorDetails(ctx, instance.Flavor) + if err != nil { + return nil, fmt.Errorf("failed to get flavor %s details: %v", instance.Flavor, err) + } + flavorCache[instance.Flavor] = flavor + } + + requirements.VCPUs += flavor.VCPUs + requirements.RAM += flavor.RAM + + logger.Infof("instance %s (flavor %s): %d vCPUs, %d MB RAM", + instance.Name, flavor.Name, flavor.VCPUs, flavor.RAM) + } + + return requirements, nil +} + +// Recreate instances from hibernation configuration +func (a *openstackActuator) restoreFromHibernationConfig(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { + infraID := cd.Spec.ClusterMetadata.InfraID logger.Infof("restoring %d instances from hibernation", len(instances)) - // Recreate each instance using saved configuration - createdServerIDs := make([]string, 0, len(instances)) + // Validate sufficient resources before starting restoration + err := a.validateRestoreResources(openstackClient, instances, logger) + if err != nil { + return fmt.Errorf("resource validation failed: %v", err) + } - for i, instance := range instances { - logger.Infof("creating instance %d/%d: %s", i+1, len(instances), instance.Name) + // Check what instances already exist + existingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + if err != nil { + logger.Warnf("could not check existing instances: %v", err) + existingServers = []ServerInfo{} // Assume none exist + } - // Validate snapshot exists + // Figure out what we need to create + instancesToCreate := a.findMissingInstances(instances, existingServers, logger) + + if len(instancesToCreate) == 0 { + logger.Info("all instances already exist - restoration complete") + return a.deleteHibernationConfigSecret(cd, hiveClient, logger) + } + + logger.Infof("need to create %d missing instances", len(instancesToCreate)) + + // Create missing instances + err = a.createMissingInstances(openstackClient, instancesToCreate, logger) + if err != nil { + logger.Errorf("some instance creation failed: %v", err) + } + + // Wait for instances to be active + err = a.waitForAllInstancesToBeActive(openstackClient, infraID, len(instances), logger) + if err != nil { + return fmt.Errorf("not all instances are active yet: %v", err) + } + + // Only clear hibernation config when we have the instances running + logger.Info("all instances confirmed active - clearing hibernation configuration") + return a.deleteHibernationConfigSecret(cd, hiveClient, logger) +} + +// Create missing instances during restoration +func (a *openstackActuator) createMissingInstances(openstackClient openstackclient.Client, instancesToCreate []OpenStackInstanceConfig, logger log.FieldLogger) error { + ctx := context.Background() + var errors []string + + for i, instance := range instancesToCreate { + logger.Infof("creating missing instance %d/%d: %s", i+1, len(instancesToCreate), instance.Name) + + // Validate snapshot still exists _, err := openstackClient.GetImage(ctx, instance.SnapshotID) if err != nil { - return fmt.Errorf("snapshot %s not found: %v", instance.SnapshotID, err) + errorMsg := fmt.Sprintf("snapshot %s not found for %s: %v", instance.SnapshotID, instance.Name, err) + errors = append(errors, errorMsg) + logger.Error(errorMsg) + continue } - // Build server creation options + // Build server creation options with complete metadata createOpts := &openstackclient.ServerCreateOpts{ Name: instance.Name, ImageRef: instance.SnapshotID, @@ -321,42 +500,93 @@ func (a *openstackActuator) restoreFromHibernationConfig(cd *hivev1.ClusterDeplo NetworkID: instance.NetworkID, PortID: instance.PortID, SecurityGroups: instance.SecurityGroups, - Metadata: map[string]string{ - "openshiftClusterID": instance.OpenshiftClusterID, - }, + Metadata: instance.Metadata, } newServer, err := openstackClient.CreateServerFromOpts(ctx, createOpts) if err != nil { - return fmt.Errorf("failed to create instance %s: %v", instance.Name, err) + errorMsg := fmt.Sprintf("failed to create instance %s: %v", instance.Name, err) + errors = append(errors, errorMsg) + logger.Error(errorMsg) + continue } - createdServerIDs = append(createdServerIDs, newServer.ID) logger.Infof("created instance %s (ID: %s)", instance.Name, newServer.ID) - } - // Wait for instances to become ACTIVE - logger.Info("waiting for instances to become active") - for i, serverID := range createdServerIDs { - instanceName := instances[i].Name - err := a.waitForServerActive(openstackClient, serverID, instanceName, logger) - if err != nil { - return fmt.Errorf("failed to wait for instance %s to become active: %v", instanceName, err) + // Tags are handled separately after instance creation + if len(instance.Tags) > 0 { + err = openstackClient.SetServerTags(ctx, newServer.ID, instance.Tags) + if err != nil { + logger.Warnf("failed to restore tags for %s: %v", instance.Name, err) + } else { + logger.Infof("restored %d tags for %s: %v", len(instance.Tags), instance.Name, instance.Tags) + } } } - // Clear hibernation config since restoration is complete - logger.Info("restoration completed - clearing hibernation configuration") - err := a.deleteHibernationConfigSecret(cd, hiveClient, logger) - if err != nil { - logger.Warnf("could not clear hibernation config: %v", err) + if len(errors) > 0 { + return fmt.Errorf("some instance creation failed: %s", strings.Join(errors, "; ")) } - logger.Info("restoration completed successfully") return nil } -// waitForServerActive waits for a server to reach ACTIVE state +// Wait for ALL instances to be active +func (a *openstackActuator) waitForAllInstancesToBeActive(openstackClient openstackclient.Client, infraID string, expectedCount int, logger log.FieldLogger) error { + maxWaitTime := 30 * time.Minute + checkInterval := 15 * time.Second + timeout := time.After(maxWaitTime) + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + + logger.Infof("waiting for all %d instances to become active", expectedCount) + + for { + select { + case <-timeout: + return fmt.Errorf("timeout waiting for all instances to become active after %v", maxWaitTime) + case <-ticker.C: + // Get current instances + currentServers, err := a.findInstancesByPrefix(openstackClient, infraID) + if err != nil { + logger.Warnf("error checking instance status: %v", err) + continue + } + + if len(currentServers) != expectedCount { + logger.Infof("have %d instances, expecting %d - still waiting", len(currentServers), expectedCount) + continue + } + + // Check if all instances are ACTIVE + activeCount := 0 + var nonActiveInstances []string + + for _, server := range currentServers { + serverDetails, err := openstackClient.GetServer(context.Background(), server.ID) + if err != nil { + logger.Warnf("could not get server %s details: %v", server.Name, err) + continue + } + + if serverDetails.Status == "ACTIVE" { + activeCount++ + } else { + nonActiveInstances = append(nonActiveInstances, fmt.Sprintf("%s(%s)", server.Name, serverDetails.Status)) + } + } + + if activeCount == expectedCount { + logger.Info("all instances are active!") + return nil + } + + logger.Infof("%d/%d instances active, waiting for: %v", activeCount, expectedCount, nonActiveInstances) + } + } +} + +// wait for a server to reach ACTIVE state func (a *openstackActuator) waitForServerActive(openstackClient openstackclient.Client, serverID, serverName string, logger log.FieldLogger) error { ctx := context.Background() maxWaitTime := 30 * time.Minute @@ -415,7 +645,7 @@ func (a *openstackActuator) MachinesRunning(cd *hivev1.ClusterDeployment, hiveCl return false, []string{"no instances found"}, nil } - // OpenStack-specific: Check actual instance states + // Check actual instance states runningCount, deletingInstances := a.categorizeInstanceStates(openstackClient, matchingServers, logger) // If instances are being deleted, hibernation is in progress @@ -427,7 +657,7 @@ func (a *openstackActuator) MachinesRunning(cd *hivev1.ClusterDeployment, hiveCl return runningCount > 0, []string{}, nil } -// categorizeInstanceStates checks the actual state of instances in OpenStack +// Check the actual state of instances in OpenStack func (a *openstackActuator) categorizeInstanceStates(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) (int, []string) { ctx := context.Background() runningCount := 0 @@ -454,7 +684,7 @@ func (a *openstackActuator) categorizeInstanceStates(openstackClient openstackcl return runningCount, deletingInstances } -// MachinesStopped checks if machines are stopped +// Check if machines are stopped func (a *openstackActuator) MachinesStopped(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) (bool, []string, error) { logger = logger.WithField("cloud", "openstack") logger.Info("checking if machines are stopped") @@ -484,24 +714,26 @@ func (a *openstackActuator) MachinesStopped(cd *hivev1.ClusterDeployment, hiveCl return false, notStopped, nil } -// ServerInfo holds basic server information +// basic server information type ServerInfo struct { ID string Name string } type OpenStackInstanceConfig struct { - Name string `json:"name"` - Flavor string `json:"flavor"` - PortID string `json:"portID"` - SnapshotID string `json:"snapshotID"` - SecurityGroups []string `json:"securityGroups"` - ClusterID string `json:"clusterID"` - NetworkID string `json:"networkID"` - OpenshiftClusterID string `json:"openshiftClusterID"` + Name string `json:"name"` + Flavor string `json:"flavor"` + PortID string `json:"portID"` + SnapshotID string `json:"snapshotID"` + SecurityGroups []string `json:"securityGroups"` + ClusterID string `json:"clusterID"` + NetworkID string `json:"networkID"` + OpenshiftClusterID string `json:"openshiftClusterID"` + Metadata map[string]string `json:"metadata"` + Tags []string `json:"tags"` } -// findInstancesByPrefix returns servers that match the infraID prefix +// Return servers that match the infraID prefix func (a *openstackActuator) findInstancesByPrefix(openstackClient openstackclient.Client, prefix string) ([]ServerInfo, error) { ctx := context.Background() @@ -535,7 +767,7 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster return fmt.Errorf("mismatch between servers (%d) and snapshot IDs (%d)", len(matchingServers), len(snapshotIDs)) } - // Use infraID directly instead of extracting from instance name + // Get InfraID infraID := cd.Spec.ClusterMetadata.InfraID // Get shared configuration @@ -557,7 +789,7 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster } } - // Get all ports once for efficiency + // Get all ports allPorts, err := openstackClient.ListPorts(ctx) if err != nil { return fmt.Errorf("error listing ports: %v", err) @@ -571,6 +803,13 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster return fmt.Errorf("error getting server details for %s: %v", serverInfo.Name, err) } + // Get server tags + serverTags, err := openstackClient.GetServerTags(ctx, serverInfo.ID) + if err != nil { + logger.Warnf("could not get tags for %s: %v", serverInfo.Name, err) + serverTags = []string{} // Use empty tags if we can't get them + } + // Get flavor ID var flavorID string if serverDetails.Flavor != nil { @@ -608,15 +847,22 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster PortID: portID, SnapshotID: snapshotIDs[i], SecurityGroups: secGroups, - ClusterID: infraID, // Use infraID directly + ClusterID: infraID, NetworkID: networkID, OpenshiftClusterID: openshiftClusterID, + Metadata: serverDetails.Metadata, + Tags: serverTags, }) + + logger.Infof("captured metadata for %s: %d properties, %d tags", + serverInfo.Name, len(serverDetails.Metadata), len(serverTags)) } return a.saveHibernationConfigToSecret(cd, hiveClient, instanceConfigs, logger) } + +// Store hibernation information to secrets func (a *openstackActuator) saveHibernationConfigToSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, instanceConfigs []OpenStackInstanceConfig, logger log.FieldLogger) error { ctx := context.Background() @@ -673,6 +919,43 @@ func (a *openstackActuator) saveHibernationConfigToSecret(cd *hivev1.ClusterDepl return nil } +// Waits for OpenStack to fully remove an instance +func (a *openstackActuator) waitForInstanceCleanup(openstackClient openstackclient.Client, infraID string, logger log.FieldLogger) error { + maxWaitTime := 5 * time.Minute // Maximum time to wait for cleanup + checkInterval := 5 * time.Second // Check every 5 seconds + timeout := time.After(maxWaitTime) + ticker := time.NewTicker(checkInterval) + defer ticker.Stop() + + logger.Info("waiting for OpenStack to clean up deleted instances...") + + for { + select { + case <-timeout: + return fmt.Errorf("timeout waiting for instance cleanup after %v", maxWaitTime) + case <-ticker.C: + matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + if err != nil { + logger.Warnf("error checking for remaining instances: %v", err) + continue // Continue polling despite errors + } + + if len(matchingServers) == 0 { + logger.Info("all instances have been cleaned up") + return nil + } + + // Log remaining instances + var instanceNames []string + for _, server := range matchingServers { + instanceNames = append(instanceNames, server.Name) + } + logger.Infof("still waiting for %d instances to be cleaned up: %v", len(matchingServers), instanceNames) + } + } +} + +// Get stored hibernation information from secret func (a *openstackActuator) loadHibernationConfigFromSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) ([]OpenStackInstanceConfig, error) { ctx := context.Background() secretName := fmt.Sprintf("%s-hibernation-config", cd.Name) @@ -701,6 +984,7 @@ func (a *openstackActuator) loadHibernationConfigFromSecret(cd *hivev1.ClusterDe return instanceConfigs, nil } +// Delete stored hibernation information from secret func (a *openstackActuator) deleteHibernationConfigSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { ctx := context.Background() secretName := fmt.Sprintf("%s-hibernation-config", cd.Name) @@ -724,7 +1008,7 @@ func (a *openstackActuator) deleteHibernationConfigSecret(cd *hivev1.ClusterDepl return nil } -// getNetworkIDForCluster finds the network ID for a specific cluster using infraID +// Find the network ID for a specific cluster using infraID func (a *openstackActuator) getNetworkIDForCluster(openstackClient openstackclient.Client, infraID string) (string, error) { ctx := context.Background() networkName := fmt.Sprintf("%s-openshift", infraID) diff --git a/pkg/controller/hibernation/openstack_actuator_test.go b/pkg/controller/hibernation/openstack_actuator_test.go new file mode 100644 index 00000000000..f9ce2cff522 --- /dev/null +++ b/pkg/controller/hibernation/openstack_actuator_test.go @@ -0,0 +1,492 @@ +package hibernation + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + + "github.com/golang/mock/gomock" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + hivev1 "github.com/openshift/hive/apis/hive/v1" + hivev1openstack "github.com/openshift/hive/apis/hive/v1/openstack" + "github.com/openshift/hive/pkg/openstackclient" + mockopenstackclient "github.com/openshift/hive/pkg/openstackclient/mock" + testcd "github.com/openshift/hive/pkg/test/clusterdeployment" + testfake "github.com/openshift/hive/pkg/test/fake" + "github.com/openshift/hive/pkg/util/scheme" +) + +func TestOpenStackCanHandle(t *testing.T) { + cd := testcd.BasicBuilder().Options(func(cd *hivev1.ClusterDeployment) { + cd.Spec.Platform.OpenStack = &hivev1openstack.Platform{} + }).Build() + actuator := openstackActuator{} + assert.True(t, actuator.CanHandle(cd)) + + cd = testcd.BasicBuilder().Build() + assert.False(t, actuator.CanHandle(cd)) +} + +func TestOpenStackStopMachines(t *testing.T) { + tests := []struct { + name string + instances map[string]int + setupClient func(*testing.T, *mockopenstackclient.MockClient) + expectErr bool + }{ + { + name: "stop no running instances", + instances: map[string]int{}, + }, + { + name: "stop running instances", + instances: map[string]int{"ACTIVE": 2}, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // Expect snapshot creation + c.EXPECT().CreateServerSnapshot(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return("snapshot-1", nil) + + // Expect image status checks + activeImg := &images.Image{} + activeImg.Status = "active" + c.EXPECT().GetImage(gomock.Any(), gomock.Any()).Return(activeImg, nil).AnyTimes() + + // Expect instance deletion + c.EXPECT().DeleteServer(gomock.Any(), gomock.Any()).Times(2).Return(nil) + + // Network operations + setupOpenStackNetworkOps(c) + + // Tag operations for hibernation (capture tags) + setupOpenStackTagOps(c) + + // OVERRIDE the default ListServers to simulate cleanup progression + gomock.InOrder( + // Initial calls: return instances + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, + {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, + }, nil).Times(2), + + // First cleanup poll: still there + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0"}, + {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1"}, + }, nil).Times(1), + + // Subsequent cleanup polls: instances gone + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).AnyTimes(), + ) + }, + }, + { + name: "unable to list servers", + instances: map[string]int{"ACTIVE": 2}, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return(nil, errors.New("cannot list servers")) + }, + expectErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + openstackClient := mockopenstackclient.NewMockClient(ctrl) + if !test.expectErr { + setupOpenStackClientInstances(openstackClient, test.instances) + } + if test.setupClient != nil { + test.setupClient(t, openstackClient) + } + + actuator := testOpenStackActuator(openstackClient) + c := testfake.NewFakeClientBuilder().Build() + err := actuator.StopMachines(testOpenStackClusterDeployment(), c, log.New()) + + if test.expectErr { + assert.NotNil(t, err) + } else { + assert.Nil(t, err) + } + }) + } +} + +func TestOpenStackStartMachines(t *testing.T) { + tests := []struct { + name string + instances map[string]int + setupClient func(*testing.T, *mockopenstackclient.MockClient) + withSecret bool + }{ + { + name: "start with existing instances", + instances: map[string]int{"ACTIVE": 2}, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // Setup ListServers for existing instances check + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, + {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, + }, nil).AnyTimes() + }, + }, + { + name: "start from hibernation config with quota validation", + instances: map[string]int{}, // No existing instances + withSecret: true, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // Setup quota checking mocks (sufficient resources) + setupOpenStackQuotaMocks(c) + + // Expect image validation + activeImg := &images.Image{} + activeImg.Status = "active" + c.EXPECT().GetImage(gomock.Any(), gomock.Any()).Return(activeImg, nil).AnyTimes() + + // STEP 1: First few calls should return NO instances (so creation happens) + // STEP 2: After creation, return the created instances + gomock.InOrder( + // First call: no existing instances (for existing instance check) + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), + + // Second call: still no instances (for missing instances check) + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), + + // After creation: return the newly created instances + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "new-testinfra-master-0", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "new-testinfra-worker-0", Name: "testinfra-worker-0", Status: "ACTIVE"}, + }, nil).AnyTimes(), + ) + + // Expect server creation + c.EXPECT().CreateServerFromOpts(gomock.Any(), gomock.Any()).Times(2).DoAndReturn( + func(ctx context.Context, opts *openstackclient.ServerCreateOpts) (*servers.Server, error) { + return &servers.Server{ + ID: fmt.Sprintf("new-%s", opts.Name), + Name: opts.Name, + Status: "BUILD", + }, nil + }) + + // Expect tag restoration after server creation + c.EXPECT().SetServerTags(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(nil) + + // Expect status checks for waiting + activeServer := &servers.Server{Status: "ACTIVE"} + c.EXPECT().GetServer(gomock.Any(), gomock.Any()).Return(activeServer, nil).AnyTimes() + }, + }, + { + name: "start from hibernation config - insufficient quota", + instances: map[string]int{}, // No existing instances + withSecret: true, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // Setup quota checking mocks (insufficient resources) + setupOpenStackQuotaMocksInsufficient(c) + + // First call: no existing instances + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + openstackClient := mockopenstackclient.NewMockClient(ctrl) + + // Only call setupOpenStackClientInstances for simple tests, not hibernation config tests + if !test.withSecret { + setupOpenStackClientInstances(openstackClient, test.instances) + } + + if test.setupClient != nil { + test.setupClient(t, openstackClient) + } + + var c client.Client + if test.withSecret { + c = testfake.NewFakeClientBuilder().WithRuntimeObjects(testHibernationSecretWithMetadata()).Build() + } else { + c = testfake.NewFakeClientBuilder().Build() + } + + actuator := testOpenStackActuator(openstackClient) + err := actuator.StartMachines(testOpenStackClusterDeployment(), c, log.New()) + + // Check expectations based on test case + if test.name == "start from hibernation config - insufficient quota" { + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "resource validation failed") + } else { + assert.Nil(t, err) + } + }) + } +} + +// Test quota checking functionality separately +func TestOpenStackQuotaValidation(t *testing.T) { + tests := []struct { + name string + setupClient func(*testing.T, *mockopenstackclient.MockClient) + expectErr bool + }{ + { + name: "sufficient quotas", + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + setupOpenStackQuotaMocks(c) + }, + expectErr: false, + }, + { + name: "insufficient quotas", + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + setupOpenStackQuotaMocksInsufficient(c) + }, + expectErr: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + openstackClient := mockopenstackclient.NewMockClient(ctrl) + test.setupClient(t, openstackClient) + + actuator := testOpenStackActuator(openstackClient) + + // Create test instances for validation + instances := []OpenStackInstanceConfig{ + {Name: "test-master-0", Flavor: "flavor-1"}, + {Name: "test-worker-0", Flavor: "flavor-2"}, + } + + err := actuator.validateRestoreResources(openstackClient, instances, log.New()) + + if test.expectErr { + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "insufficient") + } else { + assert.Nil(t, err) + } + }) + } +} + +func testOpenStackActuator(openstackClient openstackclient.Client) *openstackActuator { + return &openstackActuator{ + openstackClientFn: func(*hivev1.ClusterDeployment, client.Client, log.FieldLogger) (openstackclient.Client, error) { + return openstackClient, nil + }, + } +} + +func setupOpenStackClientInstances(openstackClient *mockopenstackclient.MockClient, statuses map[string]int) { + var allServers []servers.Server + for status, count := range statuses { + for i := 0; i < count; i++ { + serverName := fmt.Sprintf("%s-%d", status, i) + server := servers.Server{ + ID: fmt.Sprintf("testinfra-%s", serverName), + Name: fmt.Sprintf("testinfra-%s", serverName), + Status: status, + Flavor: map[string]interface{}{"id": "flavor-1"}, + } + allServers = append(allServers, server) + } + } + + // Only setup ListServers for simple tests (empty instances) + if len(allServers) == 0 { + openstackClient.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return(allServers, nil).AnyTimes() + } + + // Setup GetServer calls + for _, server := range allServers { + openstackClient.EXPECT().GetServer(gomock.Any(), server.ID).Return(&server, nil).AnyTimes() + } +} + +func setupOpenStackNetworkOps(openstackClient *mockopenstackclient.MockClient) { + // Network operations + network := &openstackclient.Network{ + ID: "network-1", + Name: "testinfra-openshift", + } + openstackClient.EXPECT().GetNetworkByName(gomock.Any(), "testinfra-openshift").Return(network, nil).AnyTimes() + + // Port listing + ports := []openstackclient.Port{ + {ID: "port-1", Name: "testinfra-ACTIVE-0"}, + {ID: "port-2", Name: "testinfra-ACTIVE-1"}, + } + openstackClient.EXPECT().ListPorts(gomock.Any()).Return(ports, nil).AnyTimes() + + // Security groups + openstackClient.EXPECT().GetServerSecurityGroups(gomock.Any(), gomock.Any()).Return([]string{"default"}, nil).AnyTimes() +} + +// Setup tag operations for tests +func setupOpenStackTagOps(openstackClient *mockopenstackclient.MockClient) { + // Expect tag retrieval during hibernation (capture tags) + masterTags := []string{"openshiftClusterID=testinfra"} + workerTags := []string{"cluster-api-provider-openstack", "openshift-machine-api-testinfra", "openshiftClusterID=testinfra"} + + // Mock tag calls for different servers + openstackClient.EXPECT().GetServerTags(gomock.Any(), "testinfra-ACTIVE-0").Return(masterTags, nil).AnyTimes() + openstackClient.EXPECT().GetServerTags(gomock.Any(), "testinfra-ACTIVE-1").Return(workerTags, nil).AnyTimes() +} + +// Setup quota mocks with sufficient resources +func setupOpenStackQuotaMocks(openstackClient *mockopenstackclient.MockClient) { + // Mock quotas - plenty of resources available + quotas := &openstackclient.ComputeQuotas{ + Instances: 20, + Cores: 40, + RAM: 81920, + } + openstackClient.EXPECT().GetComputeQuotas(gomock.Any()).Return(quotas, nil).AnyTimes() + + // Mock usage - minimal usage + usage := &openstackclient.ComputeUsage{ + InstancesUsed: 0, + CoresUsed: 0, + RAMUsed: 0, + } + openstackClient.EXPECT().GetComputeUsage(gomock.Any()).Return(usage, nil).AnyTimes() + + // Mock flavor details for our test flavors + flavorDetails := &openstackclient.FlavorDetails{ + ID: "flavor-1", + Name: "m1.small", + VCPUs: 2, + RAM: 2048, // 2GB in MB + Disk: 10, + } + openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-1").Return(flavorDetails, nil).AnyTimes() + + // Also mock flavor-2 for worker + flavorDetails2 := &openstackclient.FlavorDetails{ + ID: "flavor-2", + Name: "m1.medium", + VCPUs: 4, + RAM: 4096, // 4GB in MB + Disk: 20, + } + openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-2").Return(flavorDetails2, nil).AnyTimes() +} + +// Setup quota mocks with insufficient resources +func setupOpenStackQuotaMocksInsufficient(openstackClient *mockopenstackclient.MockClient) { + // Mock quotas - very limited resources + quotas := &openstackclient.ComputeQuotas{ + Instances: 2, + Cores: 4, // Only 4 vCPUs total + RAM: 4096, // Only 4GB total + } + openstackClient.EXPECT().GetComputeQuotas(gomock.Any()).Return(quotas, nil).AnyTimes() + + // Mock usage - most resources already used + usage := &openstackclient.ComputeUsage{ + InstancesUsed: 1, + CoresUsed: 3, // 3 out of 4 vCPUs used + RAMUsed: 3072, // 3GB out of 4GB used + } + openstackClient.EXPECT().GetComputeUsage(gomock.Any()).Return(usage, nil).AnyTimes() + + // Mock flavor details - will show insufficient resources + flavorDetails1 := &openstackclient.FlavorDetails{ + ID: "flavor-1", + Name: "m1.small", + VCPUs: 2, + RAM: 2048, // 2GB in MB + Disk: 10, + } + openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-1").Return(flavorDetails1, nil).AnyTimes() + + // Also mock flavor-2 for worker + flavorDetails2 := &openstackclient.FlavorDetails{ + ID: "flavor-2", + Name: "m1.medium", + VCPUs: 4, + RAM: 4096, // 4GB in MB + Disk: 20, + } + openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-2").Return(flavorDetails2, nil).AnyTimes() +} + +func testOpenStackClusterDeployment() *hivev1.ClusterDeployment { + scheme := scheme.GetScheme() + cdBuilder := testcd.FullBuilder("testns", "testopenstackcluster", scheme) + return cdBuilder.Build( + testcd.WithOpenStackPlatform(&hivev1openstack.Platform{Cloud: "openstack"}), + testcd.WithClusterMetadata(&hivev1.ClusterMetadata{InfraID: "testinfra"}), + testcd.WithPowerState(hivev1.ClusterPowerStateRunning), + ) +} + +// hibernation secret with complete metadata and tags +func testHibernationSecretWithMetadata() *corev1.Secret { + config := []OpenStackInstanceConfig{ + { + Name: "testinfra-master-0", + Flavor: "flavor-1", + PortID: "port-1", + SnapshotID: "snapshot-1", + SecurityGroups: []string{"default"}, + ClusterID: "testinfra", + NetworkID: "network-1", + OpenshiftClusterID: "testinfra", + Metadata: map[string]string{ + "Name": "testinfra-master", + "openshiftClusterID": "testinfra", + }, + Tags: []string{"openshiftClusterID=testinfra"}, + }, + { + Name: "testinfra-worker-0", + Flavor: "flavor-2", + PortID: "port-2", + SnapshotID: "snapshot-2", + SecurityGroups: []string{"default"}, + ClusterID: "testinfra", + NetworkID: "network-1", + OpenshiftClusterID: "testinfra", + Metadata: map[string]string{ + "Name": "testinfra-worker", + "openshiftClusterID": "testinfra", + }, + Tags: []string{"cluster-api-provider-openstack", "openshift-machine-api-testinfra", "openshiftClusterID=testinfra"}, + }, + } + + configData, _ := json.Marshal(config) + + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testopenstackcluster-hibernation-config", + Namespace: "testns", + }, + Data: map[string][]byte{ + "hibernation-config": configData, + }, + } +} diff --git a/pkg/openstackclient/client.go b/pkg/openstackclient/client.go index 0d4b01b1b2c..45d53d3989c 100644 --- a/pkg/openstackclient/client.go +++ b/pkg/openstackclient/client.go @@ -9,7 +9,11 @@ import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tags" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" @@ -21,38 +25,71 @@ import ( // Client is a wrapper object for actual OpenStack libraries to allow for easier mocking/testing. type Client interface { - // Servers - only what you actually use + // Servers ListServers(ctx context.Context, opts *servers.ListOpts) ([]servers.Server, error) GetServer(ctx context.Context, serverID string) (*servers.Server, error) DeleteServer(ctx context.Context, serverID string) error CreateServerSnapshot(ctx context.Context, serverID, snapshotName string) (string, error) CreateServerFromOpts(ctx context.Context, opts *ServerCreateOpts) (*servers.Server, error) + // Tags + SetServerTags(ctx context.Context, serverID string, serverTags []string) error + GetServerTags(ctx context.Context, serverID string) ([]string, error) + // Images - only snapshot checking GetImage(ctx context.Context, imageID string) (*images.Image, error) - // Networks - only what you need + // Networks GetNetworkByName(ctx context.Context, networkName string) (*Network, error) ListPorts(ctx context.Context) ([]Port, error) - // Security Groups - only reading + // Security Groups GetServerSecurityGroups(ctx context.Context, serverID string) ([]string, error) + + // Project resources + GetComputeQuotas(ctx context.Context) (*ComputeQuotas, error) + GetComputeUsage(ctx context.Context) (*ComputeUsage, error) + GetFlavorDetails(ctx context.Context, flavorID string) (*FlavorDetails, error) } -// Network represents an OpenStack network type Network struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` } -// Port represents an OpenStack port type Port struct { ID string `json:"id"` Name string `json:"name"` } -// ServerCreateOpts contains options for creating a server +type ComputeQuotas struct { + Instances int `json:"instances"` + Cores int `json:"cores"` + RAM int `json:"ram"` +} + +type ComputeUsage struct { + InstancesUsed int `json:"totalInstancesUsed"` + CoresUsed int `json:"totalCoresUsed"` + RAMUsed int `json:"totalRAMUsed"` +} + +type FlavorDetails struct { + ID string `json:"id"` + Name string `json:"name"` + VCPUs int `json:"vcpus"` + RAM int `json:"ram"` + Disk int `json:"disk"` +} + +type ResourceRequirements struct { + Instances int + VCPUs int + RAM int +} + +// Options for creating a server type ServerCreateOpts struct { Name string `json:"name"` ImageRef string `json:"imageRef"` @@ -63,7 +100,7 @@ type ServerCreateOpts struct { Metadata map[string]string `json:"metadata"` } -// OpenStack credentials structure - matches clouds.yaml format +// OpenStack credentials structure type Credentials struct { AuthURL string `json:"auth_url"` Username string `json:"username,omitempty"` @@ -79,7 +116,6 @@ type Credentials struct { Interface string `json:"interface,omitempty"` IdentityAPIVersion string `json:"identity_api_version,omitempty"` - // Legacy support TenantID string `json:"tenant_id,omitempty"` TenantName string `json:"tenant_name,omitempty"` DomainID string `json:"domain_id,omitempty"` @@ -139,7 +175,7 @@ func (c *openstackClient) DeleteServer(ctx context.Context, serverID string) err return servers.Delete(c.computeClient, serverID).ExtractErr() } -// CreateServerSnapshot creates a snapshot (image) of the specified server +// Create a snapshot of the specified server func (c *openstackClient) CreateServerSnapshot(ctx context.Context, serverID, snapshotName string) (string, error) { // Create image options createImageOpts := servers.CreateImageOpts{ @@ -150,7 +186,6 @@ func (c *openstackClient) CreateServerSnapshot(ctx context.Context, serverID, sn }, } - // Create the snapshot/image result := servers.CreateImage(c.computeClient, serverID, createImageOpts) imageID, err := result.ExtractImageID() if err != nil { @@ -160,7 +195,7 @@ func (c *openstackClient) CreateServerSnapshot(ctx context.Context, serverID, sn return imageID, nil } -// CreateServerFromOpts creates a new server using our custom ServerCreateOpts +// Creates a new server func (c *openstackClient) CreateServerFromOpts(ctx context.Context, opts *ServerCreateOpts) (*servers.Server, error) { // Build networks slice for the NIC networks := []servers.Network{ @@ -170,7 +205,7 @@ func (c *openstackClient) CreateServerFromOpts(ctx context.Context, opts *Server }, } - // Convert our custom options to Gophercloud options + // Convert custom options to satisfy GopherCloud createOpts := &servers.CreateOpts{ Name: opts.Name, ImageRef: opts.ImageRef, @@ -188,13 +223,13 @@ func (c *openstackClient) CreateServerFromOpts(ctx context.Context, opts *Server return server, nil } -// Implementation of image methods +// Image methods func (c *openstackClient) GetImage(ctx context.Context, imageID string) (*images.Image, error) { image, err := images.Get(c.imageClient, imageID).Extract() return image, err } -// Port implementations +// Ports func (c *openstackClient) ListPorts(ctx context.Context) ([]Port, error) { allPages, err := ports.List(c.networkClient, nil).AllPages() if err != nil { @@ -217,6 +252,7 @@ func (c *openstackClient) ListPorts(ctx context.Context) ([]Port, error) { return result, nil } +// Network specific func (c *openstackClient) GetNetworkByName(ctx context.Context, networkName string) (*Network, error) { listOpts := networks.ListOpts{ Name: networkName, @@ -245,7 +281,7 @@ func (c *openstackClient) GetNetworkByName(ctx context.Context, networkName stri }, nil } -// GetServerSecurityGroups gets the security group names for a specific server +// Get the security group names for a specific server func (c *openstackClient) GetServerSecurityGroups(ctx context.Context, serverID string) ([]string, error) { serverSecGroups, err := secgroups.ListByServer(c.computeClient, serverID).AllPages() if err != nil { @@ -265,26 +301,120 @@ func (c *openstackClient) GetServerSecurityGroups(ctx context.Context, serverID return secGroupNames, nil } -// NewClientFromSecret creates our client wrapper object for interacting with OpenStack. -// The OpenStack creds are read from the specified secret. -func NewClientFromSecret(secret *corev1.Secret) (Client, error) { - // Check if it's a clouds.yaml format - if cloudsYaml, ok := secret.Data["clouds.yaml"]; ok { - return newClientFromCloudsYAML(cloudsYaml) +// Adding tags +func (c *openstackClient) SetServerTags(ctx context.Context, serverID string, serverTags []string) error { + // Delete all existing tags first + err := tags.DeleteAll(c.computeClient, serverID).ExtractErr() + if err != nil { + return fmt.Errorf("failed to clear existing tags for server %s: %w", serverID, err) } - // Handle JSON credentials format directly - authJSON, ok := secret.Data["credentials"] - if !ok { - return nil, fmt.Errorf("secret does not contain \"credentials\" or \"clouds.yaml\" data") + // Add new tags one by one if any exist + for _, tag := range serverTags { + err = tags.Add(c.computeClient, serverID, tag).ExtractErr() + if err != nil { + return fmt.Errorf("failed to set tag '%s' for server %s: %w", tag, serverID, err) + } } - var creds Credentials - if err := json.Unmarshal(authJSON, &creds); err != nil { - return nil, fmt.Errorf("failed to unmarshal credentials: %w", err) + return nil +} + +// Gets all tags for a server +func (c *openstackClient) GetServerTags(ctx context.Context, serverID string) ([]string, error) { + serverTags, err := tags.List(c.computeClient, serverID).Extract() + if err != nil { + return nil, fmt.Errorf("failed to get tags for server %s: %w", serverID, err) + } + return serverTags, nil +} + +// Gets the compute quotas for the current project +func (c *openstackClient) GetComputeQuotas(ctx context.Context) (*ComputeQuotas, error) { + projectID := c.credentials.ProjectID + if projectID == "" { + projectID = c.credentials.TenantID // fallback to legacy field + } + if projectID == "" { + return nil, fmt.Errorf("no project ID found in credentials") } - return newClientFromStruct(&creds) + quotaSet, err := quotasets.Get(c.computeClient, projectID).Extract() + if err != nil { + return nil, fmt.Errorf("failed to get quota set: %w", err) + } + + return &ComputeQuotas{ + Instances: quotaSet.Instances, + Cores: quotaSet.Cores, + RAM: quotaSet.RAM, + }, nil +} + +// Gets the current compute usage for the project +func (c *openstackClient) GetComputeUsage(ctx context.Context) (*ComputeUsage, error) { + projectID := c.credentials.ProjectID + if projectID == "" { + projectID = c.credentials.TenantID // fallback to legacy field + } + if projectID == "" { + return nil, fmt.Errorf("no project ID found in credentials") + } + + // Create usage options + usageOpts := usage.SingleTenantOpts{} + + // Get usage for current project + allPages, err := usage.SingleTenant(c.computeClient, projectID, usageOpts).AllPages() + if err != nil { + return nil, fmt.Errorf("failed to get compute usage pages: %w", err) + } + + // Extract usage from pages + tenantUsage, err := usage.ExtractSingleTenant(allPages) + if err != nil { + return nil, fmt.Errorf("failed to extract compute usage: %w", err) + } + + // Check if we got valid usage data + if tenantUsage == nil { + return &ComputeUsage{ + InstancesUsed: 0, + CoresUsed: 0, + RAMUsed: 0, + }, nil + } + + return &ComputeUsage{ + InstancesUsed: len(tenantUsage.ServerUsages), + CoresUsed: int(tenantUsage.TotalVCPUsUsage), + RAMUsed: int(tenantUsage.TotalMemoryMBUsage), + }, nil +} + +// Gets detailed information about a specific flavor +func (c *openstackClient) GetFlavorDetails(ctx context.Context, flavorID string) (*FlavorDetails, error) { + flavor, err := flavors.Get(c.computeClient, flavorID).Extract() + if err != nil { + return nil, fmt.Errorf("failed to get flavor details: %w", err) + } + + return &FlavorDetails{ + ID: flavor.ID, + Name: flavor.Name, + VCPUs: flavor.VCPUs, + RAM: flavor.RAM, + Disk: flavor.Disk, + }, nil +} + +// Create a client wrapper object for interacting with OpenStack. +func NewClientFromSecret(secret *corev1.Secret) (Client, error) { + cloudsYaml, ok := secret.Data["clouds.yaml"] + if !ok { + return nil, fmt.Errorf("secret does not contain \"clouds.yaml\" data") + } + return newClientFromCloudsYAML(cloudsYaml) } // newClientFromCloudsYAML creates a client from clouds.yaml data @@ -319,16 +449,6 @@ func newClientFromCloudsYAML(cloudsYamlData []byte) (Client, error) { return newClientFromStruct(creds) } -func authJSONFromSecretSource(secret *corev1.Secret) func() ([]byte, error) { - return func() ([]byte, error) { - authJSON, ok := secret.Data["credentials"] // adjust key name as needed - if !ok { - return nil, fmt.Errorf("creds secret does not contain \"credentials\" data") - } - return authJSON, nil - } -} - // newClientFromStruct creates a client directly from a Credentials struct (no JSON conversion needed) func newClientFromStruct(creds *Credentials) (*openstackClient, error) { // Validate required credentials @@ -347,7 +467,6 @@ func newClientFromStruct(creds *Credentials) (*openstackClient, error) { DomainName: creds.UserDomainName, } - // Handle legacy fields for backwards compatibility if creds.TenantID != "" { authOpts.TenantID = creds.TenantID } @@ -376,17 +495,16 @@ func newClientFromStruct(creds *Credentials) (*openstackClient, error) { return nil, fmt.Errorf("failed to authenticate with OpenStack: %w", err) } - // Set region - prefer new format over legacy + // Set region region := creds.RegionName if region == "" { region = creds.Region } if region == "" { - region = "RegionOne" // default region + region = "RegionOne" } - // Set interface preference (public, internal, admin) - interfaceType := gophercloud.AvailabilityPublic // Start with public as default + interfaceType := gophercloud.AvailabilityPublic if creds.Interface != "" { switch creds.Interface { case "public": diff --git a/pkg/openstackclient/mock/client_generated.go b/pkg/openstackclient/mock/client_generated.go new file mode 100644 index 00000000000..118715765ab --- /dev/null +++ b/pkg/openstackclient/mock/client_generated.go @@ -0,0 +1,246 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./client.go + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + servers "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + images "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + openstackclient "github.com/openshift/hive/pkg/openstackclient" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// CreateServerFromOpts mocks base method. +func (m *MockClient) CreateServerFromOpts(ctx context.Context, opts *openstackclient.ServerCreateOpts) (*servers.Server, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateServerFromOpts", ctx, opts) + ret0, _ := ret[0].(*servers.Server) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateServerFromOpts indicates an expected call of CreateServerFromOpts. +func (mr *MockClientMockRecorder) CreateServerFromOpts(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateServerFromOpts", reflect.TypeOf((*MockClient)(nil).CreateServerFromOpts), ctx, opts) +} + +// CreateServerSnapshot mocks base method. +func (m *MockClient) CreateServerSnapshot(ctx context.Context, serverID, snapshotName string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateServerSnapshot", ctx, serverID, snapshotName) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateServerSnapshot indicates an expected call of CreateServerSnapshot. +func (mr *MockClientMockRecorder) CreateServerSnapshot(ctx, serverID, snapshotName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateServerSnapshot", reflect.TypeOf((*MockClient)(nil).CreateServerSnapshot), ctx, serverID, snapshotName) +} + +// DeleteServer mocks base method. +func (m *MockClient) DeleteServer(ctx context.Context, serverID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteServer", ctx, serverID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteServer indicates an expected call of DeleteServer. +func (mr *MockClientMockRecorder) DeleteServer(ctx, serverID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteServer", reflect.TypeOf((*MockClient)(nil).DeleteServer), ctx, serverID) +} + +// GetComputeQuotas mocks base method. +func (m *MockClient) GetComputeQuotas(ctx context.Context) (*openstackclient.ComputeQuotas, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetComputeQuotas", ctx) + ret0, _ := ret[0].(*openstackclient.ComputeQuotas) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetComputeQuotas indicates an expected call of GetComputeQuotas. +func (mr *MockClientMockRecorder) GetComputeQuotas(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetComputeQuotas", reflect.TypeOf((*MockClient)(nil).GetComputeQuotas), ctx) +} + +// GetComputeUsage mocks base method. +func (m *MockClient) GetComputeUsage(ctx context.Context) (*openstackclient.ComputeUsage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetComputeUsage", ctx) + ret0, _ := ret[0].(*openstackclient.ComputeUsage) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetComputeUsage indicates an expected call of GetComputeUsage. +func (mr *MockClientMockRecorder) GetComputeUsage(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetComputeUsage", reflect.TypeOf((*MockClient)(nil).GetComputeUsage), ctx) +} + +// GetFlavorDetails mocks base method. +func (m *MockClient) GetFlavorDetails(ctx context.Context, flavorID string) (*openstackclient.FlavorDetails, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFlavorDetails", ctx, flavorID) + ret0, _ := ret[0].(*openstackclient.FlavorDetails) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFlavorDetails indicates an expected call of GetFlavorDetails. +func (mr *MockClientMockRecorder) GetFlavorDetails(ctx, flavorID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFlavorDetails", reflect.TypeOf((*MockClient)(nil).GetFlavorDetails), ctx, flavorID) +} + +// GetImage mocks base method. +func (m *MockClient) GetImage(ctx context.Context, imageID string) (*images.Image, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetImage", ctx, imageID) + ret0, _ := ret[0].(*images.Image) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetImage indicates an expected call of GetImage. +func (mr *MockClientMockRecorder) GetImage(ctx, imageID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImage", reflect.TypeOf((*MockClient)(nil).GetImage), ctx, imageID) +} + +// GetNetworkByName mocks base method. +func (m *MockClient) GetNetworkByName(ctx context.Context, networkName string) (*openstackclient.Network, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNetworkByName", ctx, networkName) + ret0, _ := ret[0].(*openstackclient.Network) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNetworkByName indicates an expected call of GetNetworkByName. +func (mr *MockClientMockRecorder) GetNetworkByName(ctx, networkName interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetworkByName", reflect.TypeOf((*MockClient)(nil).GetNetworkByName), ctx, networkName) +} + +// GetServer mocks base method. +func (m *MockClient) GetServer(ctx context.Context, serverID string) (*servers.Server, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServer", ctx, serverID) + ret0, _ := ret[0].(*servers.Server) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServer indicates an expected call of GetServer. +func (mr *MockClientMockRecorder) GetServer(ctx, serverID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServer", reflect.TypeOf((*MockClient)(nil).GetServer), ctx, serverID) +} + +// GetServerSecurityGroups mocks base method. +func (m *MockClient) GetServerSecurityGroups(ctx context.Context, serverID string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServerSecurityGroups", ctx, serverID) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServerSecurityGroups indicates an expected call of GetServerSecurityGroups. +func (mr *MockClientMockRecorder) GetServerSecurityGroups(ctx, serverID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerSecurityGroups", reflect.TypeOf((*MockClient)(nil).GetServerSecurityGroups), ctx, serverID) +} + +// GetServerTags mocks base method. +func (m *MockClient) GetServerTags(ctx context.Context, serverID string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServerTags", ctx, serverID) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServerTags indicates an expected call of GetServerTags. +func (mr *MockClientMockRecorder) GetServerTags(ctx, serverID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerTags", reflect.TypeOf((*MockClient)(nil).GetServerTags), ctx, serverID) +} + +// ListPorts mocks base method. +func (m *MockClient) ListPorts(ctx context.Context) ([]openstackclient.Port, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPorts", ctx) + ret0, _ := ret[0].([]openstackclient.Port) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPorts indicates an expected call of ListPorts. +func (mr *MockClientMockRecorder) ListPorts(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPorts", reflect.TypeOf((*MockClient)(nil).ListPorts), ctx) +} + +// ListServers mocks base method. +func (m *MockClient) ListServers(ctx context.Context, opts *servers.ListOpts) ([]servers.Server, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListServers", ctx, opts) + ret0, _ := ret[0].([]servers.Server) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListServers indicates an expected call of ListServers. +func (mr *MockClientMockRecorder) ListServers(ctx, opts interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListServers", reflect.TypeOf((*MockClient)(nil).ListServers), ctx, opts) +} + +// SetServerTags mocks base method. +func (m *MockClient) SetServerTags(ctx context.Context, serverID string, serverTags []string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetServerTags", ctx, serverID, serverTags) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetServerTags indicates an expected call of SetServerTags. +func (mr *MockClientMockRecorder) SetServerTags(ctx, serverID, serverTags interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetServerTags", reflect.TypeOf((*MockClient)(nil).SetServerTags), ctx, serverID, serverTags) +} diff --git a/pkg/test/clusterdeployment/clusterdeployment.go b/pkg/test/clusterdeployment/clusterdeployment.go index 2efddb55e76..2f6c0954c0f 100644 --- a/pkg/test/clusterdeployment/clusterdeployment.go +++ b/pkg/test/clusterdeployment/clusterdeployment.go @@ -12,6 +12,7 @@ import ( hivev1azure "github.com/openshift/hive/apis/hive/v1/azure" hivev1gcp "github.com/openshift/hive/apis/hive/v1/gcp" hivev1ibmcloud "github.com/openshift/hive/apis/hive/v1/ibmcloud" + hivev1openstack "github.com/openshift/hive/apis/hive/v1/openstack" "github.com/openshift/hive/pkg/constants" "github.com/openshift/hive/pkg/test/generic" ) From 07cf30bd7a42438630e4f148b37ab88949914177 Mon Sep 17 00:00:00 2001 From: daturece Date: Mon, 28 Jul 2025 19:24:01 +0200 Subject: [PATCH 3/8] add docs, more changes --- docs/hibernating-clusters.md | 21 +++++++++- .../hibernation/openstack_actuator.go | 24 +----------- .../hibernation/openstack_actuator_test.go | 39 +++++-------------- pkg/openstackclient/client.go | 36 +---------------- pkg/openstackclient/mock/client_generated.go | 31 +-------------- 5 files changed, 34 insertions(+), 117 deletions(-) diff --git a/docs/hibernating-clusters.md b/docs/hibernating-clusters.md index 06e7237fbfe..fa83eae3538 100644 --- a/docs/hibernating-clusters.md +++ b/docs/hibernating-clusters.md @@ -15,6 +15,7 @@ - [Handling Incompatible Cloud Provider](#handling-incompatible-cloud-provider) - [Approving CSRs](#approving-csrs) - [Resuming from a Hibernating State](#resuming-from-a-hibernating-state) + - [OpenStack Hibernation](#openstack-hibernation) # Hibernating Clusters @@ -345,4 +346,22 @@ The unreachable controller tracks the reachability of a cluster and is responsib Once a cluster hibernates and stops responding, the unreachable controller sets the `Unreachable` condition's `Status` to `True`. -Once the cluster resumes, the unreachable controller set the condition `False` and the hive controllers are free to resume syncing. \ No newline at end of file +Once the cluster resumes, the unreachable controller set the condition `False` and the hive controllers are free to resume syncing. + +## OpenStack Hibernation +OpenStack hibernation deletes cluster instances while preserving the ability to restore them. Unlike other cloud providers, OpenStack lacks native hibernation, therefore an implementation based on snapshots is necessary. + +While OpenStack provides shelving functionality, replicating true hibernation behavior similar to other cloud providers is difficult and often not possible depending on the OpenStack deployment configuration. Hive's snapshot-based approach provides a universally compatible solution that works with any OpenStack setup. + +OpenStack hibernation creates snapshots of all instances, saves their complete configuration into a secret (CD namespace), and deletes the instances, completely freeing all project resources consumed by the cluster. + +Restoration recreates instances with identical attributes to their original state. + +Due to the specific nature of the snapshot based implementation, the hibernation and restoration process inherently requires more time to finish compared to other cloud providers. + +Once the ClusterPool is created and the cluster is claimed, the workflow is similar to other implementations: + +```bash +$ oc patch cd $cd -n $cd_ns --type='merge' -p $'spec:\n powerState: Hibernating' +$ oc patch cd $cd -n $cd_ns --type='merge' -p $'spec:\n powerState: Running' +``` \ No newline at end of file diff --git a/pkg/controller/hibernation/openstack_actuator.go b/pkg/controller/hibernation/openstack_actuator.go index 9d510cf7538..3367eeb1bc1 100644 --- a/pkg/controller/hibernation/openstack_actuator.go +++ b/pkg/controller/hibernation/openstack_actuator.go @@ -512,16 +512,6 @@ func (a *openstackActuator) createMissingInstances(openstackClient openstackclie } logger.Infof("created instance %s (ID: %s)", instance.Name, newServer.ID) - - // Tags are handled separately after instance creation - if len(instance.Tags) > 0 { - err = openstackClient.SetServerTags(ctx, newServer.ID, instance.Tags) - if err != nil { - logger.Warnf("failed to restore tags for %s: %v", instance.Name, err) - } else { - logger.Infof("restored %d tags for %s: %v", len(instance.Tags), instance.Name, instance.Tags) - } - } } if len(errors) > 0 { @@ -730,7 +720,6 @@ type OpenStackInstanceConfig struct { NetworkID string `json:"networkID"` OpenshiftClusterID string `json:"openshiftClusterID"` Metadata map[string]string `json:"metadata"` - Tags []string `json:"tags"` } // Return servers that match the infraID prefix @@ -803,13 +792,6 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster return fmt.Errorf("error getting server details for %s: %v", serverInfo.Name, err) } - // Get server tags - serverTags, err := openstackClient.GetServerTags(ctx, serverInfo.ID) - if err != nil { - logger.Warnf("could not get tags for %s: %v", serverInfo.Name, err) - serverTags = []string{} // Use empty tags if we can't get them - } - // Get flavor ID var flavorID string if serverDetails.Flavor != nil { @@ -851,17 +833,15 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster NetworkID: networkID, OpenshiftClusterID: openshiftClusterID, Metadata: serverDetails.Metadata, - Tags: serverTags, }) - logger.Infof("captured metadata for %s: %d properties, %d tags", - serverInfo.Name, len(serverDetails.Metadata), len(serverTags)) + logger.Infof("captured metadata for %s: %d properties", + serverInfo.Name, len(serverDetails.Metadata)) } return a.saveHibernationConfigToSecret(cd, hiveClient, instanceConfigs, logger) } - // Store hibernation information to secrets func (a *openstackActuator) saveHibernationConfigToSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, instanceConfigs []OpenStackInstanceConfig, logger log.FieldLogger) error { ctx := context.Background() diff --git a/pkg/controller/hibernation/openstack_actuator_test.go b/pkg/controller/hibernation/openstack_actuator_test.go index f9ce2cff522..bf719bf3f13 100644 --- a/pkg/controller/hibernation/openstack_actuator_test.go +++ b/pkg/controller/hibernation/openstack_actuator_test.go @@ -46,7 +46,7 @@ func TestOpenStackStopMachines(t *testing.T) { }{ { name: "stop no running instances", - instances: map[string]int{}, + instances: map[string]int{}, }, { name: "stop running instances", @@ -66,9 +66,6 @@ func TestOpenStackStopMachines(t *testing.T) { // Network operations setupOpenStackNetworkOps(c) - // Tag operations for hibernation (capture tags) - setupOpenStackTagOps(c) - // OVERRIDE the default ListServers to simulate cleanup progression gomock.InOrder( // Initial calls: return instances @@ -181,9 +178,6 @@ func TestOpenStackStartMachines(t *testing.T) { }, nil }) - // Expect tag restoration after server creation - c.EXPECT().SetServerTags(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(nil) - // Expect status checks for waiting activeServer := &servers.Server{Status: "ACTIVE"} c.EXPECT().GetServer(gomock.Any(), gomock.Any()).Return(activeServer, nil).AnyTimes() @@ -344,17 +338,6 @@ func setupOpenStackNetworkOps(openstackClient *mockopenstackclient.MockClient) { openstackClient.EXPECT().GetServerSecurityGroups(gomock.Any(), gomock.Any()).Return([]string{"default"}, nil).AnyTimes() } -// Setup tag operations for tests -func setupOpenStackTagOps(openstackClient *mockopenstackclient.MockClient) { - // Expect tag retrieval during hibernation (capture tags) - masterTags := []string{"openshiftClusterID=testinfra"} - workerTags := []string{"cluster-api-provider-openstack", "openshift-machine-api-testinfra", "openshiftClusterID=testinfra"} - - // Mock tag calls for different servers - openstackClient.EXPECT().GetServerTags(gomock.Any(), "testinfra-ACTIVE-0").Return(masterTags, nil).AnyTimes() - openstackClient.EXPECT().GetServerTags(gomock.Any(), "testinfra-ACTIVE-1").Return(workerTags, nil).AnyTimes() -} - // Setup quota mocks with sufficient resources func setupOpenStackQuotaMocks(openstackClient *mockopenstackclient.MockClient) { // Mock quotas - plenty of resources available @@ -378,7 +361,7 @@ func setupOpenStackQuotaMocks(openstackClient *mockopenstackclient.MockClient) { ID: "flavor-1", Name: "m1.small", VCPUs: 2, - RAM: 2048, // 2GB in MB + RAM: 2048, Disk: 10, } openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-1").Return(flavorDetails, nil).AnyTimes() @@ -388,7 +371,7 @@ func setupOpenStackQuotaMocks(openstackClient *mockopenstackclient.MockClient) { ID: "flavor-2", Name: "m1.medium", VCPUs: 4, - RAM: 4096, // 4GB in MB + RAM: 4096, Disk: 20, } openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-2").Return(flavorDetails2, nil).AnyTimes() @@ -399,16 +382,16 @@ func setupOpenStackQuotaMocksInsufficient(openstackClient *mockopenstackclient.M // Mock quotas - very limited resources quotas := &openstackclient.ComputeQuotas{ Instances: 2, - Cores: 4, // Only 4 vCPUs total - RAM: 4096, // Only 4GB total + Cores: 4, + RAM: 4096, } openstackClient.EXPECT().GetComputeQuotas(gomock.Any()).Return(quotas, nil).AnyTimes() // Mock usage - most resources already used usage := &openstackclient.ComputeUsage{ InstancesUsed: 1, - CoresUsed: 3, // 3 out of 4 vCPUs used - RAMUsed: 3072, // 3GB out of 4GB used + CoresUsed: 3, + RAMUsed: 3072, } openstackClient.EXPECT().GetComputeUsage(gomock.Any()).Return(usage, nil).AnyTimes() @@ -417,7 +400,7 @@ func setupOpenStackQuotaMocksInsufficient(openstackClient *mockopenstackclient.M ID: "flavor-1", Name: "m1.small", VCPUs: 2, - RAM: 2048, // 2GB in MB + RAM: 2048, Disk: 10, } openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-1").Return(flavorDetails1, nil).AnyTimes() @@ -427,7 +410,7 @@ func setupOpenStackQuotaMocksInsufficient(openstackClient *mockopenstackclient.M ID: "flavor-2", Name: "m1.medium", VCPUs: 4, - RAM: 4096, // 4GB in MB + RAM: 4096, Disk: 20, } openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-2").Return(flavorDetails2, nil).AnyTimes() @@ -443,7 +426,7 @@ func testOpenStackClusterDeployment() *hivev1.ClusterDeployment { ) } -// hibernation secret with complete metadata and tags +// hibernation secret with complete metadata func testHibernationSecretWithMetadata() *corev1.Secret { config := []OpenStackInstanceConfig{ { @@ -459,7 +442,6 @@ func testHibernationSecretWithMetadata() *corev1.Secret { "Name": "testinfra-master", "openshiftClusterID": "testinfra", }, - Tags: []string{"openshiftClusterID=testinfra"}, }, { Name: "testinfra-worker-0", @@ -474,7 +456,6 @@ func testHibernationSecretWithMetadata() *corev1.Secret { "Name": "testinfra-worker", "openshiftClusterID": "testinfra", }, - Tags: []string{"cluster-api-provider-openstack", "openshift-machine-api-testinfra", "openshiftClusterID=testinfra"}, }, } diff --git a/pkg/openstackclient/client.go b/pkg/openstackclient/client.go index 45d53d3989c..7d091a303cd 100644 --- a/pkg/openstackclient/client.go +++ b/pkg/openstackclient/client.go @@ -2,7 +2,6 @@ package openstackclient import ( "context" - "encoding/json" "fmt" "gopkg.in/yaml.v2" @@ -11,7 +10,6 @@ import ( "github.com/gophercloud/gophercloud/openstack" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups" - "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/tags" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage" "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" @@ -32,10 +30,6 @@ type Client interface { CreateServerSnapshot(ctx context.Context, serverID, snapshotName string) (string, error) CreateServerFromOpts(ctx context.Context, opts *ServerCreateOpts) (*servers.Server, error) - // Tags - SetServerTags(ctx context.Context, serverID string, serverTags []string) error - GetServerTags(ctx context.Context, serverID string) ([]string, error) - // Images - only snapshot checking GetImage(ctx context.Context, imageID string) (*images.Image, error) @@ -301,34 +295,6 @@ func (c *openstackClient) GetServerSecurityGroups(ctx context.Context, serverID return secGroupNames, nil } -// Adding tags -func (c *openstackClient) SetServerTags(ctx context.Context, serverID string, serverTags []string) error { - // Delete all existing tags first - err := tags.DeleteAll(c.computeClient, serverID).ExtractErr() - if err != nil { - return fmt.Errorf("failed to clear existing tags for server %s: %w", serverID, err) - } - - // Add new tags one by one if any exist - for _, tag := range serverTags { - err = tags.Add(c.computeClient, serverID, tag).ExtractErr() - if err != nil { - return fmt.Errorf("failed to set tag '%s' for server %s: %w", tag, serverID, err) - } - } - - return nil -} - -// Gets all tags for a server -func (c *openstackClient) GetServerTags(ctx context.Context, serverID string) ([]string, error) { - serverTags, err := tags.List(c.computeClient, serverID).Extract() - if err != nil { - return nil, fmt.Errorf("failed to get tags for server %s: %w", serverID, err) - } - return serverTags, nil -} - // Gets the compute quotas for the current project func (c *openstackClient) GetComputeQuotas(ctx context.Context) (*ComputeQuotas, error) { projectID := c.credentials.ProjectID @@ -501,7 +467,7 @@ func newClientFromStruct(creds *Credentials) (*openstackClient, error) { region = creds.Region } if region == "" { - region = "RegionOne" + region = "RegionOne" } interfaceType := gophercloud.AvailabilityPublic diff --git a/pkg/openstackclient/mock/client_generated.go b/pkg/openstackclient/mock/client_generated.go index 118715765ab..7d52fb6b758 100644 --- a/pkg/openstackclient/mock/client_generated.go +++ b/pkg/openstackclient/mock/client_generated.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: ./client.go +// Source: ./pkg/openstackclient/client.go // Package mock is a generated GoMock package. package mock @@ -186,21 +186,6 @@ func (mr *MockClientMockRecorder) GetServerSecurityGroups(ctx, serverID interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerSecurityGroups", reflect.TypeOf((*MockClient)(nil).GetServerSecurityGroups), ctx, serverID) } -// GetServerTags mocks base method. -func (m *MockClient) GetServerTags(ctx context.Context, serverID string) ([]string, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetServerTags", ctx, serverID) - ret0, _ := ret[0].([]string) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetServerTags indicates an expected call of GetServerTags. -func (mr *MockClientMockRecorder) GetServerTags(ctx, serverID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerTags", reflect.TypeOf((*MockClient)(nil).GetServerTags), ctx, serverID) -} - // ListPorts mocks base method. func (m *MockClient) ListPorts(ctx context.Context) ([]openstackclient.Port, error) { m.ctrl.T.Helper() @@ -230,17 +215,3 @@ func (mr *MockClientMockRecorder) ListServers(ctx, opts interface{}) *gomock.Cal mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListServers", reflect.TypeOf((*MockClient)(nil).ListServers), ctx, opts) } - -// SetServerTags mocks base method. -func (m *MockClient) SetServerTags(ctx context.Context, serverID string, serverTags []string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetServerTags", ctx, serverID, serverTags) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetServerTags indicates an expected call of SetServerTags. -func (mr *MockClientMockRecorder) SetServerTags(ctx, serverID, serverTags interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetServerTags", reflect.TypeOf((*MockClient)(nil).SetServerTags), ctx, serverID, serverTags) -} From cd33a4852803fa0e7c4338ec555aae242f27e680 Mon Sep 17 00:00:00 2001 From: daturece Date: Mon, 11 Aug 2025 17:46:01 +0200 Subject: [PATCH 4/8] - Address first wave of review feedback - add new two-phase snapshot cleanup - add vendor --- docs/hibernating-clusters.md | 9 +- .../hibernation/openstack_actuator.go | 279 +++--- .../hibernation/openstack_actuator_test.go | 196 +++- pkg/openstackclient/client.go | 262 ++---- pkg/openstackclient/mock/client_generated.go | 69 +- .../gophercloud/gophercloud/.gitignore | 4 + .../gophercloud/gophercloud/CHANGELOG.md | 883 ++++++++++++++++++ .../gophercloud/gophercloud/LICENSE | 192 ++++ .../gophercloud/gophercloud/README.md | 172 ++++ .../gophercloud/gophercloud/RELEASE.md | 79 ++ .../gophercloud/gophercloud/auth_options.go | 514 ++++++++++ .../gophercloud/gophercloud/auth_result.go | 52 ++ .../github.com/gophercloud/gophercloud/doc.go | 148 +++ .../gophercloud/endpoint_search.go | 76 ++ .../gophercloud/gophercloud/errors.go | 571 +++++++++++ .../gophercloud/openstack/auth_env.go | 137 +++ .../gophercloud/openstack/client.go | 503 ++++++++++ .../compute/v2/extensions/quotasets/doc.go | 36 + .../v2/extensions/quotasets/requests.go | 103 ++ .../v2/extensions/quotasets/results.go | 198 ++++ .../compute/v2/extensions/quotasets/urls.go | 25 + .../compute/v2/extensions/secgroups/doc.go | 111 +++ .../v2/extensions/secgroups/requests.go | 191 ++++ .../v2/extensions/secgroups/results.go | 218 +++++ .../compute/v2/extensions/secgroups/urls.go | 32 + .../compute/v2/extensions/usage/doc.go | 58 ++ .../compute/v2/extensions/usage/requests.go | 134 +++ .../compute/v2/extensions/usage/results.go | 191 ++++ .../compute/v2/extensions/usage/urls.go | 13 + .../openstack/compute/v2/flavors/doc.go | 150 +++ .../openstack/compute/v2/flavors/requests.go | 364 ++++++++ .../openstack/compute/v2/flavors/results.go | 271 ++++++ .../openstack/compute/v2/flavors/urls.go | 53 ++ .../openstack/compute/v2/servers/doc.go | 135 +++ .../openstack/compute/v2/servers/errors.go | 71 ++ .../openstack/compute/v2/servers/requests.go | 784 ++++++++++++++++ .../openstack/compute/v2/servers/results.go | 444 +++++++++ .../openstack/compute/v2/servers/urls.go | 51 + .../openstack/compute/v2/servers/util.go | 21 + .../gophercloud/gophercloud/openstack/doc.go | 14 + .../openstack/endpoint_location.go | 111 +++ .../gophercloud/openstack/errors.go | 47 + .../openstack/identity/v2/tenants/doc.go | 65 ++ .../openstack/identity/v2/tenants/requests.go | 120 +++ .../openstack/identity/v2/tenants/results.go | 95 ++ .../openstack/identity/v2/tenants/urls.go | 23 + .../openstack/identity/v2/tokens/doc.go | 46 + .../openstack/identity/v2/tokens/requests.go | 105 +++ .../openstack/identity/v2/tokens/results.go | 174 ++++ .../openstack/identity/v2/tokens/urls.go | 13 + .../identity/v3/extensions/ec2tokens/doc.go | 40 + .../v3/extensions/ec2tokens/requests.go | 377 ++++++++ .../identity/v3/extensions/ec2tokens/urls.go | 11 + .../identity/v3/extensions/oauth1/doc.go | 122 +++ .../identity/v3/extensions/oauth1/requests.go | 587 ++++++++++++ .../identity/v3/extensions/oauth1/results.go | 317 +++++++ .../identity/v3/extensions/oauth1/urls.go | 43 + .../openstack/identity/v3/tokens/doc.go | 107 +++ .../openstack/identity/v3/tokens/requests.go | 174 ++++ .../openstack/identity/v3/tokens/results.go | 194 ++++ .../openstack/identity/v3/tokens/urls.go | 7 + .../openstack/imageservice/v2/images/doc.go | 60 ++ .../imageservice/v2/images/requests.go | 418 +++++++++ .../imageservice/v2/images/results.go | 246 +++++ .../openstack/imageservice/v2/images/types.go | 108 +++ .../openstack/imageservice/v2/images/urls.go | 65 ++ .../openstack/networking/v2/networks/doc.go | 66 ++ .../networking/v2/networks/requests.go | 165 ++++ .../networking/v2/networks/results.go | 177 ++++ .../openstack/networking/v2/networks/urls.go | 31 + .../openstack/networking/v2/ports/doc.go | 73 ++ .../openstack/networking/v2/ports/requests.go | 209 +++++ .../openstack/networking/v2/ports/results.go | 209 +++++ .../openstack/networking/v2/ports/urls.go | 31 + .../openstack/utils/base_endpoint.go | 28 + .../openstack/utils/choose_version.go | 111 +++ .../gophercloud/pagination/http.go | 62 ++ .../gophercloud/pagination/linked.go | 92 ++ .../gophercloud/pagination/marker.go | 58 ++ .../gophercloud/pagination/pager.go | 254 +++++ .../gophercloud/gophercloud/pagination/pkg.go | 4 + .../gophercloud/pagination/single.go | 33 + .../gophercloud/gophercloud/params.go | 496 ++++++++++ .../gophercloud/provider_client.go | 652 +++++++++++++ .../gophercloud/gophercloud/results.go | 465 +++++++++ .../gophercloud/gophercloud/service_client.go | 162 ++++ .../gophercloud/gophercloud/util.go | 130 +++ vendor/modules.txt | 19 + 88 files changed, 14654 insertions(+), 362 deletions(-) create mode 100644 vendor/github.com/gophercloud/gophercloud/.gitignore create mode 100644 vendor/github.com/gophercloud/gophercloud/CHANGELOG.md create mode 100644 vendor/github.com/gophercloud/gophercloud/LICENSE create mode 100644 vendor/github.com/gophercloud/gophercloud/README.md create mode 100644 vendor/github.com/gophercloud/gophercloud/RELEASE.md create mode 100644 vendor/github.com/gophercloud/gophercloud/auth_options.go create mode 100644 vendor/github.com/gophercloud/gophercloud/auth_result.go create mode 100644 vendor/github.com/gophercloud/gophercloud/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/endpoint_search.go create mode 100644 vendor/github.com/gophercloud/gophercloud/errors.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/client.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/errors.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/utils/base_endpoint.go create mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go create mode 100644 vendor/github.com/gophercloud/gophercloud/pagination/http.go create mode 100644 vendor/github.com/gophercloud/gophercloud/pagination/linked.go create mode 100644 vendor/github.com/gophercloud/gophercloud/pagination/marker.go create mode 100644 vendor/github.com/gophercloud/gophercloud/pagination/pager.go create mode 100644 vendor/github.com/gophercloud/gophercloud/pagination/pkg.go create mode 100644 vendor/github.com/gophercloud/gophercloud/pagination/single.go create mode 100644 vendor/github.com/gophercloud/gophercloud/params.go create mode 100644 vendor/github.com/gophercloud/gophercloud/provider_client.go create mode 100644 vendor/github.com/gophercloud/gophercloud/results.go create mode 100644 vendor/github.com/gophercloud/gophercloud/service_client.go create mode 100644 vendor/github.com/gophercloud/gophercloud/util.go diff --git a/docs/hibernating-clusters.md b/docs/hibernating-clusters.md index fa83eae3538..40b7434d11b 100644 --- a/docs/hibernating-clusters.md +++ b/docs/hibernating-clusters.md @@ -357,11 +357,4 @@ OpenStack hibernation creates snapshots of all instances, saves their complete c Restoration recreates instances with identical attributes to their original state. -Due to the specific nature of the snapshot based implementation, the hibernation and restoration process inherently requires more time to finish compared to other cloud providers. - -Once the ClusterPool is created and the cluster is claimed, the workflow is similar to other implementations: - -```bash -$ oc patch cd $cd -n $cd_ns --type='merge' -p $'spec:\n powerState: Hibernating' -$ oc patch cd $cd -n $cd_ns --type='merge' -p $'spec:\n powerState: Running' -``` \ No newline at end of file +Due to the specific nature of the snapshot based implementation, the hibernation and restoration process inherently requires more time to finish compared to other cloud providers. \ No newline at end of file diff --git a/pkg/controller/hibernation/openstack_actuator.go b/pkg/controller/hibernation/openstack_actuator.go index 3367eeb1bc1..50fbeeb7d78 100644 --- a/pkg/controller/hibernation/openstack_actuator.go +++ b/pkg/controller/hibernation/openstack_actuator.go @@ -7,6 +7,9 @@ import ( "strings" "time" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" hivev1 "github.com/openshift/hive/apis/hive/v1" "github.com/openshift/hive/pkg/openstackclient" @@ -60,32 +63,6 @@ func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClien logger = logger.WithField("cloud", "openstack") logger.Info("stopping machines and creating snapshots") - _, err := a.loadHibernationConfigFromSecret(cd, hiveClient, logger) - if err == nil { - logger.Info("Hibernation config already exists - checking if hibernation completed") - - openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) - if err != nil { - return fmt.Errorf("failed to create OpenStack client: %v", err) - } - - infraID := cd.Spec.ClusterMetadata.InfraID - matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) - if err != nil { - return fmt.Errorf("error finding instances: %v", err) - } - - if len(matchingServers) == 0 { - logger.Info("Hibernation already completed - config exists and no instances found") - return nil - } - - logger.Info("Hibernation config exists but instances still found - proceeding with cleanup") - } - - logger = logger.WithField("cloud", "openstack") - logger.Info("stopping machines and creating snapshots") - openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) if err != nil { return fmt.Errorf("failed to create OpenStack client: %v", err) @@ -93,6 +70,7 @@ func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClien infraID := cd.Spec.ClusterMetadata.InfraID + // 1. matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) if err != nil { return fmt.Errorf("error finding instances: %v", err) @@ -105,41 +83,44 @@ func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClien logger.Infof("found %d instances to hibernate", len(matchingServers)) - // Validate instance states before snapshotting - err = a.validateInstanceStates(openstackClient, matchingServers, logger) - if err != nil { + if err := a.validateInstanceStates(openstackClient, matchingServers, logger); err != nil { return err } - // Create snapshots for each instance + // 3. snapshotIDs, err := a.createSnapshots(openstackClient, matchingServers, logger) if err != nil { return err } - // Wait for all snapshots to complete - err = a.waitForSnapshots(openstackClient, snapshotIDs, matchingServers, logger) - if err != nil { + // 4. + if err := a.waitForSnapshots(openstackClient, snapshotIDs, matchingServers, logger); err != nil { return err } - // Save configuration to Secret - err = a.saveInstanceConfigurationToSecret(cd, hiveClient, openstackClient, matchingServers, snapshotIDs, logger) - if err != nil { + // 5. + if err := a.saveInstanceConfigurationToSecret(cd, hiveClient, openstackClient, matchingServers, snapshotIDs, logger); err != nil { return fmt.Errorf("error saving configuration: %v", err) } - // Delete the instances - err = a.deleteInstances(openstackClient, matchingServers, logger) - if err != nil { + // 6. + if err := a.deleteInstances(openstackClient, matchingServers, logger); err != nil { return err } - err = a.waitForInstanceCleanup(openstackClient, infraID, logger) - if err != nil { + // 7. + if err := a.waitForInstanceCleanup(openstackClient, infraID, logger); err != nil { return err } + // 8. + logger.Info("cleaning up old hibernation snapshots after instance deletion") + err = a.cleanupOldSnapshotsAfterDeletion(openstackClient, infraID, snapshotIDs, logger) + if err != nil { + // Don't fail hibernation due to cleanup issues + logger.Warnf("some old snapshots couldn't be cleaned up: %v", err) + } + logger.Info("hibernation completed successfully") return nil } @@ -199,9 +180,10 @@ func (a *openstackActuator) createSnapshots(openstackClient openstackclient.Clie for i, server := range servers { logger.Infof("creating snapshot %d/%d for instance %s", i+1, len(servers), server.Name) - snapshotID, err := openstackClient.CreateServerSnapshot(ctx, server.ID, server.Name) + snapshotName := fmt.Sprintf("%s-hibernation-%s", server.Name, time.Now().UTC().Format("20060102-150405")) + + snapshotID, err := openstackClient.CreateServerSnapshot(ctx, server.ID, snapshotName) if err != nil { - // Enhanced error handling for conflicts if strings.Contains(err.Error(), "task_state deleting") || strings.Contains(err.Error(), "409") { return nil, fmt.Errorf("hibernation conflict: instance %s is being modified by another process", server.Name) } @@ -246,16 +228,14 @@ func (a *openstackActuator) deleteInstances(openstackClient openstackclient.Clie // Wait for snapshot to reach ACTIVE state func (a *openstackActuator) waitForSnapshotCompletion(openstackClient openstackclient.Client, snapshotID, serverName string, logger log.FieldLogger) error { ctx := context.Background() - maxWaitTime := 30 * time.Minute - checkInterval := 10 * time.Second - timeout := time.After(maxWaitTime) - ticker := time.NewTicker(checkInterval) + timeout := time.After(30 * time.Minute) + ticker := time.NewTicker(45 * time.Second) defer ticker.Stop() for { select { case <-timeout: - return fmt.Errorf("timeout waiting for snapshot %s to complete after %v", snapshotID, maxWaitTime) + return fmt.Errorf("timeout waiting for snapshot %s to complete after %v", snapshotID, timeout) case <-ticker.C: image, err := openstackClient.GetImage(ctx, snapshotID) if err != nil { @@ -322,10 +302,10 @@ func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClie logger.Warnf("could not check existing instances: %v", err) } - // Check if we already have all the instances we need + // Check if we already have all the instances we need and clear hibernation config if len(existingServers) >= len(instances) { logger.Info("sufficient instances already exist - clearing hibernation config") - _ = a.deleteHibernationConfigSecret(cd, hiveClient, logger) // Best effort cleanup + _ = a.deleteHibernationConfigSecret(cd, hiveClient, logger) return nil } @@ -360,10 +340,18 @@ func (a *openstackActuator) validateRestoreResources(openstackClient openstackcl logger.Infof("restoration requires: %d instances, %d vCPUs, %d MB RAM", requirements.Instances, requirements.VCPUs, requirements.RAM) + // Handle usage data (can be nil) + var instancesUsed, coresUsed, ramUsed int + if usage != nil { + instancesUsed = len(usage.ServerUsages) + coresUsed = int(usage.TotalVCPUsUsage) + ramUsed = int(usage.TotalMemoryMBUsage) + } + // Check available resources - availableInstances := quotas.Instances - usage.InstancesUsed - availableVCPUs := quotas.Cores - usage.CoresUsed - availableRAM := quotas.RAM - usage.RAMUsed + availableInstances := quotas.Instances - instancesUsed + availableVCPUs := quotas.Cores - coresUsed + availableRAM := quotas.RAM - ramUsed logger.Infof("available resources: %d instances, %d vCPUs, %d MB RAM", availableInstances, availableVCPUs, availableRAM) @@ -401,13 +389,13 @@ func (a *openstackActuator) calculateResourceRequirements(openstackClient openst requirements := &openstackclient.ResourceRequirements{} // Track unique flavors to avoid duplicate API calls - flavorCache := make(map[string]*openstackclient.FlavorDetails) + flavorCache := make(map[string]*flavors.Flavor) // Changed type for _, instance := range instances { requirements.Instances++ // Get flavor details - var flavor *openstackclient.FlavorDetails + var flavor *flavors.Flavor // Changed type if cached, exists := flavorCache[instance.Flavor]; exists { flavor = cached } else { @@ -470,6 +458,13 @@ func (a *openstackActuator) restoreFromHibernationConfig(cd *hivev1.ClusterDeplo return fmt.Errorf("not all instances are active yet: %v", err) } + logger.Info("cleaning up hibernation snapshots after successful restoration") + err = a.cleanupRestorationSnapshots(openstackClient, instances, logger) + if err != nil { + // Log but don't fail - restoration succeeded, cleanup is best-effort + logger.Warnf("failed to cleanup some snapshots: %v", err) + } + // Only clear hibernation config when we have the instances running logger.Info("all instances confirmed active - clearing hibernation configuration") return a.deleteHibernationConfigSecret(cd, hiveClient, logger) @@ -493,12 +488,16 @@ func (a *openstackActuator) createMissingInstances(openstackClient openstackclie } // Build server creation options with complete metadata - createOpts := &openstackclient.ServerCreateOpts{ - Name: instance.Name, - ImageRef: instance.SnapshotID, - FlavorRef: instance.Flavor, - NetworkID: instance.NetworkID, - PortID: instance.PortID, + createOpts := &servers.CreateOpts{ + Name: instance.Name, + ImageRef: instance.SnapshotID, + FlavorRef: instance.Flavor, + Networks: []servers.Network{ + { + UUID: instance.NetworkID, + Port: instance.PortID, + }, + }, SecurityGroups: instance.SecurityGroups, Metadata: instance.Metadata, } @@ -523,10 +522,8 @@ func (a *openstackActuator) createMissingInstances(openstackClient openstackclie // Wait for ALL instances to be active func (a *openstackActuator) waitForAllInstancesToBeActive(openstackClient openstackclient.Client, infraID string, expectedCount int, logger log.FieldLogger) error { - maxWaitTime := 30 * time.Minute - checkInterval := 15 * time.Second - timeout := time.After(maxWaitTime) - ticker := time.NewTicker(checkInterval) + timeout := time.After(30 * time.Minute) + ticker := time.NewTicker(45 * time.Second) defer ticker.Stop() logger.Infof("waiting for all %d instances to become active", expectedCount) @@ -534,7 +531,7 @@ func (a *openstackActuator) waitForAllInstancesToBeActive(openstackClient openst for { select { case <-timeout: - return fmt.Errorf("timeout waiting for all instances to become active after %v", maxWaitTime) + return fmt.Errorf("timeout waiting for all instances to become active after %v", timeout) case <-ticker.C: // Get current instances currentServers, err := a.findInstancesByPrefix(openstackClient, infraID) @@ -576,42 +573,6 @@ func (a *openstackActuator) waitForAllInstancesToBeActive(openstackClient openst } } -// wait for a server to reach ACTIVE state -func (a *openstackActuator) waitForServerActive(openstackClient openstackclient.Client, serverID, serverName string, logger log.FieldLogger) error { - ctx := context.Background() - maxWaitTime := 30 * time.Minute - checkInterval := 15 * time.Second - timeout := time.After(maxWaitTime) - ticker := time.NewTicker(checkInterval) - defer ticker.Stop() - - for { - select { - case <-timeout: - return fmt.Errorf("timeout waiting for server %s to become ACTIVE after %v", serverID, maxWaitTime) - case <-ticker.C: - server, err := openstackClient.GetServer(ctx, serverID) - if err != nil { - logger.Warnf("error checking server %s status: %v", serverID, err) - continue - } - - logger.Infof("server %s (%s) status: %s", serverID, serverName, server.Status) - - switch server.Status { - case "ACTIVE": - return nil - case "BUILD", "REBUILD": - continue - case "ERROR", "DELETED": - return fmt.Errorf("server %s failed with status: %s", serverID, server.Status) - default: - continue - } - } - } -} - // MachinesRunning checks if machines are running func (a *openstackActuator) MachinesRunning(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) (bool, []string, error) { logger = logger.WithField("cloud", "openstack") @@ -818,7 +779,7 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster } // Get security groups - secGroups, err := openstackClient.GetServerSecurityGroups(ctx, serverInfo.ID) + secGroups, err := openstackClient.GetServerSecurityGroupNames(ctx, serverInfo.ID) if err != nil { return fmt.Errorf("error getting security groups for %s: %v", serverInfo.Name, err) } @@ -901,10 +862,8 @@ func (a *openstackActuator) saveHibernationConfigToSecret(cd *hivev1.ClusterDepl // Waits for OpenStack to fully remove an instance func (a *openstackActuator) waitForInstanceCleanup(openstackClient openstackclient.Client, infraID string, logger log.FieldLogger) error { - maxWaitTime := 5 * time.Minute // Maximum time to wait for cleanup - checkInterval := 5 * time.Second // Check every 5 seconds - timeout := time.After(maxWaitTime) - ticker := time.NewTicker(checkInterval) + timeout := time.After(5 * time.Minute) + ticker := time.NewTicker(10 * time.Second) defer ticker.Stop() logger.Info("waiting for OpenStack to clean up deleted instances...") @@ -912,7 +871,7 @@ func (a *openstackActuator) waitForInstanceCleanup(openstackClient openstackclie for { select { case <-timeout: - return fmt.Errorf("timeout waiting for instance cleanup after %v", maxWaitTime) + return fmt.Errorf("timeout waiting for instance cleanup after %v", timeout) case <-ticker.C: matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) if err != nil { @@ -1000,3 +959,107 @@ func (a *openstackActuator) getNetworkIDForCluster(openstackClient openstackclie return network.ID, nil } + +func (a *openstackActuator) cleanupRestorationSnapshots(openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { + ctx := context.Background() + + logger.Info("attempting best-effort cleanup of restoration snapshots") + + successCount := 0 + for _, instance := range instances { + if instance.SnapshotID != "" { + logger.Infof("attempting to delete restoration snapshot %s for instance %s", instance.SnapshotID, instance.Name) + err := openstackClient.DeleteImage(ctx, instance.SnapshotID) + if err != nil { + if strings.Contains(err.Error(), "in use") || strings.Contains(err.Error(), "409") { + logger.Infof("snapshot %s still in use (will be cleaned up next hibernation cycle)", instance.SnapshotID) + } else { + logger.Warnf("failed to delete snapshot %s: %v", instance.SnapshotID, err) + } + } else { + logger.Infof("successfully deleted restoration snapshot %s", instance.SnapshotID) + successCount++ + } + } + } + + logger.Infof("restoration snapshot cleanup: %d/%d snapshots deleted (remaining will be cleaned up next hibernation)", successCount, len(instances)) + return nil // Always succeed +} + +func (a *openstackActuator) cleanupOldSnapshotsAfterDeletion(openstackClient openstackclient.Client, infraID string, currentSnapshotIDs []string, logger log.FieldLogger) error { + ctx := context.Background() + + // Get all hibernation snapshots for this cluster + allSnapshots, err := a.findAllHibernationSnapshots(openstackClient, infraID, logger) + if err != nil { + return err + } + + if len(allSnapshots) == 0 { + logger.Info("no hibernation snapshots found") + return nil + } + + // Create map of current snapshot IDs for exclusion + currentIDs := make(map[string]bool) + for _, id := range currentSnapshotIDs { + currentIDs[id] = true + } + + // Filter out current snapshots - only delete OLD ones + var oldSnapshots []images.Image + for _, snapshot := range allSnapshots { + if !currentIDs[snapshot.ID] { + oldSnapshots = append(oldSnapshots, snapshot) + logger.Infof("found OLD snapshot to delete: %s (ID: %s)", snapshot.Name, snapshot.ID) + } else { + logger.Infof("keeping NEW snapshot: %s (ID: %s)", snapshot.Name, snapshot.ID) + } + } + + if len(oldSnapshots) == 0 { + logger.Info("no old snapshots to clean up") + return nil + } + + logger.Infof("deleting %d old hibernation snapshots", len(oldSnapshots)) + + successCount := 0 + for _, snapshot := range oldSnapshots { + logger.Infof("deleting old hibernation snapshot: %s", snapshot.Name) + err := openstackClient.DeleteImage(ctx, snapshot.ID) + if err != nil { + logger.Warnf("failed to delete old snapshot %s: %v", snapshot.Name, err) + } else { + logger.Infof("successfully deleted old snapshot: %s", snapshot.Name) + successCount++ + } + } + + logger.Infof("old snapshot cleanup completed: %d/%d snapshots deleted", successCount, len(oldSnapshots)) + return nil +} + +// Helper function to find all hibernation snapshots +func (a *openstackActuator) findAllHibernationSnapshots(openstackClient openstackclient.Client, infraID string, logger log.FieldLogger) ([]images.Image, error) { + ctx := context.Background() + + // List all images + existingSnapshots, err := openstackClient.ListImages(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to list snapshots: %v", err) + } + + var hibernationSnapshots []images.Image + + // Filter snapshots + for _, snapshot := range existingSnapshots { + if strings.Contains(snapshot.Name, infraID) && strings.Contains(snapshot.Name, "hibernation") { + hibernationSnapshots = append(hibernationSnapshots, snapshot) + } + } + + logger.Infof("found %d total hibernation snapshots for cluster %s", len(hibernationSnapshots), infraID) + return hibernationSnapshots, nil +} diff --git a/pkg/controller/hibernation/openstack_actuator_test.go b/pkg/controller/hibernation/openstack_actuator_test.go index bf719bf3f13..69cf93358e3 100644 --- a/pkg/controller/hibernation/openstack_actuator_test.go +++ b/pkg/controller/hibernation/openstack_actuator_test.go @@ -8,8 +8,13 @@ import ( "testing" "github.com/golang/mock/gomock" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" + "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage" + "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" + "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -47,15 +52,20 @@ func TestOpenStackStopMachines(t *testing.T) { { name: "stop no running instances", instances: map[string]int{}, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + }, }, { name: "stop running instances", instances: map[string]int{"ACTIVE": 2}, setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // Expect cleanup of existing snapshots + c.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return([]images.Image{}, nil).Times(1) + // Expect snapshot creation c.EXPECT().CreateServerSnapshot(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return("snapshot-1", nil) - // Expect image status checks + // Expect image status checks for waiting activeImg := &images.Image{} activeImg.Status = "active" c.EXPECT().GetImage(gomock.Any(), gomock.Any()).Return(activeImg, nil).AnyTimes() @@ -63,25 +73,91 @@ func TestOpenStackStopMachines(t *testing.T) { // Expect instance deletion c.EXPECT().DeleteServer(gomock.Any(), gomock.Any()).Times(2).Return(nil) - // Network operations + // Network operations for saving configuration setupOpenStackNetworkOps(c) - // OVERRIDE the default ListServers to simulate cleanup progression + // Server details for metadata extraction + c.EXPECT().GetServer(gomock.Any(), gomock.Any()).Return(&servers.Server{ + ID: "testinfra-ACTIVE-0", + Name: "testinfra-ACTIVE-0", + Status: "ACTIVE", + Flavor: map[string]interface{}{"id": "flavor-1"}, + Metadata: map[string]string{"openshiftClusterID": "testinfra"}, + }, nil).AnyTimes() + + // ListServers expectations for cleanup verification gomock.InOrder( - // Initial calls: return instances + // First calls: find instances to hibernate c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ - {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, - {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, - }, nil).Times(2), + {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0", Status: "ACTIVE"}, + {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1", Status: "ACTIVE"}, + }, nil).Times(1), // Called by findInstancesByPrefix - // First cleanup poll: still there + // After deletion: verify cleanup (waitForInstanceCleanup) c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ - {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0"}, - {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1"}, - }, nil).Times(1), + {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0", Status: "DELETING"}, + {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1", Status: "DELETING"}, + }, nil).Times(1), // First cleanup check - still deleting + + // Final check: all gone + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), + ) + }, + }, + { + name: "stop running instances with existing snapshots to cleanup", + instances: map[string]int{"ACTIVE": 2}, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // ===== FIX: Return snapshots that match our filter criteria ===== + oldSnapshots := []images.Image{ + { + ID: "old-snap-1", + Name: "testinfra-master-0-hibernation-20240101", + Metadata: map[string]string{ + "cluster_infra_id": "testinfra", + "hive_hibernation": "true", + }, + }, + { + ID: "old-snap-2", + Name: "testinfra-worker-0-hibernation-20240101", + Metadata: map[string]string{ + "cluster_infra_id": "testinfra", + "hive_hibernation": "true", + }, + }, + } + c.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return(oldSnapshots, nil).Times(1) + + c.EXPECT().DeleteImage(gomock.Any(), "old-snap-1").Return(nil).Times(1) + c.EXPECT().DeleteImage(gomock.Any(), "old-snap-2").Return(nil).Times(1) + + // Continue with normal hibernation flow + c.EXPECT().CreateServerSnapshot(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return("snapshot-1", nil) + + // Add these missing expectations: + activeImg := &images.Image{} + activeImg.Status = "active" + c.EXPECT().GetImage(gomock.Any(), gomock.Any()).Return(activeImg, nil).AnyTimes() - // Subsequent cleanup polls: instances gone - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).AnyTimes(), + c.EXPECT().DeleteServer(gomock.Any(), gomock.Any()).Times(2).Return(nil) + + setupOpenStackNetworkOps(c) + + c.EXPECT().GetServer(gomock.Any(), gomock.Any()).Return(&servers.Server{ + ID: "testinfra-ACTIVE-0", + Name: "testinfra-ACTIVE-0", + Status: "ACTIVE", + Flavor: map[string]interface{}{"id": "flavor-1"}, + Metadata: map[string]string{"openshiftClusterID": "testinfra"}, + }, nil).AnyTimes() + + gomock.InOrder( + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0", Status: "ACTIVE"}, + {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1", Status: "ACTIVE"}, + }, nil).Times(1), + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), ) }, }, @@ -152,8 +228,6 @@ func TestOpenStackStartMachines(t *testing.T) { activeImg.Status = "active" c.EXPECT().GetImage(gomock.Any(), gomock.Any()).Return(activeImg, nil).AnyTimes() - // STEP 1: First few calls should return NO instances (so creation happens) - // STEP 2: After creation, return the created instances gomock.InOrder( // First call: no existing instances (for existing instance check) c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), @@ -170,7 +244,7 @@ func TestOpenStackStartMachines(t *testing.T) { // Expect server creation c.EXPECT().CreateServerFromOpts(gomock.Any(), gomock.Any()).Times(2).DoAndReturn( - func(ctx context.Context, opts *openstackclient.ServerCreateOpts) (*servers.Server, error) { + func(ctx context.Context, opts *servers.CreateOpts) (*servers.Server, error) { return &servers.Server{ ID: fmt.Sprintf("new-%s", opts.Name), Name: opts.Name, @@ -181,6 +255,10 @@ func TestOpenStackStartMachines(t *testing.T) { // Expect status checks for waiting activeServer := &servers.Server{Status: "ACTIVE"} c.EXPECT().GetServer(gomock.Any(), gomock.Any()).Return(activeServer, nil).AnyTimes() + + // The hibernation config has 2 instances with snapshots + c.EXPECT().DeleteImage(gomock.Any(), "snapshot-1").Return(nil).Times(1) + c.EXPECT().DeleteImage(gomock.Any(), "snapshot-2").Return(nil).Times(1) }, }, { @@ -195,6 +273,45 @@ func TestOpenStackStartMachines(t *testing.T) { c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) }, }, + { + name: "start from hibernation - cleanup continues even if snapshot deletion fails", + instances: map[string]int{}, // No existing instances + withSecret: true, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // Setup for successful restoration + setupOpenStackQuotaMocks(c) + + activeImg := &images.Image{} + activeImg.Status = "active" + c.EXPECT().GetImage(gomock.Any(), gomock.Any()).Return(activeImg, nil).AnyTimes() + + gomock.InOrder( + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "new-testinfra-master-0", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "new-testinfra-worker-0", Name: "testinfra-worker-0", Status: "ACTIVE"}, + }, nil).AnyTimes(), + ) + + c.EXPECT().CreateServerFromOpts(gomock.Any(), gomock.Any()).Times(2).DoAndReturn( + func(ctx context.Context, opts *servers.CreateOpts) (*servers.Server, error) { + return &servers.Server{ + ID: fmt.Sprintf("new-%s", opts.Name), + Name: opts.Name, + Status: "BUILD", + }, nil + }) + + activeServer := &servers.Server{Status: "ACTIVE"} + c.EXPECT().GetServer(gomock.Any(), gomock.Any()).Return(activeServer, nil).AnyTimes() + + // First snapshot deletes successfully + c.EXPECT().DeleteImage(gomock.Any(), "snapshot-1").Return(nil).Times(1) + // Second snapshot fails to delete (should not cause overall failure) + c.EXPECT().DeleteImage(gomock.Any(), "snapshot-2").Return(errors.New("snapshot in use")).Times(1) + }, + }, } for _, test := range tests { @@ -228,6 +345,7 @@ func TestOpenStackStartMachines(t *testing.T) { assert.NotNil(t, err) assert.Contains(t, err.Error(), "resource validation failed") } else { + // Even if snapshot deletion fails, StartMachines should succeed assert.Nil(t, err) } }) @@ -321,27 +439,36 @@ func setupOpenStackClientInstances(openstackClient *mockopenstackclient.MockClie func setupOpenStackNetworkOps(openstackClient *mockopenstackclient.MockClient) { // Network operations - network := &openstackclient.Network{ - ID: "network-1", - Name: "testinfra-openshift", + network := &networks.Network{ + ID: "network-1", + Name: "testinfra-openshift", + Status: "ACTIVE", } openstackClient.EXPECT().GetNetworkByName(gomock.Any(), "testinfra-openshift").Return(network, nil).AnyTimes() // Port listing - ports := []openstackclient.Port{ + ports := []ports.Port{ {ID: "port-1", Name: "testinfra-ACTIVE-0"}, {ID: "port-2", Name: "testinfra-ACTIVE-1"}, } openstackClient.EXPECT().ListPorts(gomock.Any()).Return(ports, nil).AnyTimes() // Security groups - openstackClient.EXPECT().GetServerSecurityGroups(gomock.Any(), gomock.Any()).Return([]string{"default"}, nil).AnyTimes() + openstackClient.EXPECT().GetServerSecurityGroupNames(gomock.Any(), gomock.Any()).Return([]string{"default"}, nil).AnyTimes() +} + +func setupOpenStackClientWithSnapshotCleanup(openstackClient *mockopenstackclient.MockClient) { + // For ListImages (cleanup check) + openstackClient.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return([]images.Image{}, nil).AnyTimes() + + // For DeleteImage (cleanup action) + openstackClient.EXPECT().DeleteImage(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() } // Setup quota mocks with sufficient resources func setupOpenStackQuotaMocks(openstackClient *mockopenstackclient.MockClient) { // Mock quotas - plenty of resources available - quotas := &openstackclient.ComputeQuotas{ + quotas := "asets.QuotaSet{ Instances: 20, Cores: 40, RAM: 81920, @@ -349,15 +476,11 @@ func setupOpenStackQuotaMocks(openstackClient *mockopenstackclient.MockClient) { openstackClient.EXPECT().GetComputeQuotas(gomock.Any()).Return(quotas, nil).AnyTimes() // Mock usage - minimal usage - usage := &openstackclient.ComputeUsage{ - InstancesUsed: 0, - CoresUsed: 0, - RAMUsed: 0, - } + var usage *usage.TenantUsage = nil openstackClient.EXPECT().GetComputeUsage(gomock.Any()).Return(usage, nil).AnyTimes() // Mock flavor details for our test flavors - flavorDetails := &openstackclient.FlavorDetails{ + flavorDetails := &flavors.Flavor{ ID: "flavor-1", Name: "m1.small", VCPUs: 2, @@ -367,7 +490,7 @@ func setupOpenStackQuotaMocks(openstackClient *mockopenstackclient.MockClient) { openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-1").Return(flavorDetails, nil).AnyTimes() // Also mock flavor-2 for worker - flavorDetails2 := &openstackclient.FlavorDetails{ + flavorDetails2 := &flavors.Flavor{ ID: "flavor-2", Name: "m1.medium", VCPUs: 4, @@ -380,7 +503,7 @@ func setupOpenStackQuotaMocks(openstackClient *mockopenstackclient.MockClient) { // Setup quota mocks with insufficient resources func setupOpenStackQuotaMocksInsufficient(openstackClient *mockopenstackclient.MockClient) { // Mock quotas - very limited resources - quotas := &openstackclient.ComputeQuotas{ + quotas := "asets.QuotaSet{ Instances: 2, Cores: 4, RAM: 4096, @@ -388,15 +511,15 @@ func setupOpenStackQuotaMocksInsufficient(openstackClient *mockopenstackclient.M openstackClient.EXPECT().GetComputeQuotas(gomock.Any()).Return(quotas, nil).AnyTimes() // Mock usage - most resources already used - usage := &openstackclient.ComputeUsage{ - InstancesUsed: 1, - CoresUsed: 3, - RAMUsed: 3072, + usage := &usage.TenantUsage{ + ServerUsages: make([]usage.ServerUsage, 1), // 1 instance used + TotalVCPUsUsage: 3, + TotalMemoryMBUsage: 3072, } openstackClient.EXPECT().GetComputeUsage(gomock.Any()).Return(usage, nil).AnyTimes() // Mock flavor details - will show insufficient resources - flavorDetails1 := &openstackclient.FlavorDetails{ + flavorDetails1 := &flavors.Flavor{ ID: "flavor-1", Name: "m1.small", VCPUs: 2, @@ -405,8 +528,7 @@ func setupOpenStackQuotaMocksInsufficient(openstackClient *mockopenstackclient.M } openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-1").Return(flavorDetails1, nil).AnyTimes() - // Also mock flavor-2 for worker - flavorDetails2 := &openstackclient.FlavorDetails{ + flavorDetails2 := &flavors.Flavor{ ID: "flavor-2", Name: "m1.medium", VCPUs: 4, diff --git a/pkg/openstackclient/client.go b/pkg/openstackclient/client.go index 7d091a303cd..41b3da313a4 100644 --- a/pkg/openstackclient/client.go +++ b/pkg/openstackclient/client.go @@ -16,6 +16,8 @@ import ( "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/openshift/hive/pkg/constants" + corev1 "k8s.io/api/core/v1" ) @@ -28,53 +30,24 @@ type Client interface { GetServer(ctx context.Context, serverID string) (*servers.Server, error) DeleteServer(ctx context.Context, serverID string) error CreateServerSnapshot(ctx context.Context, serverID, snapshotName string) (string, error) - CreateServerFromOpts(ctx context.Context, opts *ServerCreateOpts) (*servers.Server, error) + CreateServerFromOpts(ctx context.Context, opts *servers.CreateOpts) (*servers.Server, error) - // Images - only snapshot checking + // Images GetImage(ctx context.Context, imageID string) (*images.Image, error) + ListImages(ctx context.Context, opts *images.ListOpts) ([]images.Image, error) + DeleteImage(ctx context.Context, imageID string) error // Networks - GetNetworkByName(ctx context.Context, networkName string) (*Network, error) - ListPorts(ctx context.Context) ([]Port, error) + GetNetworkByName(ctx context.Context, networkName string) (*networks.Network, error) + ListPorts(ctx context.Context) ([]ports.Port, error) - // Security Groups - GetServerSecurityGroups(ctx context.Context, serverID string) ([]string, error) + // Security Group Names + GetServerSecurityGroupNames(ctx context.Context, serverID string) ([]string, error) // Project resources - GetComputeQuotas(ctx context.Context) (*ComputeQuotas, error) - GetComputeUsage(ctx context.Context) (*ComputeUsage, error) - GetFlavorDetails(ctx context.Context, flavorID string) (*FlavorDetails, error) -} - -type Network struct { - ID string `json:"id"` - Name string `json:"name"` - Status string `json:"status"` -} - -type Port struct { - ID string `json:"id"` - Name string `json:"name"` -} - -type ComputeQuotas struct { - Instances int `json:"instances"` - Cores int `json:"cores"` - RAM int `json:"ram"` -} - -type ComputeUsage struct { - InstancesUsed int `json:"totalInstancesUsed"` - CoresUsed int `json:"totalCoresUsed"` - RAMUsed int `json:"totalRAMUsed"` -} - -type FlavorDetails struct { - ID string `json:"id"` - Name string `json:"name"` - VCPUs int `json:"vcpus"` - RAM int `json:"ram"` - Disk int `json:"disk"` + GetComputeQuotas(ctx context.Context) (*quotasets.QuotaSet, error) + GetComputeUsage(ctx context.Context) (*usage.TenantUsage, error) + GetFlavorDetails(ctx context.Context, flavorID string) (*flavors.Flavor, error) } type ResourceRequirements struct { @@ -83,18 +56,6 @@ type ResourceRequirements struct { RAM int } -// Options for creating a server -type ServerCreateOpts struct { - Name string `json:"name"` - ImageRef string `json:"imageRef"` - FlavorRef string `json:"flavorRef"` - NetworkID string `json:"networkID"` - PortID string `json:"portID"` - SecurityGroups []string `json:"securityGroups"` - Metadata map[string]string `json:"metadata"` -} - -// OpenStack credentials structure type Credentials struct { AuthURL string `json:"auth_url"` Username string `json:"username,omitempty"` @@ -161,8 +122,7 @@ func (c *openstackClient) ListServers(ctx context.Context, opts *servers.ListOpt } func (c *openstackClient) GetServer(ctx context.Context, serverID string) (*servers.Server, error) { - server, err := servers.Get(c.computeClient, serverID).Extract() - return server, err + return servers.Get(c.computeClient, serverID).Extract() } func (c *openstackClient) DeleteServer(ctx context.Context, serverID string) error { @@ -171,13 +131,8 @@ func (c *openstackClient) DeleteServer(ctx context.Context, serverID string) err // Create a snapshot of the specified server func (c *openstackClient) CreateServerSnapshot(ctx context.Context, serverID, snapshotName string) (string, error) { - // Create image options createImageOpts := servers.CreateImageOpts{ Name: snapshotName, - Metadata: map[string]string{ - "snapshot_type": "server_snapshot", - "source_server": serverID, - }, } result := servers.CreateImage(c.computeClient, serverID, createImageOpts) @@ -189,65 +144,47 @@ func (c *openstackClient) CreateServerSnapshot(ctx context.Context, serverID, sn return imageID, nil } +func (c *openstackClient) ListImages(ctx context.Context, opts *images.ListOpts) ([]images.Image, error) { + if opts == nil { + opts = &images.ListOpts{} + } + allPages, err := images.List(c.imageClient, opts).AllPages() + if err != nil { + return nil, err + } + return images.ExtractImages(allPages) +} + +func (c *openstackClient) DeleteImage(ctx context.Context, imageID string) error { + return images.Delete(c.imageClient, imageID).ExtractErr() +} + // Creates a new server -func (c *openstackClient) CreateServerFromOpts(ctx context.Context, opts *ServerCreateOpts) (*servers.Server, error) { - // Build networks slice for the NIC - networks := []servers.Network{ - { - UUID: opts.NetworkID, - Port: opts.PortID, - }, - } - - // Convert custom options to satisfy GopherCloud - createOpts := &servers.CreateOpts{ - Name: opts.Name, - ImageRef: opts.ImageRef, - FlavorRef: opts.FlavorRef, - Networks: networks, - SecurityGroups: opts.SecurityGroups, - Metadata: opts.Metadata, - } - - server, err := servers.Create(c.computeClient, createOpts).Extract() +func (c *openstackClient) CreateServerFromOpts(ctx context.Context, opts *servers.CreateOpts) (*servers.Server, error) { + server, err := servers.Create(c.computeClient, opts).Extract() if err != nil { return nil, fmt.Errorf("failed to create server: %w", err) } - return server, nil } // Image methods func (c *openstackClient) GetImage(ctx context.Context, imageID string) (*images.Image, error) { - image, err := images.Get(c.imageClient, imageID).Extract() - return image, err + return images.Get(c.imageClient, imageID).Extract() } // Ports -func (c *openstackClient) ListPorts(ctx context.Context) ([]Port, error) { +func (c *openstackClient) ListPorts(ctx context.Context) ([]ports.Port, error) { allPages, err := ports.List(c.networkClient, nil).AllPages() if err != nil { return nil, err } - portList, err := ports.ExtractPorts(allPages) - if err != nil { - return nil, err - } - - var result []Port - for _, port := range portList { - result = append(result, Port{ - ID: port.ID, - Name: port.Name, - }) - } - - return result, nil + return ports.ExtractPorts(allPages) } // Network specific -func (c *openstackClient) GetNetworkByName(ctx context.Context, networkName string) (*Network, error) { +func (c *openstackClient) GetNetworkByName(ctx context.Context, networkName string) (*networks.Network, error) { listOpts := networks.ListOpts{ Name: networkName, } @@ -266,17 +203,12 @@ func (c *openstackClient) GetNetworkByName(ctx context.Context, networkName stri return nil, fmt.Errorf("network with name '%s' not found", networkName) } - // Return the first match - net := networkList[0] - return &Network{ - ID: net.ID, - Name: net.Name, - Status: net.Status, - }, nil + // Return the first match directly + return &networkList[0], nil } // Get the security group names for a specific server -func (c *openstackClient) GetServerSecurityGroups(ctx context.Context, serverID string) ([]string, error) { +func (c *openstackClient) GetServerSecurityGroupNames(ctx context.Context, serverID string) ([]string, error) { serverSecGroups, err := secgroups.ListByServer(c.computeClient, serverID).AllPages() if err != nil { return nil, err @@ -296,10 +228,10 @@ func (c *openstackClient) GetServerSecurityGroups(ctx context.Context, serverID } // Gets the compute quotas for the current project -func (c *openstackClient) GetComputeQuotas(ctx context.Context) (*ComputeQuotas, error) { +func (c *openstackClient) GetComputeQuotas(ctx context.Context) (*quotasets.QuotaSet, error) { projectID := c.credentials.ProjectID if projectID == "" { - projectID = c.credentials.TenantID // fallback to legacy field + projectID = c.credentials.TenantID } if projectID == "" { return nil, fmt.Errorf("no project ID found in credentials") @@ -310,28 +242,21 @@ func (c *openstackClient) GetComputeQuotas(ctx context.Context) (*ComputeQuotas, return nil, fmt.Errorf("failed to get quota set: %w", err) } - return &ComputeQuotas{ - Instances: quotaSet.Instances, - Cores: quotaSet.Cores, - RAM: quotaSet.RAM, - }, nil + return quotaSet, nil } // Gets the current compute usage for the project -func (c *openstackClient) GetComputeUsage(ctx context.Context) (*ComputeUsage, error) { +func (c *openstackClient) GetComputeUsage(ctx context.Context) (*usage.TenantUsage, error) { projectID := c.credentials.ProjectID if projectID == "" { - projectID = c.credentials.TenantID // fallback to legacy field + projectID = c.credentials.TenantID } if projectID == "" { return nil, fmt.Errorf("no project ID found in credentials") } - // Create usage options - usageOpts := usage.SingleTenantOpts{} - // Get usage for current project - allPages, err := usage.SingleTenant(c.computeClient, projectID, usageOpts).AllPages() + allPages, err := usage.SingleTenant(c.computeClient, projectID, usage.SingleTenantOpts{}).AllPages() if err != nil { return nil, fmt.Errorf("failed to get compute usage pages: %w", err) } @@ -342,48 +267,25 @@ func (c *openstackClient) GetComputeUsage(ctx context.Context) (*ComputeUsage, e return nil, fmt.Errorf("failed to extract compute usage: %w", err) } - // Check if we got valid usage data - if tenantUsage == nil { - return &ComputeUsage{ - InstancesUsed: 0, - CoresUsed: 0, - RAMUsed: 0, - }, nil - } - - return &ComputeUsage{ - InstancesUsed: len(tenantUsage.ServerUsages), - CoresUsed: int(tenantUsage.TotalVCPUsUsage), - RAMUsed: int(tenantUsage.TotalMemoryMBUsage), - }, nil + // Note: tenantUsage can be nil - caller needs to handle this + return tenantUsage, nil } // Gets detailed information about a specific flavor -func (c *openstackClient) GetFlavorDetails(ctx context.Context, flavorID string) (*FlavorDetails, error) { - flavor, err := flavors.Get(c.computeClient, flavorID).Extract() - if err != nil { - return nil, fmt.Errorf("failed to get flavor details: %w", err) - } - - return &FlavorDetails{ - ID: flavor.ID, - Name: flavor.Name, - VCPUs: flavor.VCPUs, - RAM: flavor.RAM, - Disk: flavor.Disk, - }, nil +func (c *openstackClient) GetFlavorDetails(ctx context.Context, flavorID string) (*flavors.Flavor, error) { + return flavors.Get(c.computeClient, flavorID).Extract() } // Create a client wrapper object for interacting with OpenStack. func NewClientFromSecret(secret *corev1.Secret) (Client, error) { - cloudsYaml, ok := secret.Data["clouds.yaml"] + cloudsYaml, ok := secret.Data[constants.OpenStackCredentialsName] if !ok { - return nil, fmt.Errorf("secret does not contain \"clouds.yaml\" data") + return nil, fmt.Errorf("secret does not contain %q data", constants.OpenStackCredentialsName) } return newClientFromCloudsYAML(cloudsYaml) } -// newClientFromCloudsYAML creates a client from clouds.yaml data +// Creates a client from clouds.yaml data func newClientFromCloudsYAML(cloudsYamlData []byte) (Client, error) { var clouds CloudsYAML if err := yaml.Unmarshal(cloudsYamlData, &clouds); err != nil { @@ -415,44 +317,33 @@ func newClientFromCloudsYAML(cloudsYamlData []byte) (Client, error) { return newClientFromStruct(creds) } -// newClientFromStruct creates a client directly from a Credentials struct (no JSON conversion needed) +// getRegion helper function +func getRegion(creds *Credentials) string { + if creds.RegionName != "" { + return creds.RegionName + } + if creds.Region != "" { + return creds.Region + } + return "RegionOne" +} + func newClientFromStruct(creds *Credentials) (*openstackClient, error) { // Validate required credentials if creds.AuthURL == "" { return nil, fmt.Errorf("missing auth_url in credentials") } - // Create authentication options + // Authentication options authOpts := gophercloud.AuthOptions{ IdentityEndpoint: creds.AuthURL, Username: creds.Username, UserID: creds.UserID, Password: creds.Password, - TenantID: creds.ProjectID, // Use ProjectID as TenantID + TenantID: creds.ProjectID, TenantName: creds.ProjectName, DomainName: creds.UserDomainName, - } - - if creds.TenantID != "" { - authOpts.TenantID = creds.TenantID - } - if creds.TenantName != "" { - authOpts.TenantName = creds.TenantName - } - if creds.DomainID != "" { - authOpts.DomainID = creds.DomainID - } - if creds.DomainName != "" { - authOpts.DomainName = creds.DomainName - } - if creds.UserDomainID != "" { - authOpts.DomainID = creds.UserDomainID - } - if creds.ProjectDomainID != "" { - authOpts.DomainID = creds.ProjectDomainID - } - if creds.ProjectDomainName != "" { - authOpts.DomainName = creds.ProjectDomainName + DomainID: creds.UserDomainID, } // Authenticate and get provider client @@ -461,25 +352,14 @@ func newClientFromStruct(creds *Credentials) (*openstackClient, error) { return nil, fmt.Errorf("failed to authenticate with OpenStack: %w", err) } - // Set region - region := creds.RegionName - if region == "" { - region = creds.Region - } - if region == "" { - region = "RegionOne" - } + region := getRegion(creds) interfaceType := gophercloud.AvailabilityPublic - if creds.Interface != "" { - switch creds.Interface { - case "public": - interfaceType = gophercloud.AvailabilityPublic - case "internal": - interfaceType = gophercloud.AvailabilityInternal - case "admin": - interfaceType = gophercloud.AvailabilityAdmin - } + switch creds.Interface { + case "internal": + interfaceType = gophercloud.AvailabilityInternal + case "admin": + interfaceType = gophercloud.AvailabilityAdmin } endpointOpts := gophercloud.EndpointOpts{ diff --git a/pkg/openstackclient/mock/client_generated.go b/pkg/openstackclient/mock/client_generated.go index 7d52fb6b758..23dc54fb60b 100644 --- a/pkg/openstackclient/mock/client_generated.go +++ b/pkg/openstackclient/mock/client_generated.go @@ -9,9 +9,13 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" + quotasets "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" + usage "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage" + flavors "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" servers "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" images "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" - openstackclient "github.com/openshift/hive/pkg/openstackclient" + networks "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" + ports "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" ) // MockClient is a mock of Client interface. @@ -38,7 +42,7 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder { } // CreateServerFromOpts mocks base method. -func (m *MockClient) CreateServerFromOpts(ctx context.Context, opts *openstackclient.ServerCreateOpts) (*servers.Server, error) { +func (m *MockClient) CreateServerFromOpts(ctx context.Context, opts *servers.CreateOpts) (*servers.Server, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "CreateServerFromOpts", ctx, opts) ret0, _ := ret[0].(*servers.Server) @@ -67,6 +71,20 @@ func (mr *MockClientMockRecorder) CreateServerSnapshot(ctx, serverID, snapshotNa return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateServerSnapshot", reflect.TypeOf((*MockClient)(nil).CreateServerSnapshot), ctx, serverID, snapshotName) } +// DeleteImage mocks base method. +func (m *MockClient) DeleteImage(ctx context.Context, imageID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteImage", ctx, imageID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteImage indicates an expected call of DeleteImage. +func (mr *MockClientMockRecorder) DeleteImage(ctx, imageID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteImage", reflect.TypeOf((*MockClient)(nil).DeleteImage), ctx, imageID) +} + // DeleteServer mocks base method. func (m *MockClient) DeleteServer(ctx context.Context, serverID string) error { m.ctrl.T.Helper() @@ -82,10 +100,10 @@ func (mr *MockClientMockRecorder) DeleteServer(ctx, serverID interface{}) *gomoc } // GetComputeQuotas mocks base method. -func (m *MockClient) GetComputeQuotas(ctx context.Context) (*openstackclient.ComputeQuotas, error) { +func (m *MockClient) GetComputeQuotas(ctx context.Context) (*quotasets.QuotaSet, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetComputeQuotas", ctx) - ret0, _ := ret[0].(*openstackclient.ComputeQuotas) + ret0, _ := ret[0].(*quotasets.QuotaSet) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -97,10 +115,10 @@ func (mr *MockClientMockRecorder) GetComputeQuotas(ctx interface{}) *gomock.Call } // GetComputeUsage mocks base method. -func (m *MockClient) GetComputeUsage(ctx context.Context) (*openstackclient.ComputeUsage, error) { +func (m *MockClient) GetComputeUsage(ctx context.Context) (*usage.TenantUsage, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetComputeUsage", ctx) - ret0, _ := ret[0].(*openstackclient.ComputeUsage) + ret0, _ := ret[0].(*usage.TenantUsage) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -112,10 +130,10 @@ func (mr *MockClientMockRecorder) GetComputeUsage(ctx interface{}) *gomock.Call } // GetFlavorDetails mocks base method. -func (m *MockClient) GetFlavorDetails(ctx context.Context, flavorID string) (*openstackclient.FlavorDetails, error) { +func (m *MockClient) GetFlavorDetails(ctx context.Context, flavorID string) (*flavors.Flavor, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetFlavorDetails", ctx, flavorID) - ret0, _ := ret[0].(*openstackclient.FlavorDetails) + ret0, _ := ret[0].(*flavors.Flavor) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -142,10 +160,10 @@ func (mr *MockClientMockRecorder) GetImage(ctx, imageID interface{}) *gomock.Cal } // GetNetworkByName mocks base method. -func (m *MockClient) GetNetworkByName(ctx context.Context, networkName string) (*openstackclient.Network, error) { +func (m *MockClient) GetNetworkByName(ctx context.Context, networkName string) (*networks.Network, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetNetworkByName", ctx, networkName) - ret0, _ := ret[0].(*openstackclient.Network) + ret0, _ := ret[0].(*networks.Network) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -171,26 +189,41 @@ func (mr *MockClientMockRecorder) GetServer(ctx, serverID interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServer", reflect.TypeOf((*MockClient)(nil).GetServer), ctx, serverID) } -// GetServerSecurityGroups mocks base method. -func (m *MockClient) GetServerSecurityGroups(ctx context.Context, serverID string) ([]string, error) { +// GetServerSecurityGroupNames mocks base method. +func (m *MockClient) GetServerSecurityGroupNames(ctx context.Context, serverID string) ([]string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetServerSecurityGroups", ctx, serverID) + ret := m.ctrl.Call(m, "GetServerSecurityGroupNames", ctx, serverID) ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetServerSecurityGroups indicates an expected call of GetServerSecurityGroups. -func (mr *MockClientMockRecorder) GetServerSecurityGroups(ctx, serverID interface{}) *gomock.Call { +// GetServerSecurityGroupNames indicates an expected call of GetServerSecurityGroupNames. +func (mr *MockClientMockRecorder) GetServerSecurityGroupNames(ctx, serverID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerSecurityGroupNames", reflect.TypeOf((*MockClient)(nil).GetServerSecurityGroupNames), ctx, serverID) +} + +// ListImages mocks base method. +func (m *MockClient) ListImages(ctx context.Context, opts *images.ListOpts) ([]images.Image, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListImages", ctx, opts) + ret0, _ := ret[0].([]images.Image) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListImages indicates an expected call of ListImages. +func (mr *MockClientMockRecorder) ListImages(ctx, opts interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerSecurityGroups", reflect.TypeOf((*MockClient)(nil).GetServerSecurityGroups), ctx, serverID) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImages", reflect.TypeOf((*MockClient)(nil).ListImages), ctx, opts) } // ListPorts mocks base method. -func (m *MockClient) ListPorts(ctx context.Context) ([]openstackclient.Port, error) { +func (m *MockClient) ListPorts(ctx context.Context) ([]ports.Port, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ListPorts", ctx) - ret0, _ := ret[0].([]openstackclient.Port) + ret0, _ := ret[0].([]ports.Port) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/vendor/github.com/gophercloud/gophercloud/.gitignore b/vendor/github.com/gophercloud/gophercloud/.gitignore new file mode 100644 index 00000000000..d7a5e5293a9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/.gitignore @@ -0,0 +1,4 @@ +**/*.swp +.idea +.vscode +testing_*.coverprofile diff --git a/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md b/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md new file mode 100644 index 00000000000..b470df398bc --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md @@ -0,0 +1,883 @@ +## v1.7.0 (2023-09-22) + +New features and improvements: + +* [GH-2782](https://github.com/gophercloud/gophercloud/pull/2782) [v1] (manual clean backport) Add tag field to compute block_device_v2 + +CI changes: + +* [GH-2760](https://github.com/gophercloud/gophercloud/pull/2760) [v1 backports] semver auto labels +* [GH-2775](https://github.com/gophercloud/gophercloud/pull/2775) [v1] Fix typos in comments +* [GH-2783](https://github.com/gophercloud/gophercloud/pull/2783) [v1] (clean manual backport) ci/functional: fix ubuntu version & add antelope +* [GH-2785](https://github.com/gophercloud/gophercloud/pull/2785) [v1] Acceptance: Handle numerical version names in version comparison helpers +* [GH-2787](https://github.com/gophercloud/gophercloud/pull/2787) backport-v1: fixes to semver label +* [GH-2788](https://github.com/gophercloud/gophercloud/pull/2788) [v1] Make acceptance tests internal + + +## v1.6.0 (2023-08-30) + +New features and improvements: + +* [GH-2712](https://github.com/gophercloud/gophercloud/pull/2712) [v1] README: minor change to test backport workflow +* [GH-2713](https://github.com/gophercloud/gophercloud/pull/2713) [v1] tests: run MultiAttach with a capable Cinder Type +* [GH-2714](https://github.com/gophercloud/gophercloud/pull/2714) [v1] Add CRUD support for encryption in volume v3 types +* [GH-2715](https://github.com/gophercloud/gophercloud/pull/2715) [v1] Add projectID to fwaas_v2 policy CreateOpts and ListOpts +* [GH-2716](https://github.com/gophercloud/gophercloud/pull/2716) [v1] Add projectID to fwaas_v2 CreateOpts +* [GH-2717](https://github.com/gophercloud/gophercloud/pull/2717) [v1] [manila]: add reset and force delete actions to a snapshot +* [GH-2718](https://github.com/gophercloud/gophercloud/pull/2718) [v1] [cinder]: add reset and force delete actions to volumes and snapshots +* [GH-2721](https://github.com/gophercloud/gophercloud/pull/2721) [v1] orchestration: Explicit error in optionsmap creation +* [GH-2723](https://github.com/gophercloud/gophercloud/pull/2723) [v1] Add conductor API to Baremetal V1 +* [GH-2729](https://github.com/gophercloud/gophercloud/pull/2729) [v1] networking/v2/ports: allow list filter by security group + +CI changes: + +* [GH-2675](https://github.com/gophercloud/gophercloud/pull/2675) [v1][CI] Drop periodic jobs from stable branch +* [GH-2683](https://github.com/gophercloud/gophercloud/pull/2683) [v1] CI tweaks + + +## v1.5.0 (2023-06-21) + +New features and improvements: + +* [GH-2634](https://github.com/gophercloud/gophercloud/pull/2634) baremetal: update inspection inventory with recent additions +* [GH-2635](https://github.com/gophercloud/gophercloud/pull/2635) [manila]: Add Share Replicas support +* [GH-2637](https://github.com/gophercloud/gophercloud/pull/2637) [FWaaS_v2]: Add FWaaS_V2 workflow and enable tests +* [GH-2639](https://github.com/gophercloud/gophercloud/pull/2639) Implement errors.Unwrap() on unexpected status code errors +* [GH-2648](https://github.com/gophercloud/gophercloud/pull/2648) [manila]: implement share transfer API + + +## v1.4.0 (2023-05-25) + +New features and improvements: + +* [GH-2465](https://github.com/gophercloud/gophercloud/pull/2465) keystone: add v3 limits update operation +* [GH-2596](https://github.com/gophercloud/gophercloud/pull/2596) keystone: add v3 limits get operation +* [GH-2618](https://github.com/gophercloud/gophercloud/pull/2618) keystone: add v3 limits delete operation +* [GH-2616](https://github.com/gophercloud/gophercloud/pull/2616) Add CRUD support for register limit APIs +* [GH-2610](https://github.com/gophercloud/gophercloud/pull/2610) Add PUT/HEAD/DELETE for identity/v3/OS-INHERIT +* [GH-2597](https://github.com/gophercloud/gophercloud/pull/2597) Add validation and optimise objects.BulkDelete +* [GH-2602](https://github.com/gophercloud/gophercloud/pull/2602) [swift v1]: introduce a TempURLKey argument for objects.CreateTempURLOpts struct +* [GH-2623](https://github.com/gophercloud/gophercloud/pull/2623) Add the ability to remove ingress/egress policies from fwaas_v2 groups +* [GH-2625](https://github.com/gophercloud/gophercloud/pull/2625) neutron: Support trunk_details extension + +CI changes: + +* [GH-2608](https://github.com/gophercloud/gophercloud/pull/2608) Drop train and ussuri jobs +* [GH-2589](https://github.com/gophercloud/gophercloud/pull/2589) Bump EmilienM/devstack-action from 0.10 to 0.11 +* [GH-2604](https://github.com/gophercloud/gophercloud/pull/2604) Bump mheap/github-action-required-labels from 3 to 4 +* [GH-2620](https://github.com/gophercloud/gophercloud/pull/2620) Pin goimport dep to a version that works with go 1.14 +* [GH-2619](https://github.com/gophercloud/gophercloud/pull/2619) Fix version comparison for acceptance tests +* [GH-2627](https://github.com/gophercloud/gophercloud/pull/2627) Limits: Fix ToDo to create registered limit and use it +* [GH-2629](https://github.com/gophercloud/gophercloud/pull/2629) [manila]: Add share from snapshot restore functional test + + +## v1.3.0 (2023-03-28) + +* [GH-2464](https://github.com/gophercloud/gophercloud/pull/2464) keystone: add v3 limits create operation +* [GH-2512](https://github.com/gophercloud/gophercloud/pull/2512) Manila: add List for share-access-rules API +* [GH-2529](https://github.com/gophercloud/gophercloud/pull/2529) Added target state "rebuild" for Ironic nodes +* [GH-2539](https://github.com/gophercloud/gophercloud/pull/2539) Add release instructions +* [GH-2540](https://github.com/gophercloud/gophercloud/pull/2540) [all] IsEmpty to check for HTTP status 204 +* [GH-2543](https://github.com/gophercloud/gophercloud/pull/2543) keystone: add v3 OS-FEDERATION mappings get operation +* [GH-2545](https://github.com/gophercloud/gophercloud/pull/2545) baremetal: add inspection_{started,finished}_at to Node +* [GH-2546](https://github.com/gophercloud/gophercloud/pull/2546) Drop train job for baremetal +* [GH-2549](https://github.com/gophercloud/gophercloud/pull/2549) objects: Clarify ExtractContent usage +* [GH-2550](https://github.com/gophercloud/gophercloud/pull/2550) keystone: add v3 OS-FEDERATION mappings update operation +* [GH-2552](https://github.com/gophercloud/gophercloud/pull/2552) objectstorage: Reject container names with a slash +* [GH-2555](https://github.com/gophercloud/gophercloud/pull/2555) nova: introduce servers.ListSimple along with the more detailed servers.List +* [GH-2558](https://github.com/gophercloud/gophercloud/pull/2558) Expand docs on 'clientconfig' usage +* [GH-2563](https://github.com/gophercloud/gophercloud/pull/2563) Support propagate_uplink_status for Ports +* [GH-2567](https://github.com/gophercloud/gophercloud/pull/2567) Fix invalid baremetal-introspection service type +* [GH-2568](https://github.com/gophercloud/gophercloud/pull/2568) Prefer github mirrors over opendev repos +* [GH-2571](https://github.com/gophercloud/gophercloud/pull/2571) Swift V1: support object versioning +* [GH-2572](https://github.com/gophercloud/gophercloud/pull/2572) networking v2: add extraroutes Add and Remove methods +* [GH-2573](https://github.com/gophercloud/gophercloud/pull/2573) Enable tests for object versioning +* [GH-2576](https://github.com/gophercloud/gophercloud/pull/2576) keystone: add v3 OS-FEDERATION mappings delete operation +* [GH-2578](https://github.com/gophercloud/gophercloud/pull/2578) Add periodic jobs for OpenStack zed release and reduce periodic jobs frequency +* [GH-2580](https://github.com/gophercloud/gophercloud/pull/2580) [neutron v2]: Add support for network segments update +* [GH-2583](https://github.com/gophercloud/gophercloud/pull/2583) Add missing rule protocol constants for IPIP +* [GH-2584](https://github.com/gophercloud/gophercloud/pull/2584) CI: workaround mongodb dependency for messaging and clustering master jobs +* [GH-2587](https://github.com/gophercloud/gophercloud/pull/2587) fix: Incorrect Documentation +* [GH-2593](https://github.com/gophercloud/gophercloud/pull/2593) Make TestMTUNetworkCRUDL deterministic +* [GH-2594](https://github.com/gophercloud/gophercloud/pull/2594) Bump actions/setup-go from 3 to 4 + + +## v1.2.0 (2023-01-27) + +Starting with this version, Gophercloud sends its actual version in the +user-agent string in the format `gophercloud/v1.2.0`. It no longer sends the +hardcoded string `gophercloud/2.0.0`. + +* [GH-2542](https://github.com/gophercloud/gophercloud/pull/2542) Add field hidden in containerinfra/v1/clustertemplates +* [GH-2537](https://github.com/gophercloud/gophercloud/pull/2537) Support value_specs for Ports +* [GH-2530](https://github.com/gophercloud/gophercloud/pull/2530) keystone: add v3 OS-FEDERATION mappings create operation +* [GH-2519](https://github.com/gophercloud/gophercloud/pull/2519) Modify user-agent header to ensure current gophercloud version is provided + +## v1.1.1 (2022-12-07) + +The GOPROXY cache for v1.1.0 was corrupted with a tag pointing to the wrong commit. This release fixes the problem by exposing a new release with the same content. + +Please use `v1.1.1` instead of `v1.1.0` to avoid cache issues. + +## v1.1.0 (2022-11-24) + +* [GH-2513](https://github.com/gophercloud/gophercloud/pull/2513) objectstorage: Do not parse NoContent responses +* [GH-2503](https://github.com/gophercloud/gophercloud/pull/2503) Bump golang.org/x/crypto +* [GH-2501](https://github.com/gophercloud/gophercloud/pull/2501) Staskraev/l3 agent scheduler +* [GH-2496](https://github.com/gophercloud/gophercloud/pull/2496) Manila: add Get for share-access-rules API +* [GH-2491](https://github.com/gophercloud/gophercloud/pull/2491) Add VipQosPolicyID to loadbalancer Create and Update +* [GH-2488](https://github.com/gophercloud/gophercloud/pull/2488) Add Persistance for octavia pools.UpdateOpts +* [GH-2487](https://github.com/gophercloud/gophercloud/pull/2487) Add Prometheus protocol for octavia listeners +* [GH-2482](https://github.com/gophercloud/gophercloud/pull/2482) Add createdAt, updatedAt and provisionUpdatedAt fields in Baremetal V1 nodes +* [GH-2479](https://github.com/gophercloud/gophercloud/pull/2479) Add service_types support for neutron subnet +* [GH-2477](https://github.com/gophercloud/gophercloud/pull/2477) Port CreatedAt and UpdatedAt: add back JSON tags +* [GH-2475](https://github.com/gophercloud/gophercloud/pull/2475) Support old time format for port CreatedAt and UpdatedAt +* [GH-2474](https://github.com/gophercloud/gophercloud/pull/2474) Implementing re-image volumeaction +* [GH-2470](https://github.com/gophercloud/gophercloud/pull/2470) keystone: add v3 limits GetEnforcementModel operation +* [GH-2468](https://github.com/gophercloud/gophercloud/pull/2468) keystone: add v3 OS-FEDERATION extension List Mappings +* [GH-2458](https://github.com/gophercloud/gophercloud/pull/2458) Fix typo in blockstorage/v3/attachments docs +* [GH-2456](https://github.com/gophercloud/gophercloud/pull/2456) Add support for Update for flavors +* [GH-2453](https://github.com/gophercloud/gophercloud/pull/2453) Add description to flavor +* [GH-2417](https://github.com/gophercloud/gophercloud/pull/2417) Neutron v2: ScheduleBGPSpeakerOpts, RemoveBGPSpeaker, Lis… + +## 1.0.0 (2022-08-29) + +UPGRADE NOTES + PROMISE OF COMPATIBILITY + +* Introducing Gophercloud v1! Like for every other release so far, all clients will upgrade automatically with `go get -d github.com/gophercloud/gophercloud` unless the dependency is pinned in `go.mod`. +* Gophercloud v1 comes with a promise of compatibility: no breaking changes are expected to merge before v2.0.0. + +IMPROVEMENTS + +* Added `compute.v2/extensions/services.Delete` [GH-2427](https://github.com/gophercloud/gophercloud/pull/2427) +* Added support for `standard-attr-revisions` to `networking/v2/networks`, `networking/v2/ports`, and `networking/v2/subnets` [GH-2437](https://github.com/gophercloud/gophercloud/pull/2437) +* Added `updated_at` and `created_at` fields to `networking/v2/ports.Port` [GH-2445](https://github.com/gophercloud/gophercloud/pull/2445) + +## 0.25.0 (May 30, 2022) + +BREAKING CHANGES + +* Replaced `blockstorage/noauth.NewBlockStorageNoAuth` with `NewBlockStorageNoAuthV2` and `NewBlockStorageNoAuthV3` [GH-2343](https://github.com/gophercloud/gophercloud/pull/2343) +* Renamed `blockstorage/extensions/schedulerstats.Capabilities`'s `GoodnessFuction` field to `GoodnessFunction` [GH-2346](https://github.com/gophercloud/gophercloud/pull/2346) + +IMPROVEMENTS + +* Added `RequestOpts.OmitHeaders` to provider client [GH-2315](https://github.com/gophercloud/gophercloud/pull/2315) +* Added `identity/v3/extensions/projectendpoints.List` [GH-2304](https://github.com/gophercloud/gophercloud/pull/2304) +* Added `identity/v3/extensions/projectendpoints.Create` [GH-2304](https://github.com/gophercloud/gophercloud/pull/2304) +* Added `identity/v3/extensions/projectendpoints.Delete` [GH-2304](https://github.com/gophercloud/gophercloud/pull/2304) +* Added protocol `any` to `networking/v2/extensions/security/rules.Create` [GH-2310](https://github.com/gophercloud/gophercloud/pull/2310) +* Added `REDIRECT_PREFIX` and `REDIRECT_HTTP_CODE` to `loadbalancer/v2/l7policies.Create` [GH-2324](https://github.com/gophercloud/gophercloud/pull/2324) +* Added `SOURCE_IP_PORT` LB method to `loadbalancer/v2/pools.Create` [GH-2300](https://github.com/gophercloud/gophercloud/pull/2300) +* Added `AllocatedCapacityGB` capability to `blockstorage/extensions/schedulerstats.Capabilities` [GH-2348](https://github.com/gophercloud/gophercloud/pull/2348) +* Added `Metadata` to `dns/v2/recordset.RecordSet` [GH-2353](https://github.com/gophercloud/gophercloud/pull/2353) +* Added missing fields to `compute/v2/extensions/servergroups.List` [GH-2355](https://github.com/gophercloud/gophercloud/pull/2355) +* Added missing labels fields to `containerinfra/v1/nodegroups` [GH-2377](https://github.com/gophercloud/gophercloud/pull/2377) +* Added missing fields to `loadbalancer/v2/listeners.Listener` [GH-2407](https://github.com/gophercloud/gophercloud/pull/2407) +* Added `identity/v3/limits.List` [GH-2360](https://github.com/gophercloud/gophercloud/pull/2360) +* Added `ParentProviderUUID` to `placement/v1/resourceproviders.Create` [GH-2356](https://github.com/gophercloud/gophercloud/pull/2356) +* Added `placement/v1/resourceproviders.Delete` [GH-2357](https://github.com/gophercloud/gophercloud/pull/2357) +* Added `placement/v1/resourceproviders.Get` [GH-2358](https://github.com/gophercloud/gophercloud/pull/2358) +* Added `placement/v1/resourceproviders.Update` [GH-2359](https://github.com/gophercloud/gophercloud/pull/2359) +* Added `networking/v2/extensions/bgp/peers.List` [GH-2241](https://github.com/gophercloud/gophercloud/pull/2241) +* Added `networking/v2/extensions/bgp/peers.Get` [GH-2241](https://github.com/gophercloud/gophercloud/pull/2241) +* Added `networking/v2/extensions/bgp/peers.Create` [GH-2388](https://github.com/gophercloud/gophercloud/pull/2388) +* Added `networking/v2/extensions/bgp/peers.Delete` [GH-2388](https://github.com/gophercloud/gophercloud/pull/2388) +* Added `networking/v2/extensions/bgp/peers.Update` [GH-2396](https://github.com/gophercloud/gophercloud/pull/2396) +* Added `networking/v2/extensions/bgp/speakers.Create` [GH-2395](https://github.com/gophercloud/gophercloud/pull/2395) +* Added `networking/v2/extensions/bgp/speakers.Delete` [GH-2395](https://github.com/gophercloud/gophercloud/pull/2395) +* Added `networking/v2/extensions/bgp/speakers.Update` [GH-2400](https://github.com/gophercloud/gophercloud/pull/2400) +* Added `networking/v2/extensions/bgp/speakers.AddBGPPeer` [GH-2400](https://github.com/gophercloud/gophercloud/pull/2400) +* Added `networking/v2/extensions/bgp/speakers.RemoveBGPPeer` [GH-2400](https://github.com/gophercloud/gophercloud/pull/2400) +* Added `networking/v2/extensions/bgp/speakers.GetAdvertisedRoutes` [GH-2406](https://github.com/gophercloud/gophercloud/pull/2406) +* Added `networking/v2/extensions/bgp/speakers.AddGatewayNetwork` [GH-2406](https://github.com/gophercloud/gophercloud/pull/2406) +* Added `networking/v2/extensions/bgp/speakers.RemoveGatewayNetwork` [GH-2406](https://github.com/gophercloud/gophercloud/pull/2406) +* Added `baremetal/v1/nodes.SetMaintenance` and `baremetal/v1/nodes.UnsetMaintenance` [GH-2384](https://github.com/gophercloud/gophercloud/pull/2384) +* Added `sharedfilesystems/v2/services.List` [GH-2350](https://github.com/gophercloud/gophercloud/pull/2350) +* Added `sharedfilesystems/v2/schedulerstats.List` [GH-2350](https://github.com/gophercloud/gophercloud/pull/2350) +* Added `sharedfilesystems/v2/schedulerstats.ListDetail` [GH-2350](https://github.com/gophercloud/gophercloud/pull/2350) +* Added ability to handle 502 and 504 errors [GH-2245](https://github.com/gophercloud/gophercloud/pull/2245) +* Added `IncludeSubtree` to `identity/v3/roles.ListAssignments` [GH-2411](https://github.com/gophercloud/gophercloud/pull/2411) + +## 0.24.0 (December 13, 2021) + +UPGRADE NOTES + +* Set Go minimum version to 1.14 [GH-2294](https://github.com/gophercloud/gophercloud/pull/2294) + +IMPROVEMENTS + +* Added `blockstorage/v3/qos.Get` [GH-2283](https://github.com/gophercloud/gophercloud/pull/2283) +* Added `blockstorage/v3/qos.Update` [GH-2283](https://github.com/gophercloud/gophercloud/pull/2283) +* Added `blockstorage/v3/qos.DeleteKeys` [GH-2283](https://github.com/gophercloud/gophercloud/pull/2283) +* Added `blockstorage/v3/qos.Associate` [GH-2284](https://github.com/gophercloud/gophercloud/pull/2284) +* Added `blockstorage/v3/qos.Disassociate` [GH-2284](https://github.com/gophercloud/gophercloud/pull/2284) +* Added `blockstorage/v3/qos.DisassociateAll` [GH-2284](https://github.com/gophercloud/gophercloud/pull/2284) +* Added `blockstorage/v3/qos.ListAssociations` [GH-2284](https://github.com/gophercloud/gophercloud/pull/2284) + +## 0.23.0 (November 12, 2021) + +IMPROVEMENTS + +* Added `networking/v2/extensions/agents.ListBGPSpeakers` [GH-2229](https://github.com/gophercloud/gophercloud/pull/2229) +* Added `networking/v2/extensions/bgp/speakers.BGPSpeaker` [GH-2229](https://github.com/gophercloud/gophercloud/pull/2229) +* Added `identity/v3/roles.Project.Domain` [GH-2235](https://github.com/gophercloud/gophercloud/pull/2235) +* Added `identity/v3/roles.User.Domain` [GH-2235](https://github.com/gophercloud/gophercloud/pull/2235) +* Added `identity/v3/roles.Group.Domain` [GH-2235](https://github.com/gophercloud/gophercloud/pull/2235) +* Added `loadbalancer/v2/pools.CreateOpts.Tags` [GH-2237](https://github.com/gophercloud/gophercloud/pull/2237) +* Added `loadbalancer/v2/pools.UpdateOpts.Tags` [GH-2237](https://github.com/gophercloud/gophercloud/pull/2237) +* Added `loadbalancer/v2/pools.Pool.Tags` [GH-2237](https://github.com/gophercloud/gophercloud/pull/2237) +* Added `networking/v2/extensions/bgp/speakers.List` [GH-2238](https://github.com/gophercloud/gophercloud/pull/2238) +* Added `networking/v2/extensions/bgp/speakers.Get` [GH-2238](https://github.com/gophercloud/gophercloud/pull/2238) +* Added `compute/v2/extensions/keypairs.CreateOpts.Type` [GH-2231](https://github.com/gophercloud/gophercloud/pull/2231) +* When doing Keystone re-authentification, keep the error if it failed [GH-2259](https://github.com/gophercloud/gophercloud/pull/2259) +* Added new loadbalancer pool monitor types (TLS-HELLO, UDP-CONNECT and SCTP) [GH-2237](https://github.com/gophercloud/gophercloud/pull/2261) + +## 0.22.0 (October 7, 2021) + +BREAKING CHANGES + +* The types of several Object Storage Update fields have been changed to pointers in order to allow the value to be unset via the HTTP headers: + * `objectstorage/v1/accounts.UpdateOpts.ContentType` + * `objectstorage/v1/accounts.UpdateOpts.DetectContentType` + * `objectstorage/v1/containers.UpdateOpts.ContainerRead` + * `objectstorage/v1/containers.UpdateOpts.ContainerSyncTo` + * `objectstorage/v1/containers.UpdateOpts.ContainerSyncKey` + * `objectstorage/v1/containers.UpdateOpts.ContainerWrite` + * `objectstorage/v1/containers.UpdateOpts.ContentType` + * `objectstorage/v1/containers.UpdateOpts.DetectContentType` + * `objectstorage/v1/objects.UpdateOpts.ContentDisposition` + * `objectstorage/v1/objects.UpdateOpts.ContentEncoding` + * `objectstorage/v1/objects.UpdateOpts.ContentType` + * `objectstorage/v1/objects.UpdateOpts.DeleteAfter` + * `objectstorage/v1/objects.UpdateOpts.DeleteAt` + * `objectstorage/v1/objects.UpdateOpts.DetectContentType` + +BUG FIXES + +* Fixed issue with not being able to unset Object Storage values via HTTP headers [GH-2218](https://github.com/gophercloud/gophercloud/pull/2218) + +IMPROVEMENTS + +* Added `compute/v2/servers.Server.ServerGroups` [GH-2217](https://github.com/gophercloud/gophercloud/pull/2217) +* Added `imageservice/v2/images.ReplaceImageProtected` to allow the `protected` field to be updated [GH-2221](https://github.com/gophercloud/gophercloud/pull/2221) +* More details added to the 404/Not Found error message [GH-2223](https://github.com/gophercloud/gophercloud/pull/2223) +* Added `openstack/baremetal/v1/nodes.CreateSubscriptionOpts.HttpHeaders` [GH-2224](https://github.com/gophercloud/gophercloud/pull/2224) + +## 0.21.0 (September 14, 2021) + +IMPROVEMENTS + +* Added `blockstorage/extensions/volumehost` [GH-2212](https://github.com/gophercloud/gophercloud/pull/2212) +* Added `loadbalancer/v2/listeners.CreateOpts.Tags` [GH-2214](https://github.com/gophercloud/gophercloud/pull/2214) +* Added `loadbalancer/v2/listeners.UpdateOpts.Tags` [GH-2214](https://github.com/gophercloud/gophercloud/pull/2214) +* Added `loadbalancer/v2/listeners.Listener.Tags` [GH-2214](https://github.com/gophercloud/gophercloud/pull/2214) + +## 0.20.0 (August 10, 2021) + +IMPROVEMENTS + +* Added `RetryFunc` to enable custom retry functions. [GH-2194](https://github.com/gophercloud/gophercloud/pull/2194) +* Added `openstack/baremetal/v1/nodes.GetVendorPassthruMethods` [GH-2201](https://github.com/gophercloud/gophercloud/pull/2201) +* Added `openstack/baremetal/v1/nodes.GetAllSubscriptions` [GH-2201](https://github.com/gophercloud/gophercloud/pull/2201) +* Added `openstack/baremetal/v1/nodes.GetSubscription` [GH-2201](https://github.com/gophercloud/gophercloud/pull/2201) +* Added `openstack/baremetal/v1/nodes.DeleteSubscription` [GH-2201](https://github.com/gophercloud/gophercloud/pull/2201) +* Added `openstack/baremetal/v1/nodes.CreateSubscription` [GH-2201](https://github.com/gophercloud/gophercloud/pull/2201) + +## 0.19.0 (July 22, 2021) + +NOTES / BREAKING CHANGES + +* `compute/v2/extensions/keypairs.List` now takes a `ListOptsBuilder` argument [GH-2186](https://github.com/gophercloud/gophercloud/pull/2186) +* `compute/v2/extensions/keypairs.Get` now takes a `GetOptsBuilder` argument [GH-2186](https://github.com/gophercloud/gophercloud/pull/2186) +* `compute/v2/extensions/keypairs.Delete` now takes a `DeleteOptsBuilder` argument [GH-2186](https://github.com/gophercloud/gophercloud/pull/2186) +* `compute/v2/extensions/hypervisors.List` now takes a `ListOptsBuilder` argument [GH-2187](https://github.com/gophercloud/gophercloud/pull/2187) + +IMPROVEMENTS + +* Added `blockstorage/v3/qos.List` [GH-2167](https://github.com/gophercloud/gophercloud/pull/2167) +* Added `compute/v2/extensions/volumeattach.CreateOpts.Tag` [GH-2177](https://github.com/gophercloud/gophercloud/pull/2177) +* Added `compute/v2/extensions/volumeattach.CreateOpts.DeleteOnTermination` [GH-2177](https://github.com/gophercloud/gophercloud/pull/2177) +* Added `compute/v2/extensions/volumeattach.VolumeAttachment.Tag` [GH-2177](https://github.com/gophercloud/gophercloud/pull/2177) +* Added `compute/v2/extensions/volumeattach.VolumeAttachment.DeleteOnTermination` [GH-2177](https://github.com/gophercloud/gophercloud/pull/2177) +* Added `db/v1/instances.Instance.Address` [GH-2179](https://github.com/gophercloud/gophercloud/pull/2179) +* Added `compute/v2/servers.ListOpts.AvailabilityZone` [GH-2098](https://github.com/gophercloud/gophercloud/pull/2098) +* Added `compute/v2/extensions/keypairs.ListOpts` [GH-2186](https://github.com/gophercloud/gophercloud/pull/2186) +* Added `compute/v2/extensions/keypairs.GetOpts` [GH-2186](https://github.com/gophercloud/gophercloud/pull/2186) +* Added `compute/v2/extensions/keypairs.DeleteOpts` [GH-2186](https://github.com/gophercloud/gophercloud/pull/2186) +* Added `objectstorage/v2/containers.GetHeader.Timestamp` [GH-2185](https://github.com/gophercloud/gophercloud/pull/2185) +* Added `compute/v2/extensions.ListOpts` [GH-2187](https://github.com/gophercloud/gophercloud/pull/2187) +* Added `sharedfilesystems/v2/shares.Share.CreateShareFromSnapshotSupport` [GH-2191](https://github.com/gophercloud/gophercloud/pull/2191) +* Added `compute/v2/servers.Network.Tag` for use in `CreateOpts` [GH-2193](https://github.com/gophercloud/gophercloud/pull/2193) + +## 0.18.0 (June 11, 2021) + +NOTES / BREAKING CHANGES + +* As of [GH-2160](https://github.com/gophercloud/gophercloud/pull/2160), Gophercloud no longer URL encodes Object Storage containers and object names. You can still encode them yourself before passing the names to the Object Storage functions. + +* `baremetal/v1/nodes.ListBIOSSettings` now takes three parameters. The third, new, parameter is `ListBIOSSettingsOptsBuilder` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) + +BUG FIXES + +* Fixed expected OK codes to use default codes [GH-2173](https://github.com/gophercloud/gophercloud/pull/2173) +* Fixed inablity to create sub-containers (objects with `/` in their name) [GH-2160](https://github.com/gophercloud/gophercloud/pull/2160) + +IMPROVEMENTS + +* Added `orchestration/v1/stacks.ListOpts.ShowHidden` [GH-2104](https://github.com/gophercloud/gophercloud/pull/2104) +* Added `loadbalancer/v2/listeners.ProtocolSCTP` [GH-2149](https://github.com/gophercloud/gophercloud/pull/2149) +* Added `loadbalancer/v2/listeners.CreateOpts.TLSVersions` [GH-2150](https://github.com/gophercloud/gophercloud/pull/2150) +* Added `loadbalancer/v2/listeners.UpdateOpts.TLSVersions` [GH-2150](https://github.com/gophercloud/gophercloud/pull/2150) +* Added `baremetal/v1/nodes.CreateOpts.NetworkData` [GH-2154](https://github.com/gophercloud/gophercloud/pull/2154) +* Added `baremetal/v1/nodes.Node.NetworkData` [GH-2154](https://github.com/gophercloud/gophercloud/pull/2154) +* Added `loadbalancer/v2/pools.ProtocolPROXYV2` [GH-2158](https://github.com/gophercloud/gophercloud/pull/2158) +* Added `loadbalancer/v2/pools.ProtocolSCTP` [GH-2158](https://github.com/gophercloud/gophercloud/pull/2158) +* Added `placement/v1/resourceproviders.GetAllocations` [GH-2162](https://github.com/gophercloud/gophercloud/pull/2162) +* Added `baremetal/v1/nodes.CreateOpts.BIOSInterface` [GH-2164](https://github.com/gophercloud/gophercloud/pull/2164) +* Added `baremetal/v1/nodes.Node.BIOSInterface` [GH-2164](https://github.com/gophercloud/gophercloud/pull/2164) +* Added `baremetal/v1/nodes.NodeValidation.BIOS` [GH-2164](https://github.com/gophercloud/gophercloud/pull/2164) +* Added `baremetal/v1/nodes.ListBIOSSettings` [GH-2171](https://github.com/gophercloud/gophercloud/pull/2171) +* Added `baremetal/v1/nodes.GetBIOSSetting` [GH-2171](https://github.com/gophercloud/gophercloud/pull/2171) +* Added `baremetal/v1/nodes.ListBIOSSettingsOpts` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) +* Added `baremetal/v1/nodes.BIOSSetting.AttributeType` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) +* Added `baremetal/v1/nodes.BIOSSetting.AllowableValues` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) +* Added `baremetal/v1/nodes.BIOSSetting.LowerBound` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) +* Added `baremetal/v1/nodes.BIOSSetting.UpperBound` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) +* Added `baremetal/v1/nodes.BIOSSetting.MinLength` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) +* Added `baremetal/v1/nodes.BIOSSetting.MaxLength` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) +* Added `baremetal/v1/nodes.BIOSSetting.ReadOnly` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) +* Added `baremetal/v1/nodes.BIOSSetting.ResetRequired` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) +* Added `baremetal/v1/nodes.BIOSSetting.Unique` [GH-2174](https://github.com/gophercloud/gophercloud/pull/2174) + +## 0.17.0 (April 9, 2021) + +IMPROVEMENTS + +* `networking/v2/extensions/quotas.QuotaDetail.Reserved` can handle both `int` and `string` values [GH-2126](https://github.com/gophercloud/gophercloud/pull/2126) +* Added `blockstorage/v3/volumetypes.ListExtraSpecs` [GH-2123](https://github.com/gophercloud/gophercloud/pull/2123) +* Added `blockstorage/v3/volumetypes.GetExtraSpec` [GH-2123](https://github.com/gophercloud/gophercloud/pull/2123) +* Added `blockstorage/v3/volumetypes.CreateExtraSpecs` [GH-2123](https://github.com/gophercloud/gophercloud/pull/2123) +* Added `blockstorage/v3/volumetypes.UpdateExtraSpec` [GH-2123](https://github.com/gophercloud/gophercloud/pull/2123) +* Added `blockstorage/v3/volumetypes.DeleteExtraSpec` [GH-2123](https://github.com/gophercloud/gophercloud/pull/2123) +* Added `identity/v3/roles.ListAssignmentOpts.IncludeNames` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `identity/v3/roles.AssignedRoles.Name` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `identity/v3/roles.Domain.Name` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `identity/v3/roles.Project.Name` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `identity/v3/roles.User.Name` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `identity/v3/roles.Group.Name` [GH-2133](https://github.com/gophercloud/gophercloud/pull/2133) +* Added `blockstorage/extensions/availabilityzones.List` [GH-2135](https://github.com/gophercloud/gophercloud/pull/2135) +* Added `blockstorage/v3/volumetypes.ListAccesses` [GH-2138](https://github.com/gophercloud/gophercloud/pull/2138) +* Added `blockstorage/v3/volumetypes.AddAccess` [GH-2138](https://github.com/gophercloud/gophercloud/pull/2138) +* Added `blockstorage/v3/volumetypes.RemoveAccess` [GH-2138](https://github.com/gophercloud/gophercloud/pull/2138) +* Added `blockstorage/v3/qos.Create` [GH-2140](https://github.com/gophercloud/gophercloud/pull/2140) +* Added `blockstorage/v3/qos.Delete` [GH-2140](https://github.com/gophercloud/gophercloud/pull/2140) + +## 0.16.0 (February 23, 2021) + +UPGRADE NOTES + +* `baremetal/v1/nodes.CleanStep.Interface` has changed from `string` to `StepInterface` [GH-2120](https://github.com/gophercloud/gophercloud/pull/2120) + +BUG FIXES + +* Fixed `xor` logic issues in `loadbalancers/v2/l7policies.CreateOpts` [GH-2087](https://github.com/gophercloud/gophercloud/pull/2087) +* Fixed `xor` logic issues in `loadbalancers/v2/listeners.CreateOpts` [GH-2087](https://github.com/gophercloud/gophercloud/pull/2087) +* Fixed `If-Modified-Since` so it's correctly sent in a `objectstorage/v1/objects.Download` request [GH-2108](https://github.com/gophercloud/gophercloud/pull/2108) +* Fixed `If-Unmodified-Since` so it's correctly sent in a `objectstorage/v1/objects.Download` request [GH-2108](https://github.com/gophercloud/gophercloud/pull/2108) + +IMPROVEMENTS + +* Added `blockstorage/extensions/limits.Get` [GH-2084](https://github.com/gophercloud/gophercloud/pull/2084) +* `clustering/v1/clusters.RemoveNodes` now returns an `ActionResult` [GH-2089](https://github.com/gophercloud/gophercloud/pull/2089) +* Added `identity/v3/projects.ListAvailable` [GH-2090](https://github.com/gophercloud/gophercloud/pull/2090) +* Added `blockstorage/extensions/backups.ListDetail` [GH-2085](https://github.com/gophercloud/gophercloud/pull/2085) +* Allow all ports to be removed in `networking/v2/extensions/fwaas_v2/groups.UpdateOpts` [GH-2073] +* Added `imageservice/v2/images.ListOpts.Hidden` [GH-2094](https://github.com/gophercloud/gophercloud/pull/2094) +* Added `imageservice/v2/images.CreateOpts.Hidden` [GH-2094](https://github.com/gophercloud/gophercloud/pull/2094) +* Added `imageservice/v2/images.ReplaceImageHidden` [GH-2094](https://github.com/gophercloud/gophercloud/pull/2094) +* Added `imageservice/v2/images.Image.Hidden` [GH-2094](https://github.com/gophercloud/gophercloud/pull/2094) +* Added `containerinfra/v1/clusters.CreateOpts.MasterLBEnabled` [GH-2102](https://github.com/gophercloud/gophercloud/pull/2102) +* Added the ability to define a custom function to handle "Retry-After" (429) responses [GH-2097](https://github.com/gophercloud/gophercloud/pull/2097) +* Added `baremetal/v1/nodes.JBOD` constant for the `RAIDLevel` type [GH-2103](https://github.com/gophercloud/gophercloud/pull/2103) +* Added support for Block Storage quotas of volume typed resources [GH-2109](https://github.com/gophercloud/gophercloud/pull/2109) +* Added `blockstorage/extensions/volumeactions.ChangeType` [GH-2113](https://github.com/gophercloud/gophercloud/pull/2113) +* Added `baremetal/v1/nodes.DeployStep` [GH-2120](https://github.com/gophercloud/gophercloud/pull/2120) +* Added `baremetal/v1/nodes.ProvisionStateOpts.DeploySteps` [GH-2120](https://github.com/gophercloud/gophercloud/pull/2120) +* Added `baremetal/v1/nodes.CreateOpts.AutomatedClean` [GH-2122](https://github.com/gophercloud/gophercloud/pull/2122) + +## 0.15.0 (December 27, 2020) + +BREAKING CHANGES + +* `compute/v2/extensions/servergroups.List` now takes a `ListOpts` parameter. You can pass `nil` if you don't need to use this. + +IMPROVEMENTS + +* Added `loadbalancer/v2/pools.CreateMemberOpts.Tags` [GH-2056](https://github.com/gophercloud/gophercloud/pull/2056) +* Added `loadbalancer/v2/pools.UpdateMemberOpts.Backup` [GH-2056](https://github.com/gophercloud/gophercloud/pull/2056) +* Added `loadbalancer/v2/pools.UpdateMemberOpts.MonitorAddress` [GH-2056](https://github.com/gophercloud/gophercloud/pull/2056) +* Added `loadbalancer/v2/pools.UpdateMemberOpts.MonitorPort` [GH-2056](https://github.com/gophercloud/gophercloud/pull/2056) +* Added `loadbalancer/v2/pools.UpdateMemberOpts.Tags` [GH-2056](https://github.com/gophercloud/gophercloud/pull/2056) +* Added `loadbalancer/v2/pools.BatchUpdateMemberOpts.Backup` [GH-2056](https://github.com/gophercloud/gophercloud/pull/2056) +* Added `loadbalancer/v2/pools.BatchUpdateMemberOpts.MonitorAddress` [GH-2056](https://github.com/gophercloud/gophercloud/pull/2056) +* Added `loadbalancer/v2/pools.BatchUpdateMemberOpts.MonitorPort` [GH-2056](https://github.com/gophercloud/gophercloud/pull/2056) +* Added `loadbalancer/v2/pools.BatchUpdateMemberOpts.Tags` [GH-2056](https://github.com/gophercloud/gophercloud/pull/2056) +* Added `networking/v2/extensions/quotas.GetDetail` [GH-2061](https://github.com/gophercloud/gophercloud/pull/2061) +* Added `networking/v2/extensions/quotas.UpdateOpts.Trunk` [GH-2061](https://github.com/gophercloud/gophercloud/pull/2061) +* Added `objectstorage/v1/accounts.UpdateOpts.RemoveMetadata` [GH-2063](https://github.com/gophercloud/gophercloud/pull/2063) +* Added `objectstorage/v1/objects.UpdateOpts.RemoveMetadata` [GH-2063](https://github.com/gophercloud/gophercloud/pull/2063) +* Added `identity/v3/catalog.List` [GH-2067](https://github.com/gophercloud/gophercloud/pull/2067) +* Added `networking/v2/extensions/fwaas_v2/policies.List` [GH-2057](https://github.com/gophercloud/gophercloud/pull/2057) +* Added `networking/v2/extensions/fwaas_v2/policies.Create` [GH-2057](https://github.com/gophercloud/gophercloud/pull/2057) +* Added `networking/v2/extensions/fwaas_v2/policies.Get` [GH-2057](https://github.com/gophercloud/gophercloud/pull/2057) +* Added `networking/v2/extensions/fwaas_v2/policies.Update` [GH-2057](https://github.com/gophercloud/gophercloud/pull/2057) +* Added `networking/v2/extensions/fwaas_v2/policies.Delete` [GH-2057](https://github.com/gophercloud/gophercloud/pull/2057) +* Added `compute/v2/extensions/servergroups.ListOpts.AllProjects` [GH-2070](https://github.com/gophercloud/gophercloud/pull/2070) +* Added `objectstorage/v1/containers.CreateOpts.StoragePolicy` [GH-2075](https://github.com/gophercloud/gophercloud/pull/2075) +* Added `blockstorage/v3/snapshots.Update` [GH-2081](https://github.com/gophercloud/gophercloud/pull/2081) +* Added `loadbalancer/v2/l7policies.CreateOpts.Rules` [GH-2077](https://github.com/gophercloud/gophercloud/pull/2077) +* Added `loadbalancer/v2/listeners.CreateOpts.DefaultPool` [GH-2077](https://github.com/gophercloud/gophercloud/pull/2077) +* Added `loadbalancer/v2/listeners.CreateOpts.L7Policies` [GH-2077](https://github.com/gophercloud/gophercloud/pull/2077) +* Added `loadbalancer/v2/listeners.Listener.DefaultPool` [GH-2077](https://github.com/gophercloud/gophercloud/pull/2077) +* Added `loadbalancer/v2/loadbalancers.CreateOpts.Listeners` [GH-2077](https://github.com/gophercloud/gophercloud/pull/2077) +* Added `loadbalancer/v2/loadbalancers.CreateOpts.Pools` [GH-2077](https://github.com/gophercloud/gophercloud/pull/2077) +* Added `loadbalancer/v2/pools.CreateOpts.Members` [GH-2077](https://github.com/gophercloud/gophercloud/pull/2077) +* Added `loadbalancer/v2/pools.CreateOpts.Monitor` [GH-2077](https://github.com/gophercloud/gophercloud/pull/2077) + + +## 0.14.0 (November 11, 2020) + +IMPROVEMENTS + +* Added `identity/v3/endpoints.Endpoint.Enabled` [GH-2030](https://github.com/gophercloud/gophercloud/pull/2030) +* Added `containerinfra/v1/clusters.Upgrade` [GH-2032](https://github.com/gophercloud/gophercloud/pull/2032) +* Added `compute/apiversions.List` [GH-2037](https://github.com/gophercloud/gophercloud/pull/2037) +* Added `compute/apiversions.Get` [GH-2037](https://github.com/gophercloud/gophercloud/pull/2037) +* Added `compute/v2/servers.ListOpts.IP` [GH-2038](https://github.com/gophercloud/gophercloud/pull/2038) +* Added `compute/v2/servers.ListOpts.IP6` [GH-2038](https://github.com/gophercloud/gophercloud/pull/2038) +* Added `compute/v2/servers.ListOpts.UserID` [GH-2038](https://github.com/gophercloud/gophercloud/pull/2038) +* Added `dns/v2/transfer/accept.List` [GH-2041](https://github.com/gophercloud/gophercloud/pull/2041) +* Added `dns/v2/transfer/accept.Get` [GH-2041](https://github.com/gophercloud/gophercloud/pull/2041) +* Added `dns/v2/transfer/accept.Create` [GH-2041](https://github.com/gophercloud/gophercloud/pull/2041) +* Added `dns/v2/transfer/requests.List` [GH-2041](https://github.com/gophercloud/gophercloud/pull/2041) +* Added `dns/v2/transfer/requests.Get` [GH-2041](https://github.com/gophercloud/gophercloud/pull/2041) +* Added `dns/v2/transfer/requests.Update` [GH-2041](https://github.com/gophercloud/gophercloud/pull/2041) +* Added `dns/v2/transfer/requests.Delete` [GH-2041](https://github.com/gophercloud/gophercloud/pull/2041) +* Added `baremetal/v1/nodes.RescueWait` [GH-2052](https://github.com/gophercloud/gophercloud/pull/2052) +* Added `baremetal/v1/nodes.Unrescuing` [GH-2052](https://github.com/gophercloud/gophercloud/pull/2052) +* Added `networking/v2/extensions/fwaas_v2/groups.List` [GH-2050](https://github.com/gophercloud/gophercloud/pull/2050) +* Added `networking/v2/extensions/fwaas_v2/groups.Get` [GH-2050](https://github.com/gophercloud/gophercloud/pull/2050) +* Added `networking/v2/extensions/fwaas_v2/groups.Create` [GH-2050](https://github.com/gophercloud/gophercloud/pull/2050) +* Added `networking/v2/extensions/fwaas_v2/groups.Update` [GH-2050](https://github.com/gophercloud/gophercloud/pull/2050) +* Added `networking/v2/extensions/fwaas_v2/groups.Delete` [GH-2050](https://github.com/gophercloud/gophercloud/pull/2050) + +BUG FIXES + +* Changed `networking/v2/extensions/layer3/routers.Routes` from `[]Route` to `*[]Route` [GH-2043](https://github.com/gophercloud/gophercloud/pull/2043) + +## 0.13.0 (September 27, 2020) + +IMPROVEMENTS + +* Added `ProtocolTerminatedHTTPS` as a valid listener protocol to `loadbalancer/v2/listeners` [GH-1992](https://github.com/gophercloud/gophercloud/pull/1992) +* Added `objectstorage/v1/objects.CreateTempURLOpts.Timestamp` [GH-1994](https://github.com/gophercloud/gophercloud/pull/1994) +* Added `compute/v2/extensions/schedulerhints.SchedulerHints.DifferentCell` [GH-2012](https://github.com/gophercloud/gophercloud/pull/2012) +* Added `loadbalancer/v2/quotas.Get` [GH-2010](https://github.com/gophercloud/gophercloud/pull/2010) +* Added `messaging/v2/queues.CreateOpts.EnableEncryptMessages` [GH-2016](https://github.com/gophercloud/gophercloud/pull/2016) +* Added `messaging/v2/queues.ListOpts.Name` [GH-2018](https://github.com/gophercloud/gophercloud/pull/2018) +* Added `messaging/v2/queues.ListOpts.WithCount` [GH-2018](https://github.com/gophercloud/gophercloud/pull/2018) +* Added `loadbalancer/v2/quotas.Update` [GH-2023](https://github.com/gophercloud/gophercloud/pull/2023) +* Added `loadbalancer/v2/loadbalancers.ListOpts.AvailabilityZone` [GH-2026](https://github.com/gophercloud/gophercloud/pull/2026) +* Added `loadbalancer/v2/loadbalancers.CreateOpts.AvailabilityZone` [GH-2026](https://github.com/gophercloud/gophercloud/pull/2026) +* Added `loadbalancer/v2/loadbalancers.LoadBalancer.AvailabilityZone` [GH-2026](https://github.com/gophercloud/gophercloud/pull/2026) +* Added `networking/v2/extensions/layer3/routers.ListL3Agents` [GH-2025](https://github.com/gophercloud/gophercloud/pull/2025) + +BUG FIXES + +* Fixed URL escaping in `objectstorage/v1/objects.CreateTempURL` [GH-1994](https://github.com/gophercloud/gophercloud/pull/1994) +* Remove unused `ServiceClient` from `compute/v2/servers.CreateOpts` [GH-2004](https://github.com/gophercloud/gophercloud/pull/2004) +* Changed `objectstorage/v1/objects.CreateOpts.DeleteAfter` from `int` to `int64` [GH-2014](https://github.com/gophercloud/gophercloud/pull/2014) +* Changed `objectstorage/v1/objects.CreateOpts.DeleteAt` from `int` to `int64` [GH-2014](https://github.com/gophercloud/gophercloud/pull/2014) +* Changed `objectstorage/v1/objects.UpdateOpts.DeleteAfter` from `int` to `int64` [GH-2014](https://github.com/gophercloud/gophercloud/pull/2014) +* Changed `objectstorage/v1/objects.UpdateOpts.DeleteAt` from `int` to `int64` [GH-2014](https://github.com/gophercloud/gophercloud/pull/2014) + + +## 0.12.0 (June 25, 2020) + +UPGRADE NOTES + +* The URL used in the `compute/v2/extensions/bootfromvolume` package has been changed from `os-volumes_boot` to `servers`. + +IMPROVEMENTS + +* The URL used in the `compute/v2/extensions/bootfromvolume` package has been changed from `os-volumes_boot` to `servers` [GH-1973](https://github.com/gophercloud/gophercloud/pull/1973) +* Modify `baremetal/v1/nodes.LogicalDisk.PhysicalDisks` type to support physical disks hints [GH-1982](https://github.com/gophercloud/gophercloud/pull/1982) +* Added `baremetalintrospection/httpbasic` which provides an HTTP Basic Auth client [GH-1986](https://github.com/gophercloud/gophercloud/pull/1986) +* Added `baremetal/httpbasic` which provides an HTTP Basic Auth client [GH-1983](https://github.com/gophercloud/gophercloud/pull/1983) +* Added `containerinfra/v1/clusters.CreateOpts.MergeLabels` [GH-1985](https://github.com/gophercloud/gophercloud/pull/1985) + +BUG FIXES + +* Changed `containerinfra/v1/clusters.Cluster.HealthStatusReason` from `string` to `map[string]interface{}` [GH-1968](https://github.com/gophercloud/gophercloud/pull/1968) +* Fixed marshalling of `blockstorage/extensions/backups.ImportBackup.Metadata` [GH-1967](https://github.com/gophercloud/gophercloud/pull/1967) +* Fixed typo of "OAUth" to "OAuth" in `identity/v3/extensions/oauth1` [GH-1969](https://github.com/gophercloud/gophercloud/pull/1969) +* Fixed goroutine leak during reauthentication [GH-1978](https://github.com/gophercloud/gophercloud/pull/1978) +* Changed `baremetalintrospection/v1/introspection.RootDiskType.Size` from `int` to `int64` [GH-1988](https://github.com/gophercloud/gophercloud/pull/1988) + +## 0.11.0 (May 14, 2020) + +UPGRADE NOTES + +* Object storage container and object names are now URL encoded [GH-1930](https://github.com/gophercloud/gophercloud/pull/1930) +* All responses now have access to the returned headers. Please report any issues this has caused [GH-1942](https://github.com/gophercloud/gophercloud/pull/1942) +* Changes have been made to the internal HTTP client to ensure response bodies are handled in a way that enables connections to be re-used more efficiently [GH-1952](https://github.com/gophercloud/gophercloud/pull/1952) + +IMPROVEMENTS + +* Added `objectstorage/v1/containers.BulkDelete` [GH-1930](https://github.com/gophercloud/gophercloud/pull/1930) +* Added `objectstorage/v1/objects.BulkDelete` [GH-1930](https://github.com/gophercloud/gophercloud/pull/1930) +* Object storage container and object names are now URL encoded [GH-1930](https://github.com/gophercloud/gophercloud/pull/1930) +* All responses now have access to the returned headers [GH-1942](https://github.com/gophercloud/gophercloud/pull/1942) +* Added `compute/v2/extensions/injectnetworkinfo.InjectNetworkInfo` [GH-1941](https://github.com/gophercloud/gophercloud/pull/1941) +* Added `compute/v2/extensions/resetnetwork.ResetNetwork` [GH-1941](https://github.com/gophercloud/gophercloud/pull/1941) +* Added `identity/v3/extensions/trusts.ListRoles` [GH-1939](https://github.com/gophercloud/gophercloud/pull/1939) +* Added `identity/v3/extensions/trusts.GetRole` [GH-1939](https://github.com/gophercloud/gophercloud/pull/1939) +* Added `identity/v3/extensions/trusts.CheckRole` [GH-1939](https://github.com/gophercloud/gophercloud/pull/1939) +* Added `identity/v3/extensions/oauth1.Create` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.CreateConsumer` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.DeleteConsumer` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.ListConsumers` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.GetConsumer` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.UpdateConsumer` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.RequestToken` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.AuthorizeToken` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.CreateAccessToken` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.GetAccessToken` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.RevokeAccessToken` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.ListAccessTokens` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.ListAccessTokenRoles` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `identity/v3/extensions/oauth1.GetAccessTokenRole` [GH-1935](https://github.com/gophercloud/gophercloud/pull/1935) +* Added `networking/v2/extensions/agents.Update` [GH-1954](https://github.com/gophercloud/gophercloud/pull/1954) +* Added `networking/v2/extensions/agents.Delete` [GH-1954](https://github.com/gophercloud/gophercloud/pull/1954) +* Added `networking/v2/extensions/agents.ScheduleDHCPNetwork` [GH-1954](https://github.com/gophercloud/gophercloud/pull/1954) +* Added `networking/v2/extensions/agents.RemoveDHCPNetwork` [GH-1954](https://github.com/gophercloud/gophercloud/pull/1954) +* Added `identity/v3/projects.CreateOpts.Extra` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951) +* Added `identity/v3/projects.CreateOpts.Options` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951) +* Added `identity/v3/projects.UpdateOpts.Extra` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951) +* Added `identity/v3/projects.UpdateOpts.Options` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951) +* Added `identity/v3/projects.Project.Extra` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951) +* Added `identity/v3/projects.Options.Options` [GH-1951](https://github.com/gophercloud/gophercloud/pull/1951) +* Added `imageservice/v2/images.Image.OpenStackImageImportMethods` [GH-1962](https://github.com/gophercloud/gophercloud/pull/1962) +* Added `imageservice/v2/images.Image.OpenStackImageStoreIDs` [GH-1962](https://github.com/gophercloud/gophercloud/pull/1962) + +BUG FIXES + +* Changed`identity/v3/extensions/trusts.Trust.RemainingUses` from `bool` to `int` [GH-1939](https://github.com/gophercloud/gophercloud/pull/1939) +* Changed `identity/v3/applicationcredentials.CreateOpts.ExpiresAt` from `string` to `*time.Time` [GH-1937](https://github.com/gophercloud/gophercloud/pull/1937) +* Fixed issue with unmarshalling/decoding slices of composed structs [GH-1964](https://github.com/gophercloud/gophercloud/pull/1964) + +## 0.10.0 (April 12, 2020) + +UPGRADE NOTES + +* The various `IDFromName` convenience functions have been moved to https://github.com/gophercloud/utils [GH-1897](https://github.com/gophercloud/gophercloud/pull/1897) +* `sharedfilesystems/v2/shares.GetExportLocations` was renamed to `sharedfilesystems/v2/shares.ListExportLocations` [GH-1932](https://github.com/gophercloud/gophercloud/pull/1932) + +IMPROVEMENTS + +* Added `blockstorage/extensions/volumeactions.SetBootable` [GH-1891](https://github.com/gophercloud/gophercloud/pull/1891) +* Added `blockstorage/extensions/backups.Export` [GH-1894](https://github.com/gophercloud/gophercloud/pull/1894) +* Added `blockstorage/extensions/backups.Import` [GH-1894](https://github.com/gophercloud/gophercloud/pull/1894) +* Added `placement/v1/resourceproviders.GetTraits` [GH-1899](https://github.com/gophercloud/gophercloud/pull/1899) +* Added the ability to authenticate with Amazon EC2 Credentials [GH-1900](https://github.com/gophercloud/gophercloud/pull/1900) +* Added ability to list Nova services by binary and host [GH-1904](https://github.com/gophercloud/gophercloud/pull/1904) +* Added `compute/v2/extensions/services.Update` [GH-1902](https://github.com/gophercloud/gophercloud/pull/1902) +* Added system scope to v3 authentication [GH-1908](https://github.com/gophercloud/gophercloud/pull/1908) +* Added `identity/v3/extensions/ec2tokens.ValidateS3Token` [GH-1906](https://github.com/gophercloud/gophercloud/pull/1906) +* Added `containerinfra/v1/clusters.Cluster.HealthStatus` [GH-1910](https://github.com/gophercloud/gophercloud/pull/1910) +* Added `containerinfra/v1/clusters.Cluster.HealthStatusReason` [GH-1910](https://github.com/gophercloud/gophercloud/pull/1910) +* Added `loadbalancer/v2/amphorae.Failover` [GH-1912](https://github.com/gophercloud/gophercloud/pull/1912) +* Added `identity/v3/extensions/ec2credentials.List` [GH-1916](https://github.com/gophercloud/gophercloud/pull/1916) +* Added `identity/v3/extensions/ec2credentials.Get` [GH-1916](https://github.com/gophercloud/gophercloud/pull/1916) +* Added `identity/v3/extensions/ec2credentials.Create` [GH-1916](https://github.com/gophercloud/gophercloud/pull/1916) +* Added `identity/v3/extensions/ec2credentials.Delete` [GH-1916](https://github.com/gophercloud/gophercloud/pull/1916) +* Added `ErrUnexpectedResponseCode.ResponseHeader` [GH-1919](https://github.com/gophercloud/gophercloud/pull/1919) +* Added support for TOTP authentication [GH-1922](https://github.com/gophercloud/gophercloud/pull/1922) +* `sharedfilesystems/v2/shares.GetExportLocations` was renamed to `sharedfilesystems/v2/shares.ListExportLocations` [GH-1932](https://github.com/gophercloud/gophercloud/pull/1932) +* Added `sharedfilesystems/v2/shares.GetExportLocation` [GH-1932](https://github.com/gophercloud/gophercloud/pull/1932) +* Added `sharedfilesystems/v2/shares.Revert` [GH-1931](https://github.com/gophercloud/gophercloud/pull/1931) +* Added `sharedfilesystems/v2/shares.ResetStatus` [GH-1931](https://github.com/gophercloud/gophercloud/pull/1931) +* Added `sharedfilesystems/v2/shares.ForceDelete` [GH-1931](https://github.com/gophercloud/gophercloud/pull/1931) +* Added `sharedfilesystems/v2/shares.Unmanage` [GH-1931](https://github.com/gophercloud/gophercloud/pull/1931) +* Added `blockstorage/v3/attachments.Create` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934) +* Added `blockstorage/v3/attachments.List` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934) +* Added `blockstorage/v3/attachments.Get` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934) +* Added `blockstorage/v3/attachments.Update` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934) +* Added `blockstorage/v3/attachments.Delete` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934) +* Added `blockstorage/v3/attachments.Complete` [GH-1934](https://github.com/gophercloud/gophercloud/pull/1934) + +BUG FIXES + +* Fixed issue with Orchestration `get_file` only being able to read JSON and YAML files [GH-1915](https://github.com/gophercloud/gophercloud/pull/1915) + +## 0.9.0 (March 10, 2020) + +UPGRADE NOTES + +* The way we implement new API result fields added by microversions has changed. Previously, we would declare a dedicated `ExtractFoo` function in a file called `microversions.go`. Now, we are declaring those fields inline of the original result struct as a pointer. [GH-1854](https://github.com/gophercloud/gophercloud/pull/1854) + +* `compute/v2/servers.CreateOpts.Networks` has changed from `[]Network` to `interface{}` in order to support creating servers that have no networks. [GH-1884](https://github.com/gophercloud/gophercloud/pull/1884) + +IMPROVEMENTS + +* Added `compute/v2/extensions/instanceactions.List` [GH-1848](https://github.com/gophercloud/gophercloud/pull/1848) +* Added `compute/v2/extensions/instanceactions.Get` [GH-1848](https://github.com/gophercloud/gophercloud/pull/1848) +* Added `networking/v2/ports.List.FixedIPs` [GH-1849](https://github.com/gophercloud/gophercloud/pull/1849) +* Added `identity/v3/extensions/trusts.List` [GH-1855](https://github.com/gophercloud/gophercloud/pull/1855) +* Added `identity/v3/extensions/trusts.Get` [GH-1855](https://github.com/gophercloud/gophercloud/pull/1855) +* Added `identity/v3/extensions/trusts.Trust.ExpiresAt` [GH-1857](https://github.com/gophercloud/gophercloud/pull/1857) +* Added `identity/v3/extensions/trusts.Trust.DeletedAt` [GH-1857](https://github.com/gophercloud/gophercloud/pull/1857) +* Added `compute/v2/extensions/instanceactions.InstanceActionDetail` [GH-1851](https://github.com/gophercloud/gophercloud/pull/1851) +* Added `compute/v2/extensions/instanceactions.Event` [GH-1851](https://github.com/gophercloud/gophercloud/pull/1851) +* Added `compute/v2/extensions/instanceactions.ListOpts` [GH-1858](https://github.com/gophercloud/gophercloud/pull/1858) +* Added `objectstorage/v1/containers.UpdateOpts.TempURLKey` [GH-1864](https://github.com/gophercloud/gophercloud/pull/1864) +* Added `objectstorage/v1/containers.UpdateOpts.TempURLKey2` [GH-1864](https://github.com/gophercloud/gophercloud/pull/1864) +* Added `placement/v1/resourceproviders.GetUsages` [GH-1862](https://github.com/gophercloud/gophercloud/pull/1862) +* Added `placement/v1/resourceproviders.GetInventories` [GH-1862](https://github.com/gophercloud/gophercloud/pull/1862) +* Added `imageservice/v2/images.ReplaceImageMinRam` [GH-1867](https://github.com/gophercloud/gophercloud/pull/1867) +* Added `objectstorage/v1/containers.UpdateOpts.TempURLKey` [GH-1865](https://github.com/gophercloud/gophercloud/pull/1865) +* Added `objectstorage/v1/containers.CreateOpts.TempURLKey2` [GH-1865](https://github.com/gophercloud/gophercloud/pull/1865) +* Added `blockstorage/extensions/volumetransfers.List` [GH-1869](https://github.com/gophercloud/gophercloud/pull/1869) +* Added `blockstorage/extensions/volumetransfers.Create` [GH-1869](https://github.com/gophercloud/gophercloud/pull/1869) +* Added `blockstorage/extensions/volumetransfers.Accept` [GH-1869](https://github.com/gophercloud/gophercloud/pull/1869) +* Added `blockstorage/extensions/volumetransfers.Get` [GH-1869](https://github.com/gophercloud/gophercloud/pull/1869) +* Added `blockstorage/extensions/volumetransfers.Delete` [GH-1869](https://github.com/gophercloud/gophercloud/pull/1869) +* Added `blockstorage/extensions/backups.RestoreFromBackup` [GH-1871](https://github.com/gophercloud/gophercloud/pull/1871) +* Added `blockstorage/v3/volumes.CreateOpts.BackupID` [GH-1871](https://github.com/gophercloud/gophercloud/pull/1871) +* Added `blockstorage/v3/volumes.Volume.BackupID` [GH-1871](https://github.com/gophercloud/gophercloud/pull/1871) +* Added `identity/v3/projects.ListOpts.Tags` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882) +* Added `identity/v3/projects.ListOpts.TagsAny` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882) +* Added `identity/v3/projects.ListOpts.NotTags` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882) +* Added `identity/v3/projects.ListOpts.NotTagsAny` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882) +* Added `identity/v3/projects.CreateOpts.Tags` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882) +* Added `identity/v3/projects.UpdateOpts.Tags` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882) +* Added `identity/v3/projects.Project.Tags` [GH-1882](https://github.com/gophercloud/gophercloud/pull/1882) +* Changed `compute/v2/servers.CreateOpts.Networks` from `[]Network` to `interface{}` to support creating servers with no networks. [GH-1884](https://github.com/gophercloud/gophercloud/pull/1884) + + +BUG FIXES + +* Added support for `int64` headers, which were previously being silently dropped [GH-1860](https://github.com/gophercloud/gophercloud/pull/1860) +* Allow image properties with empty values [GH-1875](https://github.com/gophercloud/gophercloud/pull/1875) +* Fixed `compute/v2/extensions/extendedserverattributes.ServerAttributesExt.Userdata` JSON tag [GH-1881](https://github.com/gophercloud/gophercloud/pull/1881) + +## 0.8.0 (February 8, 2020) + +UPGRADE NOTES + +* The behavior of `keymanager/v1/acls.SetOpts` has changed. Instead of a struct, it is now `[]SetOpt`. See [GH-1816](https://github.com/gophercloud/gophercloud/pull/1816) for implementation details. + +IMPROVEMENTS + +* The result of `containerinfra/v1/clusters.Resize` now returns only the UUID when calling `Extract`. This is a backwards-breaking change from the previous struct that was returned [GH-1649](https://github.com/gophercloud/gophercloud/pull/1649) +* Added `compute/v2/extensions/shelveunshelve.Shelve` [GH-1799](https://github.com/gophercloud/gophercloud/pull/1799) +* Added `compute/v2/extensions/shelveunshelve.ShelveOffload` [GH-1799](https://github.com/gophercloud/gophercloud/pull/1799) +* Added `compute/v2/extensions/shelveunshelve.Unshelve` [GH-1799](https://github.com/gophercloud/gophercloud/pull/1799) +* Added `containerinfra/v1/nodegroups.Get` [GH-1774](https://github.com/gophercloud/gophercloud/pull/1774) +* Added `containerinfra/v1/nodegroups.List` [GH-1774](https://github.com/gophercloud/gophercloud/pull/1774) +* Added `orchestration/v1/resourcetypes.List` [GH-1806](https://github.com/gophercloud/gophercloud/pull/1806) +* Added `orchestration/v1/resourcetypes.GetSchema` [GH-1806](https://github.com/gophercloud/gophercloud/pull/1806) +* Added `orchestration/v1/resourcetypes.GenerateTemplate` [GH-1806](https://github.com/gophercloud/gophercloud/pull/1806) +* Added `keymanager/v1/acls.SetOpt` and changed `keymanager/v1/acls.SetOpts` to `[]SetOpt` [GH-1816](https://github.com/gophercloud/gophercloud/pull/1816) +* Added `blockstorage/apiversions.List` [GH-458](https://github.com/gophercloud/gophercloud/pull/458) +* Added `blockstorage/apiversions.Get` [GH-458](https://github.com/gophercloud/gophercloud/pull/458) +* Added `StatusCodeError` interface and `GetStatusCode` convenience method [GH-1820](https://github.com/gophercloud/gophercloud/pull/1820) +* Added pagination support to `compute/v2/extensions/usage.SingleTenant` [GH-1819](https://github.com/gophercloud/gophercloud/pull/1819) +* Added pagination support to `compute/v2/extensions/usage.AllTenants` [GH-1819](https://github.com/gophercloud/gophercloud/pull/1819) +* Added `placement/v1/resourceproviders.List` [GH-1815](https://github.com/gophercloud/gophercloud/pull/1815) +* Allow `CreateMemberOptsBuilder` to be passed in `loadbalancer/v2/pools.Create` [GH-1822](https://github.com/gophercloud/gophercloud/pull/1822) +* Added `Backup` to `loadbalancer/v2/pools.CreateMemberOpts` [GH-1824](https://github.com/gophercloud/gophercloud/pull/1824) +* Added `MonitorAddress` to `loadbalancer/v2/pools.CreateMemberOpts` [GH-1824](https://github.com/gophercloud/gophercloud/pull/1824) +* Added `MonitorPort` to `loadbalancer/v2/pools.CreateMemberOpts` [GH-1824](https://github.com/gophercloud/gophercloud/pull/1824) +* Changed `Impersonation` to a non-required field in `identity/v3/extensions/trusts.CreateOpts` [GH-1818](https://github.com/gophercloud/gophercloud/pull/1818) +* Added `InsertHeaders` to `loadbalancer/v2/listeners.UpdateOpts` [GH-1835](https://github.com/gophercloud/gophercloud/pull/1835) +* Added `NUMATopology` to `baremetalintrospection/v1/introspection.Data` [GH-1842](https://github.com/gophercloud/gophercloud/pull/1842) +* Added `placement/v1/resourceproviders.Create` [GH-1841](https://github.com/gophercloud/gophercloud/pull/1841) +* Added `blockstorage/extensions/volumeactions.UploadImageOpts.Visibility` [GH-1873](https://github.com/gophercloud/gophercloud/pull/1873) +* Added `blockstorage/extensions/volumeactions.UploadImageOpts.Protected` [GH-1873](https://github.com/gophercloud/gophercloud/pull/1873) +* Added `blockstorage/extensions/volumeactions.VolumeImage.Visibility` [GH-1873](https://github.com/gophercloud/gophercloud/pull/1873) +* Added `blockstorage/extensions/volumeactions.VolumeImage.Protected` [GH-1873](https://github.com/gophercloud/gophercloud/pull/1873) + +BUG FIXES + +* Changed `sort_key` to `sort_keys` in ` workflow/v2/crontriggers.ListOpts` [GH-1809](https://github.com/gophercloud/gophercloud/pull/1809) +* Allow `blockstorage/extensions/schedulerstats.Capabilities.MaxOverSubscriptionRatio` to accept both string and int/float responses [GH-1817](https://github.com/gophercloud/gophercloud/pull/1817) +* Fixed bug in `NewLoadBalancerV2` for situations when the LBaaS service was advertised without a `/v2.0` endpoint [GH-1829](https://github.com/gophercloud/gophercloud/pull/1829) +* Fixed JSON tags in `baremetal/v1/ports.UpdateOperation` [GH-1840](https://github.com/gophercloud/gophercloud/pull/1840) +* Fixed JSON tags in `networking/v2/extensions/lbaas/vips.commonResult.Extract()` [GH-1840](https://github.com/gophercloud/gophercloud/pull/1840) + +## 0.7.0 (December 3, 2019) + +IMPROVEMENTS + +* Allow a token to be used directly for authentication instead of generating a new token based on a given token [GH-1752](https://github.com/gophercloud/gophercloud/pull/1752) +* Moved `tags.ServerTagsExt` to servers.TagsExt` [GH-1760](https://github.com/gophercloud/gophercloud/pull/1760) +* Added `tags`, `tags-any`, `not-tags`, and `not-tags-any` to `compute/v2/servers.ListOpts` [GH-1759](https://github.com/gophercloud/gophercloud/pull/1759) +* Added `AccessRule` to `identity/v3/applicationcredentials` [GH-1758](https://github.com/gophercloud/gophercloud/pull/1758) +* Gophercloud no longer returns an error when multiple endpoints are found. Instead, it will choose the first endpoint and discard the others [GH-1766](https://github.com/gophercloud/gophercloud/pull/1766) +* Added `networking/v2/extensions/fwaas_v2/rules.Create` [GH-1768](https://github.com/gophercloud/gophercloud/pull/1768) +* Added `networking/v2/extensions/fwaas_v2/rules.Delete` [GH-1771](https://github.com/gophercloud/gophercloud/pull/1771) +* Added `loadbalancer/v2/providers.List` [GH-1765](https://github.com/gophercloud/gophercloud/pull/1765) +* Added `networking/v2/extensions/fwaas_v2/rules.Get` [GH-1772](https://github.com/gophercloud/gophercloud/pull/1772) +* Added `networking/v2/extensions/fwaas_v2/rules.Update` [GH-1776](https://github.com/gophercloud/gophercloud/pull/1776) +* Added `networking/v2/extensions/fwaas_v2/rules.List` [GH-1783](https://github.com/gophercloud/gophercloud/pull/1783) +* Added `MaxRetriesDown` into `loadbalancer/v2/monitors.CreateOpts` [GH-1785](https://github.com/gophercloud/gophercloud/pull/1785) +* Added `MaxRetriesDown` into `loadbalancer/v2/monitors.UpdateOpts` [GH-1786](https://github.com/gophercloud/gophercloud/pull/1786) +* Added `MaxRetriesDown` into `loadbalancer/v2/monitors.Monitor` [GH-1787](https://github.com/gophercloud/gophercloud/pull/1787) +* Added `MaxRetriesDown` into `loadbalancer/v2/monitors.ListOpts` [GH-1788](https://github.com/gophercloud/gophercloud/pull/1788) +* Updated `go.mod` dependencies, specifically to account for CVE-2019-11840 with `golang.org/x/crypto` [GH-1793](https://github.com/gophercloud/gophercloud/pull/1788) + +## 0.6.0 (October 17, 2019) + +UPGRADE NOTES + +* The way reauthentication works has been refactored. This should not cause a problem, but please report bugs if it does. See [GH-1746](https://github.com/gophercloud/gophercloud/pull/1746) for more information. + +IMPROVEMENTS + +* Added `networking/v2/extensions/quotas.Get` [GH-1742](https://github.com/gophercloud/gophercloud/pull/1742) +* Added `networking/v2/extensions/quotas.Update` [GH-1747](https://github.com/gophercloud/gophercloud/pull/1747) +* Refactored the reauthentication implementation to use goroutines and added a check to prevent an infinite loop in certain situations. [GH-1746](https://github.com/gophercloud/gophercloud/pull/1746) + +BUG FIXES + +* Changed `Flavor` to `FlavorID` in `loadbalancer/v2/loadbalancers` [GH-1744](https://github.com/gophercloud/gophercloud/pull/1744) +* Changed `Flavor` to `FlavorID` in `networking/v2/extensions/lbaas_v2/loadbalancers` [GH-1744](https://github.com/gophercloud/gophercloud/pull/1744) +* The `go-yaml` dependency was updated to `v2.2.4` to fix possible DDOS vulnerabilities [GH-1751](https://github.com/gophercloud/gophercloud/pull/1751) + +## 0.5.0 (October 13, 2019) + +IMPROVEMENTS + +* Added `VolumeType` to `compute/v2/extensions/bootfromvolume.BlockDevice`[GH-1690](https://github.com/gophercloud/gophercloud/pull/1690) +* Added `networking/v2/extensions/layer3/portforwarding.List` [GH-1688](https://github.com/gophercloud/gophercloud/pull/1688) +* Added `networking/v2/extensions/layer3/portforwarding.Get` [GH-1698](https://github.com/gophercloud/gophercloud/pull/1696) +* Added `compute/v2/extensions/tags.ReplaceAll` [GH-1696](https://github.com/gophercloud/gophercloud/pull/1696) +* Added `compute/v2/extensions/tags.Add` [GH-1696](https://github.com/gophercloud/gophercloud/pull/1696) +* Added `networking/v2/extensions/layer3/portforwarding.Update` [GH-1703](https://github.com/gophercloud/gophercloud/pull/1703) +* Added `ExtractDomain` method to token results in `identity/v3/tokens` [GH-1712](https://github.com/gophercloud/gophercloud/pull/1712) +* Added `AllowedCIDRs` to `loadbalancer/v2/listeners.CreateOpts` [GH-1710](https://github.com/gophercloud/gophercloud/pull/1710) +* Added `AllowedCIDRs` to `loadbalancer/v2/listeners.UpdateOpts` [GH-1710](https://github.com/gophercloud/gophercloud/pull/1710) +* Added `AllowedCIDRs` to `loadbalancer/v2/listeners.Listener` [GH-1710](https://github.com/gophercloud/gophercloud/pull/1710) +* Added `compute/v2/extensions/tags.Add` [GH-1695](https://github.com/gophercloud/gophercloud/pull/1695) +* Added `compute/v2/extensions/tags.ReplaceAll` [GH-1694](https://github.com/gophercloud/gophercloud/pull/1694) +* Added `compute/v2/extensions/tags.Delete` [GH-1699](https://github.com/gophercloud/gophercloud/pull/1699) +* Added `compute/v2/extensions/tags.DeleteAll` [GH-1700](https://github.com/gophercloud/gophercloud/pull/1700) +* Added `ImageStatusImporting` as an image status [GH-1725](https://github.com/gophercloud/gophercloud/pull/1725) +* Added `ByPath` to `baremetalintrospection/v1/introspection.RootDiskType` [GH-1730](https://github.com/gophercloud/gophercloud/pull/1730) +* Added `AttachedVolumes` to `compute/v2/servers.Server` [GH-1732](https://github.com/gophercloud/gophercloud/pull/1732) +* Enable unmarshaling server tags to a `compute/v2/servers.Server` struct [GH-1734] +* Allow setting an empty members list in `loadbalancer/v2/pools.BatchUpdateMembers` [GH-1736](https://github.com/gophercloud/gophercloud/pull/1736) +* Allow unsetting members' subnet ID and name in `loadbalancer/v2/pools.BatchUpdateMemberOpts` [GH-1738](https://github.com/gophercloud/gophercloud/pull/1738) + +BUG FIXES + +* Changed struct type for options in `networking/v2/extensions/lbaas_v2/listeners` to `UpdateOptsBuilder` interface instead of specific UpdateOpts type [GH-1705](https://github.com/gophercloud/gophercloud/pull/1705) +* Changed struct type for options in `networking/v2/extensions/lbaas_v2/loadbalancers` to `UpdateOptsBuilder` interface instead of specific UpdateOpts type [GH-1706](https://github.com/gophercloud/gophercloud/pull/1706) +* Fixed issue with `blockstorage/v1/volumes.Create` where the response was expected to be 202 [GH-1720](https://github.com/gophercloud/gophercloud/pull/1720) +* Changed `DefaultTlsContainerRef` from `string` to `*string` in `loadbalancer/v2/listeners.UpdateOpts` to allow the value to be removed during update. [GH-1723](https://github.com/gophercloud/gophercloud/pull/1723) +* Changed `SniContainerRefs` from `[]string{}` to `*[]string{}` in `loadbalancer/v2/listeners.UpdateOpts` to allow the value to be removed during update. [GH-1723](https://github.com/gophercloud/gophercloud/pull/1723) +* Changed `DefaultTlsContainerRef` from `string` to `*string` in `networking/v2/extensions/lbaas_v2/listeners.UpdateOpts` to allow the value to be removed during update. [GH-1723](https://github.com/gophercloud/gophercloud/pull/1723) +* Changed `SniContainerRefs` from `[]string{}` to `*[]string{}` in `networking/v2/extensions/lbaas_v2/listeners.UpdateOpts` to allow the value to be removed during update. [GH-1723](https://github.com/gophercloud/gophercloud/pull/1723) + + +## 0.4.0 (September 3, 2019) + +IMPROVEMENTS + +* Added `blockstorage/extensions/quotasets.results.QuotaSet.Groups` [GH-1668](https://github.com/gophercloud/gophercloud/pull/1668) +* Added `blockstorage/extensions/quotasets.results.QuotaUsageSet.Groups` [GH-1668](https://github.com/gophercloud/gophercloud/pull/1668) +* Added `containerinfra/v1/clusters.CreateOpts.FixedNetwork` [GH-1674](https://github.com/gophercloud/gophercloud/pull/1674) +* Added `containerinfra/v1/clusters.CreateOpts.FixedSubnet` [GH-1676](https://github.com/gophercloud/gophercloud/pull/1676) +* Added `containerinfra/v1/clusters.CreateOpts.FloatingIPEnabled` [GH-1677](https://github.com/gophercloud/gophercloud/pull/1677) +* Added `CreatedAt` and `UpdatedAt` to `loadbalancers/v2/loadbalancers.LoadBalancer` [GH-1681](https://github.com/gophercloud/gophercloud/pull/1681) +* Added `networking/v2/extensions/layer3/portforwarding.Create` [GH-1651](https://github.com/gophercloud/gophercloud/pull/1651) +* Added `networking/v2/extensions/agents.ListDHCPNetworks` [GH-1686](https://github.com/gophercloud/gophercloud/pull/1686) +* Added `networking/v2/extensions/layer3/portforwarding.Delete` [GH-1652](https://github.com/gophercloud/gophercloud/pull/1652) +* Added `compute/v2/extensions/tags.List` [GH-1679](https://github.com/gophercloud/gophercloud/pull/1679) +* Added `compute/v2/extensions/tags.Check` [GH-1679](https://github.com/gophercloud/gophercloud/pull/1679) + +BUG FIXES + +* Changed `identity/v3/endpoints.ListOpts.RegionID` from `int` to `string` [GH-1664](https://github.com/gophercloud/gophercloud/pull/1664) +* Fixed issue where older time formats in some networking APIs/resources were unable to be parsed [GH-1671](https://github.com/gophercloud/gophercloud/pull/1664) +* Changed `SATA`, `SCSI`, and `SAS` types to `InterfaceType` in `baremetal/v1/nodes` [GH-1683] + +## 0.3.0 (July 31, 2019) + +IMPROVEMENTS + +* Added `baremetal/apiversions.List` [GH-1577](https://github.com/gophercloud/gophercloud/pull/1577) +* Added `baremetal/apiversions.Get` [GH-1577](https://github.com/gophercloud/gophercloud/pull/1577) +* Added `compute/v2/extensions/servergroups.CreateOpts.Policy` [GH-1636](https://github.com/gophercloud/gophercloud/pull/1636) +* Added `identity/v3/extensions/trusts.Create` [GH-1644](https://github.com/gophercloud/gophercloud/pull/1644) +* Added `identity/v3/extensions/trusts.Delete` [GH-1644](https://github.com/gophercloud/gophercloud/pull/1644) +* Added `CreatedAt` and `UpdatedAt` to `networking/v2/extensions/layer3/floatingips.FloatingIP` [GH-1647](https://github.com/gophercloud/gophercloud/issues/1646) +* Added `CreatedAt` and `UpdatedAt` to `networking/v2/extensions/security/groups.SecGroup` [GH-1654](https://github.com/gophercloud/gophercloud/issues/1654) +* Added `CreatedAt` and `UpdatedAt` to `networking/v2/networks.Network` [GH-1657](https://github.com/gophercloud/gophercloud/issues/1657) +* Added `keymanager/v1/containers.CreateSecretRef` [GH-1659](https://github.com/gophercloud/gophercloud/issues/1659) +* Added `keymanager/v1/containers.DeleteSecretRef` [GH-1659](https://github.com/gophercloud/gophercloud/issues/1659) +* Added `sharedfilesystems/v2/shares.GetMetadata` [GH-1656](https://github.com/gophercloud/gophercloud/issues/1656) +* Added `sharedfilesystems/v2/shares.GetMetadatum` [GH-1656](https://github.com/gophercloud/gophercloud/issues/1656) +* Added `sharedfilesystems/v2/shares.SetMetadata` [GH-1656](https://github.com/gophercloud/gophercloud/issues/1656) +* Added `sharedfilesystems/v2/shares.UpdateMetadata` [GH-1656](https://github.com/gophercloud/gophercloud/issues/1656) +* Added `sharedfilesystems/v2/shares.DeleteMetadatum` [GH-1656](https://github.com/gophercloud/gophercloud/issues/1656) +* Added `sharedfilesystems/v2/sharetypes.IDFromName` [GH-1662](https://github.com/gophercloud/gophercloud/issues/1662) + + + +BUG FIXES + +* Changed `baremetal/v1/nodes.CleanStep.Args` from `map[string]string` to `map[string]interface{}` [GH-1638](https://github.com/gophercloud/gophercloud/pull/1638) +* Removed `URLPath` and `ExpectedCodes` from `loadbalancer/v2/monitors.ToMonitorCreateMap` since Octavia now provides default values when these fields are not specified [GH-1640](https://github.com/gophercloud/gophercloud/pull/1540) + + +## 0.2.0 (June 17, 2019) + +IMPROVEMENTS + +* Added `networking/v2/extensions/qos/rules.ListBandwidthLimitRules` [GH-1584](https://github.com/gophercloud/gophercloud/pull/1584) +* Added `networking/v2/extensions/qos/rules.GetBandwidthLimitRule` [GH-1584](https://github.com/gophercloud/gophercloud/pull/1584) +* Added `networking/v2/extensions/qos/rules.CreateBandwidthLimitRule` [GH-1584](https://github.com/gophercloud/gophercloud/pull/1584) +* Added `networking/v2/extensions/qos/rules.UpdateBandwidthLimitRule` [GH-1589](https://github.com/gophercloud/gophercloud/pull/1589) +* Added `networking/v2/extensions/qos/rules.DeleteBandwidthLimitRule` [GH-1590](https://github.com/gophercloud/gophercloud/pull/1590) +* Added `networking/v2/extensions/qos/policies.List` [GH-1591](https://github.com/gophercloud/gophercloud/pull/1591) +* Added `networking/v2/extensions/qos/policies.Get` [GH-1593](https://github.com/gophercloud/gophercloud/pull/1593) +* Added `networking/v2/extensions/qos/rules.ListDSCPMarkingRules` [GH-1594](https://github.com/gophercloud/gophercloud/pull/1594) +* Added `networking/v2/extensions/qos/policies.Create` [GH-1595](https://github.com/gophercloud/gophercloud/pull/1595) +* Added `compute/v2/extensions/diagnostics.Get` [GH-1592](https://github.com/gophercloud/gophercloud/pull/1592) +* Added `networking/v2/extensions/qos/policies.Update` [GH-1603](https://github.com/gophercloud/gophercloud/pull/1603) +* Added `networking/v2/extensions/qos/policies.Delete` [GH-1603](https://github.com/gophercloud/gophercloud/pull/1603) +* Added `networking/v2/extensions/qos/rules.CreateDSCPMarkingRule` [GH-1605](https://github.com/gophercloud/gophercloud/pull/1605) +* Added `networking/v2/extensions/qos/rules.UpdateDSCPMarkingRule` [GH-1605](https://github.com/gophercloud/gophercloud/pull/1605) +* Added `networking/v2/extensions/qos/rules.GetDSCPMarkingRule` [GH-1609](https://github.com/gophercloud/gophercloud/pull/1609) +* Added `networking/v2/extensions/qos/rules.DeleteDSCPMarkingRule` [GH-1609](https://github.com/gophercloud/gophercloud/pull/1609) +* Added `networking/v2/extensions/qos/rules.ListMinimumBandwidthRules` [GH-1615](https://github.com/gophercloud/gophercloud/pull/1615) +* Added `networking/v2/extensions/qos/rules.GetMinimumBandwidthRule` [GH-1615](https://github.com/gophercloud/gophercloud/pull/1615) +* Added `networking/v2/extensions/qos/rules.CreateMinimumBandwidthRule` [GH-1615](https://github.com/gophercloud/gophercloud/pull/1615) +* Added `Hostname` to `baremetalintrospection/v1/introspection.Data` [GH-1627](https://github.com/gophercloud/gophercloud/pull/1627) +* Added `networking/v2/extensions/qos/rules.UpdateMinimumBandwidthRule` [GH-1624](https://github.com/gophercloud/gophercloud/pull/1624) +* Added `networking/v2/extensions/qos/rules.DeleteMinimumBandwidthRule` [GH-1624](https://github.com/gophercloud/gophercloud/pull/1624) +* Added `networking/v2/extensions/qos/ruletypes.GetRuleType` [GH-1625](https://github.com/gophercloud/gophercloud/pull/1625) +* Added `Extra` to `baremetalintrospection/v1/introspection.Data` [GH-1611](https://github.com/gophercloud/gophercloud/pull/1611) +* Added `blockstorage/extensions/volumeactions.SetImageMetadata` [GH-1621](https://github.com/gophercloud/gophercloud/pull/1621) + +BUG FIXES + +* Updated `networking/v2/extensions/qos/rules.UpdateBandwidthLimitRule` to use return code 200 [GH-1606](https://github.com/gophercloud/gophercloud/pull/1606) +* Fixed bug in `compute/v2/extensions/schedulerhints.SchedulerHints.Query` where contents will now be marshalled to a string [GH-1620](https://github.com/gophercloud/gophercloud/pull/1620) + +## 0.1.0 (May 27, 2019) + +Initial tagged release. diff --git a/vendor/github.com/gophercloud/gophercloud/LICENSE b/vendor/github.com/gophercloud/gophercloud/LICENSE new file mode 100644 index 00000000000..c3f4f2f7c9b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/LICENSE @@ -0,0 +1,192 @@ +Copyright 2012-2013 Rackspace, Inc. +Copyright Gophercloud 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. + +------ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/github.com/gophercloud/gophercloud/README.md b/vendor/github.com/gophercloud/gophercloud/README.md new file mode 100644 index 00000000000..4e6e57dadb2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/README.md @@ -0,0 +1,172 @@ +# Gophercloud: an OpenStack SDK for Go +[![Coverage Status](https://coveralls.io/repos/github/gophercloud/gophercloud/badge.svg?branch=v1)](https://coveralls.io/github/gophercloud/gophercloud?branch=v1) + +Gophercloud is an OpenStack Go SDK. + +## Useful links + +* [Reference documentation](http://godoc.org/github.com/gophercloud/gophercloud) +* [Effective Go](https://golang.org/doc/effective_go.html) + +## How to install + +Reference a Gophercloud package in your code: + +```go +import "github.com/gophercloud/gophercloud" +``` + +Then update your `go.mod`: + +```shell +go mod tidy +``` + +## Getting started + +### Credentials + +Because you'll be hitting an API, you will need to retrieve your OpenStack +credentials and either store them in a `clouds.yaml` file, as environment +variables, or in your local Go files. The first method is recommended because +it decouples credential information from source code, allowing you to push the +latter to your version control system without any security risk. + +You will need to retrieve the following: + +* A valid Keystone identity URL +* Credentials. These can be a username/password combo, a set of Application + Credentials, a pre-generated token, or any other supported authentication + mechanism. + +For users who have the OpenStack dashboard installed, there's a shortcut. If +you visit the `project/api_access` path in Horizon and click on the +"Download OpenStack RC File" button at the top right hand corner, you can +download either a `clouds.yaml` file or an `openrc` bash file that exports all +of your access details to environment variables. To use the `clouds.yaml` file, +place it at `~/.config/openstack/clouds.yaml`. To use the `openrc` file, run +`source openrc` and you will be prompted for your password. + +### Authentication + +Once you have access to your credentials, you can begin plugging them into +Gophercloud. The next step is authentication, which is handled by a base +"Provider" struct. There are number of ways to construct such a struct. + +**With `gophercloud/utils`** + +The [github.com/gophercloud/utils](https://github.com/gophercloud/utils) +library provides the `clientconfig` package to simplify authentication. It +provides additional functionality, such as the ability to read `clouds.yaml` +files. To generate a "Provider" struct using the `clientconfig` package: + +```go +import ( + "github.com/gophercloud/utils/openstack/clientconfig" +) + +// You can also skip configuring this and instead set 'OS_CLOUD' in your +// environment +opts := new(clientconfig.ClientOpts) +opts.Cloud = "devstack-admin" + +provider, err := clientconfig.AuthenticatedClient(opts) +``` + +A provider client is a top-level client that all of your OpenStack service +clients derive from. The provider contains all of the authentication details +that allow your Go code to access the API - such as the base URL and token ID. + +Once we have a base Provider, we inject it as a dependency into each OpenStack +service. For example, in order to work with the Compute API, we need a Compute +service client. This can be created like so: + +```go +client, err := clientconfig.NewServiceClient("compute", opts) +``` + +**Without `gophercloud/utils`** + +> *Note* +> gophercloud doesn't provide support for `clouds.yaml` file so you need to +> implement this functionality yourself if you don't wish to use +> `gophercloud/utils`. + +You can also generate a "Provider" struct without using the `clientconfig` +package from `gophercloud/utils`. To do this, you can either pass in your +credentials explicitly or tell Gophercloud to use environment variables: + +```go +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" +) + +// Option 1: Pass in the values yourself +opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://openstack.example.com:5000/v2.0", + Username: "{username}", + Password: "{password}", +} + +// Option 2: Use a utility function to retrieve all your environment variables +opts, err := openstack.AuthOptionsFromEnv() +``` + +Once you have the `opts` variable, you can pass it in and get back a +`ProviderClient` struct: + +```go +provider, err := openstack.AuthenticatedClient(opts) +``` + +As above, you can then use this provider client to generate a service client +for a particular OpenStack service: + +```go +client, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), +}) +``` + +### Provision a server + +We can use the Compute service client generated above for any Compute API +operation we want. In our case, we want to provision a new server. To do this, +we invoke the `Create` method and pass in the flavor ID (hardware +specification) and image ID (operating system) we're interested in: + +```go +import "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" + +server, err := servers.Create(client, servers.CreateOpts{ + Name: "My new server!", + FlavorRef: "flavor_id", + ImageRef: "image_id", +}).Extract() +``` + +The above code sample creates a new server with the parameters, and embodies the +new resource in the `server` variable (a +[`servers.Server`](http://godoc.org/github.com/gophercloud/gophercloud) struct). + +## Advanced Usage + +Have a look at the [FAQ](./docs/FAQ.md) for some tips on customizing the way Gophercloud works. + +## Backwards-Compatibility Guarantees + +Gophercloud versioning follows [semver](https://semver.org/spec/v2.0.0.html). + +Before `v1.0.0`, there were no guarantees. Starting with v1, there will be no breaking changes within a major release. + +See the [Release instructions](./RELEASE.md). + +## Contributing + +See the [contributing guide](./.github/CONTRIBUTING.md). + +## Help and feedback + +If you're struggling with something or have spotted a potential bug, feel free +to submit an issue to our [bug tracker](https://github.com/gophercloud/gophercloud/issues). diff --git a/vendor/github.com/gophercloud/gophercloud/RELEASE.md b/vendor/github.com/gophercloud/gophercloud/RELEASE.md new file mode 100644 index 00000000000..6490ed88778 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/RELEASE.md @@ -0,0 +1,79 @@ +# Gophercloud release + +## Contributions + +### The semver label + +Gophercloud follows [semver](https://semver.org/). + +Each Pull request must have a label indicating its impact on the API: +* `semver:patch` for changes that don't impact the API +* `semver:minor` for changes that impact the API in a backwards-compatible fashion +* `semver:major` for changes that introduce a breaking change in the API + +Automation prevents merges if the label is not present. + +### Metadata + +The release notes for a given release are generated based on the PR title: make +sure that the PR title is descriptive. + +## Release of a new version + +Requirements: +* [`gh`](https://github.com/cli/cli) +* [`jq`](https://stedolan.github.io/jq/) + +### Step 1: Collect all PRs since the last release + +Supposing that the base release is `v1.2.0`: + +``` +for commit_sha in $(git log --pretty=format:"%h" v1.2.0..HEAD); do + gh pr list --search "$commit_sha" --state merged --json number,title,labels,url +done | jq '.[]' | jq --slurp 'unique_by(.number)' > prs.json +``` + +This JSON file will be useful later. + +### Step 2: Determine the version + +In order to determine the version of the next release, we first check that no incompatible change is detected in the code that has been merged since the last release. This step can be automated with the `gorelease` tool: + +```shell +gorelease | grep -B2 -A0 '^## incompatible changes' +``` + +If the tool detects incompatible changes outside a `testing` package, then the bump is major. + +Next, we check all PRs merged since the last release using the file `prs.json` that we generated above. + +* Find PRs labeled with `semver:major`: `jq 'map(select(contains({labels: [{name: "semver:major"}]}) ))' prs.json` +* Find PRs labeled with `semver:minor`: `jq 'map(select(contains({labels: [{name: "semver:minor"}]}) ))' prs.json` + +The highest semver descriptor determines the release bump. + +### Step 3: Release notes and version string + +Once all PRs have a sensible title, generate the release notes: + +```shell +jq -r '.[] | "* [GH-\(.number)](\(.url)) \(.title)"' prs.json +``` + +Add that to the top of `CHANGELOG.md`. Also add any information that could be useful to consumers willing to upgrade. + +**Set the new version string in the `DefaultUserAgent` constant in `provider_client.go`.** + +Create a PR with these two changes. The new PR should be labeled with the semver label corresponding to the type of bump. + +### Step 3: Git tag and Github release + +The Go mod system relies on Git tags. In order to simulate a review mechanism, we rely on Github to create the tag through the Release mechanism. + +* [Prepare a new release](https://github.com/gophercloud/gophercloud/releases/new) +* Let Github generate the release notes by clicking on Generate release notes +* Click on **Save draft** +* Ask another Gophercloud maintainer to review and publish the release + +_Note: never change a release or force-push a tag. Tags are almost immediately picked up by the Go proxy and changing the commit it points to will be detected as tampering._ diff --git a/vendor/github.com/gophercloud/gophercloud/auth_options.go b/vendor/github.com/gophercloud/gophercloud/auth_options.go new file mode 100644 index 00000000000..335ce87957d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/auth_options.go @@ -0,0 +1,514 @@ +package gophercloud + +/* +AuthOptions stores information needed to authenticate to an OpenStack Cloud. +You can populate one manually, or use a provider's AuthOptionsFromEnv() function +to read relevant information from the standard environment variables. Pass one +to a provider's AuthenticatedClient function to authenticate and obtain a +ProviderClient representing an active session on that provider. + +Its fields are the union of those recognized by each identity implementation and +provider. + +An example of manually providing authentication information: + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://openstack.example.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", + } + + provider, err := openstack.AuthenticatedClient(opts) + +An example of using AuthOptionsFromEnv(), where the environment variables can +be read from a file, such as a standard openrc file: + + opts, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(opts) +*/ +type AuthOptions struct { + // IdentityEndpoint specifies the HTTP endpoint that is required to work with + // the Identity API of the appropriate version. While it's ultimately needed by + // all of the identity services, it will often be populated by a provider-level + // function. + // + // The IdentityEndpoint is typically referred to as the "auth_url" or + // "OS_AUTH_URL" in the information provided by the cloud operator. + IdentityEndpoint string `json:"-"` + + // Username is required if using Identity V2 API. Consult with your provider's + // control panel to discover your account's username. In Identity V3, either + // UserID or a combination of Username and DomainID or DomainName are needed. + Username string `json:"username,omitempty"` + UserID string `json:"-"` + + Password string `json:"password,omitempty"` + + // Passcode is used in TOTP authentication method + Passcode string `json:"passcode,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"-"` + DomainName string `json:"name,omitempty"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // The same fields are known as project_id and project_name in the Identity + // V3 API, but are collected as TenantID and TenantName here in both cases. + // Some providers allow you to specify a TenantName instead of the TenantId. + // Some require both. Your provider's authentication policies will determine + // how these fields influence authentication. + // If DomainID or DomainName are provided, they will also apply to TenantName. + // It is not currently possible to authenticate with Username and a Domain + // and scope to a Project in a different Domain by using TenantName. To + // accomplish that, the ProjectID will need to be provided as the TenantID + // option. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // AllowReauth should be set to true if you grant permission for Gophercloud to + // cache your credentials in memory, and to allow Gophercloud to attempt to + // re-authenticate automatically if/when your token expires. If you set it to + // false, it will not cache these settings, but re-authentication will not be + // possible. This setting defaults to false. + // + // NOTE: The reauth function will try to re-authenticate endlessly if left + // unchecked. The way to limit the number of attempts is to provide a custom + // HTTP client to the provider client and provide a transport that implements + // the RoundTripper interface and stores the number of failed retries. For an + // example of this, see here: + // https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311 + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + // Scope determines the scoping of the authentication request. + Scope *AuthScope `json:"-"` + + // Authentication through Application Credentials requires supplying name, project and secret + // For project we can use TenantID + ApplicationCredentialID string `json:"-"` + ApplicationCredentialName string `json:"-"` + ApplicationCredentialSecret string `json:"-"` +} + +// AuthScope allows a created token to be limited to a specific domain or project. +type AuthScope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string + System bool +} + +// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v2 tokens package +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { + // Populate the request map. + authMap := make(map[string]interface{}) + + if opts.Username != "" { + if opts.Password != "" { + authMap["passwordCredentials"] = map[string]interface{}{ + "username": opts.Username, + "password": opts.Password, + } + } else { + return nil, ErrMissingInput{Argument: "Password"} + } + } else if opts.TokenID != "" { + authMap["token"] = map[string]interface{}{ + "id": opts.TokenID, + } + } else { + return nil, ErrMissingInput{Argument: "Username"} + } + + if opts.TenantID != "" { + authMap["tenantId"] = opts.TenantID + } + if opts.TenantName != "" { + authMap["tenantName"] = opts.TenantName + } + + return map[string]interface{}{"auth": authMap}, nil +} + +// ToTokenV3CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v3 tokens package +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + type domainReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + } + + type projectReq struct { + Domain *domainReq `json:"domain,omitempty"` + Name *string `json:"name,omitempty"` + ID *string `json:"id,omitempty"` + } + + type userReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Password *string `json:"password,omitempty"` + Passcode *string `json:"passcode,omitempty"` + Domain *domainReq `json:"domain,omitempty"` + } + + type passwordReq struct { + User userReq `json:"user"` + } + + type tokenReq struct { + ID string `json:"id"` + } + + type applicationCredentialReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + User *userReq `json:"user,omitempty"` + Secret *string `json:"secret,omitempty"` + } + + type totpReq struct { + User *userReq `json:"user,omitempty"` + } + + type identityReq struct { + Methods []string `json:"methods"` + Password *passwordReq `json:"password,omitempty"` + Token *tokenReq `json:"token,omitempty"` + ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"` + TOTP *totpReq `json:"totp,omitempty"` + } + + type authReq struct { + Identity identityReq `json:"identity"` + } + + type request struct { + Auth authReq `json:"auth"` + } + + // Populate the request structure based on the provided arguments. Create and return an error + // if insufficient or incompatible information is present. + var req request + + if opts.Password == "" && opts.Passcode == "" { + if opts.TokenID != "" { + // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication + // parameters. + if opts.Username != "" { + return nil, ErrUsernameWithToken{} + } + if opts.UserID != "" { + return nil, ErrUserIDWithToken{} + } + if opts.DomainID != "" { + return nil, ErrDomainIDWithToken{} + } + if opts.DomainName != "" { + return nil, ErrDomainNameWithToken{} + } + + // Configure the request for Token authentication. + req.Auth.Identity.Methods = []string{"token"} + req.Auth.Identity.Token = &tokenReq{ + ID: opts.TokenID, + } + + } else if opts.ApplicationCredentialID != "" { + // Configure the request for ApplicationCredentialID authentication. + // https://github.com/openstack/keystoneauth/blob/stable/rocky/keystoneauth1/identity/v3/application_credential.py#L48-L67 + // There are three kinds of possible application_credential requests + // 1. application_credential id + secret + // 2. application_credential name + secret + user_id + // 3. application_credential name + secret + username + domain_id / domain_name + if opts.ApplicationCredentialSecret == "" { + return nil, ErrAppCredMissingSecret{} + } + req.Auth.Identity.Methods = []string{"application_credential"} + req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{ + ID: &opts.ApplicationCredentialID, + Secret: &opts.ApplicationCredentialSecret, + } + } else if opts.ApplicationCredentialName != "" { + if opts.ApplicationCredentialSecret == "" { + return nil, ErrAppCredMissingSecret{} + } + + var userRequest *userReq + + if opts.UserID != "" { + // UserID could be used without the domain information + userRequest = &userReq{ + ID: &opts.UserID, + } + } + + if userRequest == nil && opts.Username == "" { + // Make sure that Username or UserID are provided + return nil, ErrUsernameOrUserID{} + } + + if userRequest == nil && opts.DomainID != "" { + userRequest = &userReq{ + Name: &opts.Username, + Domain: &domainReq{ID: &opts.DomainID}, + } + } + + if userRequest == nil && opts.DomainName != "" { + userRequest = &userReq{ + Name: &opts.Username, + Domain: &domainReq{Name: &opts.DomainName}, + } + } + + // Make sure that DomainID or DomainName are provided among Username + if userRequest == nil { + return nil, ErrDomainIDOrDomainName{} + } + + req.Auth.Identity.Methods = []string{"application_credential"} + req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{ + Name: &opts.ApplicationCredentialName, + User: userRequest, + Secret: &opts.ApplicationCredentialSecret, + } + } else { + // If no password or token ID or ApplicationCredential are available, authentication can't continue. + return nil, ErrMissingPassword{} + } + } else { + // Password authentication. + if opts.Password != "" { + req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "password") + } + + // TOTP authentication. + if opts.Passcode != "" { + req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "totp") + } + + // At least one of Username and UserID must be specified. + if opts.Username == "" && opts.UserID == "" { + return nil, ErrUsernameOrUserID{} + } + + if opts.Username != "" { + // If Username is provided, UserID may not be provided. + if opts.UserID != "" { + return nil, ErrUsernameOrUserID{} + } + + // Either DomainID or DomainName must also be specified. + if opts.DomainID == "" && opts.DomainName == "" { + return nil, ErrDomainIDOrDomainName{} + } + + if opts.DomainID != "" { + if opts.DomainName != "" { + return nil, ErrDomainIDOrDomainName{} + } + + // Configure the request for Username and Password authentication with a DomainID. + if opts.Password != "" { + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: &opts.Password, + Domain: &domainReq{ID: &opts.DomainID}, + }, + } + } + if opts.Passcode != "" { + req.Auth.Identity.TOTP = &totpReq{ + User: &userReq{ + Name: &opts.Username, + Passcode: &opts.Passcode, + Domain: &domainReq{ID: &opts.DomainID}, + }, + } + } + } + + if opts.DomainName != "" { + // Configure the request for Username and Password authentication with a DomainName. + if opts.Password != "" { + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: &opts.Password, + Domain: &domainReq{Name: &opts.DomainName}, + }, + } + } + + if opts.Passcode != "" { + req.Auth.Identity.TOTP = &totpReq{ + User: &userReq{ + Name: &opts.Username, + Passcode: &opts.Passcode, + Domain: &domainReq{Name: &opts.DomainName}, + }, + } + } + } + } + + if opts.UserID != "" { + // If UserID is specified, neither DomainID nor DomainName may be. + if opts.DomainID != "" { + return nil, ErrDomainIDWithUserID{} + } + if opts.DomainName != "" { + return nil, ErrDomainNameWithUserID{} + } + + // Configure the request for UserID and Password authentication. + if opts.Password != "" { + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + ID: &opts.UserID, + Password: &opts.Password, + }, + } + } + + if opts.Passcode != "" { + req.Auth.Identity.TOTP = &totpReq{ + User: &userReq{ + ID: &opts.UserID, + Passcode: &opts.Passcode, + }, + } + } + } + } + + b, err := BuildRequestBody(req, "") + if err != nil { + return nil, err + } + + if len(scope) != 0 { + b["auth"].(map[string]interface{})["scope"] = scope + } + + return b, nil +} + +// ToTokenV3ScopeMap builds a scope from AuthOptions and satisfies interface in +// the v3 tokens package. +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + // For backwards compatibility. + // If AuthOptions.Scope was not set, try to determine it. + // This works well for common scenarios. + if opts.Scope == nil { + opts.Scope = new(AuthScope) + if opts.TenantID != "" { + opts.Scope.ProjectID = opts.TenantID + } else { + if opts.TenantName != "" { + opts.Scope.ProjectName = opts.TenantName + opts.Scope.DomainID = opts.DomainID + opts.Scope.DomainName = opts.DomainName + } + } + } + + if opts.Scope.System { + return map[string]interface{}{ + "system": map[string]interface{}{ + "all": true, + }, + }, nil + } + + if opts.Scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + if opts.Scope.ProjectID != "" { + return nil, ErrScopeProjectIDOrProjectName{} + } + + if opts.Scope.DomainID != "" { + // ProjectName + DomainID + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"id": &opts.Scope.DomainID}, + }, + }, nil + } + + if opts.Scope.DomainName != "" { + // ProjectName + DomainName + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"name": &opts.Scope.DomainName}, + }, + }, nil + } + } else if opts.Scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if opts.Scope.DomainID != "" { + return nil, ErrScopeProjectIDAlone{} + } + if opts.Scope.DomainName != "" { + return nil, ErrScopeProjectIDAlone{} + } + + // ProjectID + return map[string]interface{}{ + "project": map[string]interface{}{ + "id": &opts.Scope.ProjectID, + }, + }, nil + } else if opts.Scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if opts.Scope.DomainName != "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + + // DomainID + return map[string]interface{}{ + "domain": map[string]interface{}{ + "id": &opts.Scope.DomainID, + }, + }, nil + } else if opts.Scope.DomainName != "" { + // DomainName + return map[string]interface{}{ + "domain": map[string]interface{}{ + "name": &opts.Scope.DomainName, + }, + }, nil + } + + return nil, nil +} + +func (opts AuthOptions) CanReauth() bool { + if opts.Passcode != "" { + // cannot reauth using TOTP passcode + return false + } + + return opts.AllowReauth +} + +// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v3 tokens package. +func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) { + return nil, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/auth_result.go b/vendor/github.com/gophercloud/gophercloud/auth_result.go new file mode 100644 index 00000000000..2e4699b978c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/auth_result.go @@ -0,0 +1,52 @@ +package gophercloud + +/* +AuthResult is the result from the request that was used to obtain a provider +client's Keystone token. It is returned from ProviderClient.GetAuthResult(). + +The following types satisfy this interface: + + github.com/gophercloud/gophercloud/openstack/identity/v2/tokens.CreateResult + github.com/gophercloud/gophercloud/openstack/identity/v3/tokens.CreateResult + +Usage example: + + import ( + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + ) + + func GetAuthenticatedUserID(providerClient *gophercloud.ProviderClient) (string, error) { + r := providerClient.GetAuthResult() + if r == nil { + //ProviderClient did not use openstack.Authenticate(), e.g. because token + //was set manually with ProviderClient.SetToken() + return "", errors.New("no AuthResult available") + } + switch r := r.(type) { + case tokens2.CreateResult: + u, err := r.ExtractUser() + if err != nil { + return "", err + } + return u.ID, nil + case tokens3.CreateResult: + u, err := r.ExtractUser() + if err != nil { + return "", err + } + return u.ID, nil + default: + panic(fmt.Sprintf("got unexpected AuthResult type %t", r)) + } + } + +Both implementing types share a lot of methods by name, like ExtractUser() in +this example. But those methods cannot be part of the AuthResult interface +because the return types are different (in this case, type tokens2.User vs. +type tokens3.User). +*/ +type AuthResult interface { + ExtractTokenID() (string, error) +} diff --git a/vendor/github.com/gophercloud/gophercloud/doc.go b/vendor/github.com/gophercloud/gophercloud/doc.go new file mode 100644 index 00000000000..19b64d6508c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/doc.go @@ -0,0 +1,148 @@ +/* +Package gophercloud provides a multi-vendor interface to OpenStack-compatible +clouds. The library has a three-level hierarchy: providers, services, and +resources. + +# Authenticating with Providers + +Provider structs represent the cloud providers that offer and manage a +collection of services. You will generally want to create one Provider +client per OpenStack cloud. + + It is now recommended to use the `clientconfig` package found at + https://github.com/gophercloud/utils/tree/master/openstack/clientconfig + for all authentication purposes. + + The below documentation is still relevant. clientconfig simply implements + the below and presents it in an easier and more flexible way. + +Use your OpenStack credentials to create a Provider client. The +IdentityEndpoint is typically refered to as "auth_url" or "OS_AUTH_URL" in +information provided by the cloud operator. Additionally, the cloud may refer to +TenantID or TenantName as project_id and project_name. Credentials are +specified like so: + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://openstack.example.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", + } + + provider, err := openstack.AuthenticatedClient(opts) + +You can authenticate with a token by doing: + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: "https://openstack.example.com:5000/v2.0", + TokenID: "{token_id}", + TenantID: "{tenant_id}", + } + + provider, err := openstack.AuthenticatedClient(opts) + +You may also use the openstack.AuthOptionsFromEnv() helper function. This +function reads in standard environment variables frequently found in an +OpenStack `openrc` file. Again note that Gophercloud currently uses "tenant" +instead of "project". + + opts, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(opts) + +# Service Clients + +Service structs are specific to a provider and handle all of the logic and +operations for a particular OpenStack service. Examples of services include: +Compute, Object Storage, Block Storage. In order to define one, you need to +pass in the parent provider, like so: + + opts := gophercloud.EndpointOpts{Region: "RegionOne"} + + client, err := openstack.NewComputeV2(provider, opts) + +# Resources + +Resource structs are the domain models that services make use of in order +to work with and represent the state of API resources: + + server, err := servers.Get(client, "{serverId}").Extract() + +Intermediate Result structs are returned for API operations, which allow +generic access to the HTTP headers, response body, and any errors associated +with the network transaction. To turn a result into a usable resource struct, +you must call the Extract method which is chained to the response, or an +Extract function from an applicable extension: + + result := servers.Get(client, "{serverId}") + + // Attempt to extract the disk configuration from the OS-DCF disk config + // extension: + config, err := diskconfig.ExtractGet(result) + +All requests that enumerate a collection return a Pager struct that is used to +iterate through the results one page at a time. Use the EachPage method on that +Pager to handle each successive Page in a closure, then use the appropriate +extraction method from that request's package to interpret that Page as a slice +of results: + + err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) { + s, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + + // Handle the []servers.Server slice. + + // Return "false" or an error to prematurely stop fetching new pages. + return true, nil + }) + +If you want to obtain the entire collection of pages without doing any +intermediary processing on each page, you can use the AllPages method: + + allPages, err := servers.List(client, nil).AllPages() + allServers, err := servers.ExtractServers(allPages) + +This top-level package contains utility functions and data types that are used +throughout the provider and service packages. Of particular note for end users +are the AuthOptions and EndpointOpts structs. + +An example retry backoff function, which respects the 429 HTTP response code and a "Retry-After" header: + + endpoint := "http://localhost:5000" + provider, err := openstack.NewClient(endpoint) + if err != nil { + panic(err) + } + provider.MaxBackoffRetries = 3 // max three retries + provider.RetryBackoffFunc = func(ctx context.Context, respErr *ErrUnexpectedResponseCode, e error, retries uint) error { + retryAfter := respErr.ResponseHeader.Get("Retry-After") + if retryAfter == "" { + return e + } + + var sleep time.Duration + + // Parse delay seconds or HTTP date + if v, err := strconv.ParseUint(retryAfter, 10, 32); err == nil { + sleep = time.Duration(v) * time.Second + } else if v, err := time.Parse(http.TimeFormat, retryAfter); err == nil { + sleep = time.Until(v) + } else { + return e + } + + if ctx != nil { + select { + case <-time.After(sleep): + case <-ctx.Done(): + return e + } + } else { + time.Sleep(sleep) + } + + return nil + } +*/ +package gophercloud diff --git a/vendor/github.com/gophercloud/gophercloud/endpoint_search.go b/vendor/github.com/gophercloud/gophercloud/endpoint_search.go new file mode 100644 index 00000000000..2fbc3c97f14 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/endpoint_search.go @@ -0,0 +1,76 @@ +package gophercloud + +// Availability indicates to whom a specific service endpoint is accessible: +// the internet at large, internal networks only, or only to administrators. +// Different identity services use different terminology for these. Identity v2 +// lists them as different kinds of URLs within the service catalog ("adminURL", +// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an +// endpoint's response. +type Availability string + +const ( + // AvailabilityAdmin indicates that an endpoint is only available to + // administrators. + AvailabilityAdmin Availability = "admin" + + // AvailabilityPublic indicates that an endpoint is available to everyone on + // the internet. + AvailabilityPublic Availability = "public" + + // AvailabilityInternal indicates that an endpoint is only available within + // the cluster's internal network. + AvailabilityInternal Availability = "internal" +) + +// EndpointOpts specifies search criteria used by queries against an +// OpenStack service catalog. The options must contain enough information to +// unambiguously identify one, and only one, endpoint within the catalog. +// +// Usually, these are passed to service client factory functions in a provider +// package, like "openstack.NewComputeV2()". +type EndpointOpts struct { + // Type [required] is the service type for the client (e.g., "compute", + // "object-store"). Generally, this will be supplied by the service client + // function, but a user-given value will be honored if provided. + Type string + + // Name [optional] is the service name for the client (e.g., "nova") as it + // appears in the service catalog. Services can have the same Type but a + // different Name, which is why both Type and Name are sometimes needed. + Name string + + // Region [required] is the geographic region in which the endpoint resides, + // generally specifying which datacenter should house your resources. + // Required only for services that span multiple regions. + Region string + + // Availability [optional] is the visibility of the endpoint to be returned. + // Valid types include the constants AvailabilityPublic, AvailabilityInternal, + // or AvailabilityAdmin from this package. + // + // Availability is not required, and defaults to AvailabilityPublic. Not all + // providers or services offer all Availability options. + Availability Availability +} + +/* +EndpointLocator is an internal function to be used by provider implementations. + +It provides an implementation that locates a single endpoint from a service +catalog for a specific ProviderClient based on user-provided EndpointOpts. The +provider then uses it to discover related ServiceClients. +*/ +type EndpointLocator func(EndpointOpts) (string, error) + +// ApplyDefaults is an internal method to be used by provider implementations. +// +// It sets EndpointOpts fields if not already set, including a default type. +// Currently, EndpointOpts.Availability defaults to the public endpoint. +func (eo *EndpointOpts) ApplyDefaults(t string) { + if eo.Type == "" { + eo.Type = t + } + if eo.Availability == "" { + eo.Availability = AvailabilityPublic + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/errors.go b/vendor/github.com/gophercloud/gophercloud/errors.go new file mode 100644 index 00000000000..8ab592ca491 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/errors.go @@ -0,0 +1,571 @@ +package gophercloud + +import ( + "fmt" + "net/http" + "strings" +) + +// BaseError is an error type that all other error types embed. +type BaseError struct { + DefaultErrString string + Info string +} + +func (e BaseError) Error() string { + e.DefaultErrString = "An error occurred while executing a Gophercloud request." + return e.choseErrString() +} + +func (e BaseError) choseErrString() string { + if e.Info != "" { + return e.Info + } + return e.DefaultErrString +} + +// ErrMissingInput is the error when input is required in a particular +// situation but not provided by the user +type ErrMissingInput struct { + BaseError + Argument string +} + +func (e ErrMissingInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Missing input for argument [%s]", e.Argument) + return e.choseErrString() +} + +// ErrInvalidInput is an error type used for most non-HTTP Gophercloud errors. +type ErrInvalidInput struct { + ErrMissingInput + Value interface{} +} + +func (e ErrInvalidInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Invalid input provided for argument [%s]: [%+v]", e.Argument, e.Value) + return e.choseErrString() +} + +// ErrMissingEnvironmentVariable is the error when environment variable is required +// in a particular situation but not provided by the user +type ErrMissingEnvironmentVariable struct { + BaseError + EnvironmentVariable string +} + +func (e ErrMissingEnvironmentVariable) Error() string { + e.DefaultErrString = fmt.Sprintf("Missing environment variable [%s]", e.EnvironmentVariable) + return e.choseErrString() +} + +// ErrMissingAnyoneOfEnvironmentVariables is the error when anyone of the environment variables +// is required in a particular situation but not provided by the user +type ErrMissingAnyoneOfEnvironmentVariables struct { + BaseError + EnvironmentVariables []string +} + +func (e ErrMissingAnyoneOfEnvironmentVariables) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Missing one of the following environment variables [%s]", + strings.Join(e.EnvironmentVariables, ", "), + ) + return e.choseErrString() +} + +// ErrUnexpectedResponseCode is returned by the Request method when a response code other than +// those listed in OkCodes is encountered. +type ErrUnexpectedResponseCode struct { + BaseError + URL string + Method string + Expected []int + Actual int + Body []byte + ResponseHeader http.Header +} + +func (e ErrUnexpectedResponseCode) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s", + e.Expected, e.Method, e.URL, e.Actual, e.Body, + ) + return e.choseErrString() +} + +// GetStatusCode returns the actual status code of the error. +func (e ErrUnexpectedResponseCode) GetStatusCode() int { + return e.Actual +} + +// StatusCodeError is a convenience interface to easily allow access to the +// status code field of the various ErrDefault* types. +// +// By using this interface, you only have to make a single type cast of +// the returned error to err.(StatusCodeError) and then call GetStatusCode() +// instead of having a large switch statement checking for each of the +// ErrDefault* types. +type StatusCodeError interface { + Error() string + GetStatusCode() int +} + +// ErrDefault400 is the default error type returned on a 400 HTTP response code. +type ErrDefault400 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault400) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault401 is the default error type returned on a 401 HTTP response code. +type ErrDefault401 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault401) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault403 is the default error type returned on a 403 HTTP response code. +type ErrDefault403 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault403) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault404 is the default error type returned on a 404 HTTP response code. +type ErrDefault404 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault404) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault405 is the default error type returned on a 405 HTTP response code. +type ErrDefault405 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault405) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault408 is the default error type returned on a 408 HTTP response code. +type ErrDefault408 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault408) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault409 is the default error type returned on a 409 HTTP response code. +type ErrDefault409 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault409) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault429 is the default error type returned on a 429 HTTP response code. +type ErrDefault429 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault429) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault500 is the default error type returned on a 500 HTTP response code. +type ErrDefault500 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault500) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault502 is the default error type returned on a 502 HTTP response code. +type ErrDefault502 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault502) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault503 is the default error type returned on a 503 HTTP response code. +type ErrDefault503 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault503) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +// ErrDefault504 is the default error type returned on a 504 HTTP response code. +type ErrDefault504 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault504) Unwrap() error { + return e.ErrUnexpectedResponseCode +} + +func (e ErrDefault400) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Bad request with: [%s %s], error message: %s", + e.Method, e.URL, e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault401) Error() string { + return "Authentication failed" +} +func (e ErrDefault403) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Request forbidden: [%s %s], error message: %s", + e.Method, e.URL, e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault404) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Resource not found: [%s %s], error message: %s", + e.Method, e.URL, e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault405) Error() string { + return "Method not allowed" +} +func (e ErrDefault408) Error() string { + return "The server timed out waiting for the request" +} +func (e ErrDefault429) Error() string { + return "Too many requests have been sent in a given amount of time. Pause" + + " requests, wait up to one minute, and try again." +} +func (e ErrDefault500) Error() string { + return "Internal Server Error" +} +func (e ErrDefault502) Error() string { + return "Bad Gateway" +} +func (e ErrDefault503) Error() string { + return "The service is currently unable to handle the request due to a temporary" + + " overloading or maintenance. This is a temporary condition. Try again later." +} +func (e ErrDefault504) Error() string { + return "Gateway Timeout" +} + +// Err400er is the interface resource error types implement to override the error message +// from a 400 error. +type Err400er interface { + Error400(ErrUnexpectedResponseCode) error +} + +// Err401er is the interface resource error types implement to override the error message +// from a 401 error. +type Err401er interface { + Error401(ErrUnexpectedResponseCode) error +} + +// Err403er is the interface resource error types implement to override the error message +// from a 403 error. +type Err403er interface { + Error403(ErrUnexpectedResponseCode) error +} + +// Err404er is the interface resource error types implement to override the error message +// from a 404 error. +type Err404er interface { + Error404(ErrUnexpectedResponseCode) error +} + +// Err405er is the interface resource error types implement to override the error message +// from a 405 error. +type Err405er interface { + Error405(ErrUnexpectedResponseCode) error +} + +// Err408er is the interface resource error types implement to override the error message +// from a 408 error. +type Err408er interface { + Error408(ErrUnexpectedResponseCode) error +} + +// Err409er is the interface resource error types implement to override the error message +// from a 409 error. +type Err409er interface { + Error409(ErrUnexpectedResponseCode) error +} + +// Err429er is the interface resource error types implement to override the error message +// from a 429 error. +type Err429er interface { + Error429(ErrUnexpectedResponseCode) error +} + +// Err500er is the interface resource error types implement to override the error message +// from a 500 error. +type Err500er interface { + Error500(ErrUnexpectedResponseCode) error +} + +// Err502er is the interface resource error types implement to override the error message +// from a 502 error. +type Err502er interface { + Error502(ErrUnexpectedResponseCode) error +} + +// Err503er is the interface resource error types implement to override the error message +// from a 503 error. +type Err503er interface { + Error503(ErrUnexpectedResponseCode) error +} + +// Err504er is the interface resource error types implement to override the error message +// from a 504 error. +type Err504er interface { + Error504(ErrUnexpectedResponseCode) error +} + +// ErrTimeOut is the error type returned when an operations times out. +type ErrTimeOut struct { + BaseError +} + +func (e ErrTimeOut) Error() string { + e.DefaultErrString = "A time out occurred" + return e.choseErrString() +} + +// ErrUnableToReauthenticate is the error type returned when reauthentication fails. +type ErrUnableToReauthenticate struct { + BaseError + ErrOriginal error + ErrReauth error +} + +func (e ErrUnableToReauthenticate) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to re-authenticate: %s: %s", e.ErrOriginal, e.ErrReauth) + return e.choseErrString() +} + +// ErrErrorAfterReauthentication is the error type returned when reauthentication +// succeeds, but an error occurs afterword (usually an HTTP error). +type ErrErrorAfterReauthentication struct { + BaseError + ErrOriginal error +} + +func (e ErrErrorAfterReauthentication) Error() string { + e.DefaultErrString = fmt.Sprintf("Successfully re-authenticated, but got error executing request: %s", e.ErrOriginal) + return e.choseErrString() +} + +// ErrServiceNotFound is returned when no service in a service catalog matches +// the provided EndpointOpts. This is generally returned by provider service +// factory methods like "NewComputeV2()" and can mean that a service is not +// enabled for your account. +type ErrServiceNotFound struct { + BaseError +} + +func (e ErrServiceNotFound) Error() string { + e.DefaultErrString = "No suitable service could be found in the service catalog." + return e.choseErrString() +} + +// ErrEndpointNotFound is returned when no available endpoints match the +// provided EndpointOpts. This is also generally returned by provider service +// factory methods, and usually indicates that a region was specified +// incorrectly. +type ErrEndpointNotFound struct { + BaseError +} + +func (e ErrEndpointNotFound) Error() string { + e.DefaultErrString = "No suitable endpoint could be found in the service catalog." + return e.choseErrString() +} + +// ErrResourceNotFound is the error when trying to retrieve a resource's +// ID by name and the resource doesn't exist. +type ErrResourceNotFound struct { + BaseError + Name string + ResourceType string +} + +func (e ErrResourceNotFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to find %s with name %s", e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrMultipleResourcesFound is the error when trying to retrieve a resource's +// ID by name and multiple resources have the user-provided name. +type ErrMultipleResourcesFound struct { + BaseError + Name string + Count int + ResourceType string +} + +func (e ErrMultipleResourcesFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Found %d %ss matching %s", e.Count, e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrUnexpectedType is the error when an unexpected type is encountered +type ErrUnexpectedType struct { + BaseError + Expected string + Actual string +} + +func (e ErrUnexpectedType) Error() string { + e.DefaultErrString = fmt.Sprintf("Expected %s but got %s", e.Expected, e.Actual) + return e.choseErrString() +} + +func unacceptedAttributeErr(attribute string) string { + return fmt.Sprintf("The base Identity V3 API does not accept authentication by %s", attribute) +} + +func redundantWithTokenErr(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a TokenID", attribute) +} + +func redundantWithUserID(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a UserID", attribute) +} + +// ErrAPIKeyProvided indicates that an APIKey was provided but can't be used. +type ErrAPIKeyProvided struct{ BaseError } + +func (e ErrAPIKeyProvided) Error() string { + return unacceptedAttributeErr("APIKey") +} + +// ErrTenantIDProvided indicates that a TenantID was provided but can't be used. +type ErrTenantIDProvided struct{ BaseError } + +func (e ErrTenantIDProvided) Error() string { + return unacceptedAttributeErr("TenantID") +} + +// ErrTenantNameProvided indicates that a TenantName was provided but can't be used. +type ErrTenantNameProvided struct{ BaseError } + +func (e ErrTenantNameProvided) Error() string { + return unacceptedAttributeErr("TenantName") +} + +// ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead. +type ErrUsernameWithToken struct{ BaseError } + +func (e ErrUsernameWithToken) Error() string { + return redundantWithTokenErr("Username") +} + +// ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead. +type ErrUserIDWithToken struct{ BaseError } + +func (e ErrUserIDWithToken) Error() string { + return redundantWithTokenErr("UserID") +} + +// ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead. +type ErrDomainIDWithToken struct{ BaseError } + +func (e ErrDomainIDWithToken) Error() string { + return redundantWithTokenErr("DomainID") +} + +// ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s +type ErrDomainNameWithToken struct{ BaseError } + +func (e ErrDomainNameWithToken) Error() string { + return redundantWithTokenErr("DomainName") +} + +// ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once. +type ErrUsernameOrUserID struct{ BaseError } + +func (e ErrUsernameOrUserID) Error() string { + return "Exactly one of Username and UserID must be provided for password authentication" +} + +// ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used. +type ErrDomainIDWithUserID struct{ BaseError } + +func (e ErrDomainIDWithUserID) Error() string { + return redundantWithUserID("DomainID") +} + +// ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used. +type ErrDomainNameWithUserID struct{ BaseError } + +func (e ErrDomainNameWithUserID) Error() string { + return redundantWithUserID("DomainName") +} + +// ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it. +// It may also indicate that both a DomainID and a DomainName were provided at once. +type ErrDomainIDOrDomainName struct{ BaseError } + +func (e ErrDomainIDOrDomainName) Error() string { + return "You must provide exactly one of DomainID or DomainName to authenticate by Username" +} + +// ErrMissingPassword indicates that no password was provided and no token is available. +type ErrMissingPassword struct{ BaseError } + +func (e ErrMissingPassword) Error() string { + return "You must provide a password to authenticate" +} + +// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present. +type ErrScopeDomainIDOrDomainName struct{ BaseError } + +func (e ErrScopeDomainIDOrDomainName) Error() string { + return "You must provide exactly one of DomainID or DomainName in a Scope with ProjectName" +} + +// ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope. +type ErrScopeProjectIDOrProjectName struct{ BaseError } + +func (e ErrScopeProjectIDOrProjectName) Error() string { + return "You must provide at most one of ProjectID or ProjectName in a Scope" +} + +// ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope. +type ErrScopeProjectIDAlone struct{ BaseError } + +func (e ErrScopeProjectIDAlone) Error() string { + return "ProjectID must be supplied alone in a Scope" +} + +// ErrScopeEmpty indicates that no credentials were provided in a Scope. +type ErrScopeEmpty struct{ BaseError } + +func (e ErrScopeEmpty) Error() string { + return "You must provide either a Project or Domain in a Scope" +} + +// ErrAppCredMissingSecret indicates that no Application Credential Secret was provided with Application Credential ID or Name +type ErrAppCredMissingSecret struct{ BaseError } + +func (e ErrAppCredMissingSecret) Error() string { + return "You must provide an Application Credential Secret" +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go b/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go new file mode 100644 index 00000000000..7c6d06f0c3e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/auth_env.go @@ -0,0 +1,137 @@ +package openstack + +import ( + "os" + + "github.com/gophercloud/gophercloud" +) + +var nilOptions = gophercloud.AuthOptions{} + +/* +AuthOptionsFromEnv fills out an identity.AuthOptions structure with the +settings found on the various OpenStack OS_* environment variables. + +The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, +OS_PASSWORD and OS_PROJECT_ID. + +Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must have settings, +or an error will result. OS_PROJECT_ID, is optional. + +OS_TENANT_ID and OS_TENANT_NAME are deprecated forms of OS_PROJECT_ID and +OS_PROJECT_NAME and the latter are expected against a v3 auth api. + +If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will still be referred +as "tenant" in Gophercloud. + +If OS_PROJECT_NAME is set, it requires OS_PROJECT_ID to be set as well to +handle projects not on the default domain. + +To use this function, first set the OS_* environment variables (for example, +by sourcing an `openrc` file), then: + + opts, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(opts) +*/ +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := os.Getenv("OS_AUTH_URL") + username := os.Getenv("OS_USERNAME") + userID := os.Getenv("OS_USERID") + password := os.Getenv("OS_PASSWORD") + passcode := os.Getenv("OS_PASSCODE") + tenantID := os.Getenv("OS_TENANT_ID") + tenantName := os.Getenv("OS_TENANT_NAME") + domainID := os.Getenv("OS_DOMAIN_ID") + domainName := os.Getenv("OS_DOMAIN_NAME") + applicationCredentialID := os.Getenv("OS_APPLICATION_CREDENTIAL_ID") + applicationCredentialName := os.Getenv("OS_APPLICATION_CREDENTIAL_NAME") + applicationCredentialSecret := os.Getenv("OS_APPLICATION_CREDENTIAL_SECRET") + systemScope := os.Getenv("OS_SYSTEM_SCOPE") + + // If OS_PROJECT_ID is set, overwrite tenantID with the value. + if v := os.Getenv("OS_PROJECT_ID"); v != "" { + tenantID = v + } + + // If OS_PROJECT_NAME is set, overwrite tenantName with the value. + if v := os.Getenv("OS_PROJECT_NAME"); v != "" { + tenantName = v + } + + if authURL == "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_AUTH_URL", + } + return nilOptions, err + } + + if userID == "" && username == "" { + // Empty username and userID could be ignored, when applicationCredentialID and applicationCredentialSecret are set + if applicationCredentialID == "" && applicationCredentialSecret == "" { + err := gophercloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_USERID", "OS_USERNAME"}, + } + return nilOptions, err + } + } + + if password == "" && passcode == "" && applicationCredentialID == "" && applicationCredentialName == "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + // silently ignore TOTP passcode warning, since it is not a common auth method + EnvironmentVariable: "OS_PASSWORD", + } + return nilOptions, err + } + + if (applicationCredentialID != "" || applicationCredentialName != "") && applicationCredentialSecret == "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_APPLICATION_CREDENTIAL_SECRET", + } + return nilOptions, err + } + + if domainID == "" && domainName == "" && tenantID == "" && tenantName != "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_PROJECT_ID", + } + return nilOptions, err + } + + if applicationCredentialID == "" && applicationCredentialName != "" && applicationCredentialSecret != "" { + if userID == "" && username == "" { + return nilOptions, gophercloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_USERID", "OS_USERNAME"}, + } + } + if username != "" && domainID == "" && domainName == "" { + return nilOptions, gophercloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_DOMAIN_ID", "OS_DOMAIN_NAME"}, + } + } + } + + var scope *gophercloud.AuthScope + if systemScope == "all" { + scope = &gophercloud.AuthScope{ + System: true, + } + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + UserID: userID, + Username: username, + Password: password, + Passcode: passcode, + TenantID: tenantID, + TenantName: tenantName, + DomainID: domainID, + DomainName: domainName, + ApplicationCredentialID: applicationCredentialID, + ApplicationCredentialName: applicationCredentialName, + ApplicationCredentialSecret: applicationCredentialSecret, + Scope: scope, + } + + return ao, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/client.go b/vendor/github.com/gophercloud/gophercloud/openstack/client.go new file mode 100644 index 00000000000..81c907c35b9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/client.go @@ -0,0 +1,503 @@ +package openstack + +import ( + "fmt" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens" + "github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/openstack/utils" +) + +const ( + // v2 represents Keystone v2. + // It should never increase beyond 2.0. + v2 = "v2.0" + + // v3 represents Keystone v3. + // The version can be anything from v3 to v3.x. + v3 = "v3" +) + +/* +NewClient prepares an unauthenticated ProviderClient instance. +Most users will probably prefer using the AuthenticatedClient function +instead. + +This is useful if you wish to explicitly control the version of the identity +service that's used for authentication explicitly, for example. + +A basic example of using this would be: + + ao, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.NewClient(ao.IdentityEndpoint) + client, err := openstack.NewIdentityV3(provider, gophercloud.EndpointOpts{}) +*/ +func NewClient(endpoint string) (*gophercloud.ProviderClient, error) { + base, err := utils.BaseEndpoint(endpoint) + if err != nil { + return nil, err + } + + endpoint = gophercloud.NormalizeURL(endpoint) + base = gophercloud.NormalizeURL(base) + + p := new(gophercloud.ProviderClient) + p.IdentityBase = base + p.IdentityEndpoint = endpoint + p.UseTokenLock() + + return p, nil +} + +/* +AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint +specified by the options, acquires a token, and returns a Provider Client +instance that's ready to operate. + +If the full path to a versioned identity endpoint was specified (example: +http://example.com:5000/v3), that path will be used as the endpoint to query. + +If a versionless endpoint was specified (example: http://example.com:5000/), +the endpoint will be queried to determine which versions of the identity service +are available, then chooses the most recent or most supported version. + +Example: + + ao, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(ao) + client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +*/ +func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + client, err := NewClient(options.IdentityEndpoint) + if err != nil { + return nil, err + } + + err = Authenticate(client, options) + if err != nil { + return nil, err + } + return client, nil +} + +// Authenticate or re-authenticate against the most recent identity service +// supported at the provided endpoint. +func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + versions := []*utils.Version{ + {ID: v2, Priority: 20, Suffix: "/v2.0/"}, + {ID: v3, Priority: 30, Suffix: "/v3/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v2: + return v2auth(client, endpoint, options, gophercloud.EndpointOpts{}) + case v3: + return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{}) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("Unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV2 explicitly authenticates against the identity v2 endpoint. +func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error { + return v2auth(client, "", options, eo) +} + +func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error { + v2Client, err := NewIdentityV2(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v2Client.Endpoint = endpoint + } + + v2Opts := tokens2.AuthOptions{ + IdentityEndpoint: options.IdentityEndpoint, + Username: options.Username, + Password: options.Password, + TenantID: options.TenantID, + TenantName: options.TenantName, + AllowReauth: options.AllowReauth, + TokenID: options.TokenID, + } + + result := tokens2.Create(v2Client, v2Opts) + + err = client.SetTokenAndAuthResult(result) + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + if options.AllowReauth { + // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but + // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`, + // this should retry authentication only once + tac := *client + tac.SetThrowaway(true) + tac.ReauthFunc = nil + tac.SetTokenAndAuthResult(nil) + tao := options + tao.AllowReauth = false + client.ReauthFunc = func() error { + err := v2auth(&tac, endpoint, tao, eo) + if err != nil { + return err + } + client.CopyTokenFrom(&tac) + return nil + } + } + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V2EndpointURL(catalog, opts) + } + + return nil +} + +// AuthenticateV3 explicitly authenticates against the identity v3 service. +func AuthenticateV3(client *gophercloud.ProviderClient, options tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + return v3auth(client, "", options, eo) +} + +func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + // Override the generated service endpoint with the one returned by the version endpoint. + v3Client, err := NewIdentityV3(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v3Client.Endpoint = endpoint + } + + var catalog *tokens3.ServiceCatalog + + var tokenID string + // passthroughToken allows to passthrough the token without a scope + var passthroughToken bool + switch v := opts.(type) { + case *gophercloud.AuthOptions: + tokenID = v.TokenID + passthroughToken = (v.Scope == nil || *v.Scope == gophercloud.AuthScope{}) + case *tokens3.AuthOptions: + tokenID = v.TokenID + passthroughToken = (v.Scope == tokens3.Scope{}) + } + + if tokenID != "" && passthroughToken { + // passing through the token ID without requesting a new scope + if opts.CanReauth() { + return fmt.Errorf("cannot use AllowReauth, when the token ID is defined and auth scope is not set") + } + + v3Client.SetToken(tokenID) + result := tokens3.Get(v3Client, tokenID) + if result.Err != nil { + return result.Err + } + + err = client.SetTokenAndAuthResult(result) + if err != nil { + return err + } + + catalog, err = result.ExtractServiceCatalog() + if err != nil { + return err + } + } else { + var result tokens3.CreateResult + switch opts.(type) { + case *ec2tokens.AuthOptions: + result = ec2tokens.Create(v3Client, opts) + case *oauth1.AuthOptions: + result = oauth1.Create(v3Client, opts) + default: + result = tokens3.Create(v3Client, opts) + } + + err = client.SetTokenAndAuthResult(result) + if err != nil { + return err + } + + catalog, err = result.ExtractServiceCatalog() + if err != nil { + return err + } + } + + if opts.CanReauth() { + // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but + // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`, + // this should retry authentication only once + tac := *client + tac.SetThrowaway(true) + tac.ReauthFunc = nil + tac.SetTokenAndAuthResult(nil) + var tao tokens3.AuthOptionsBuilder + switch ot := opts.(type) { + case *gophercloud.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *tokens3.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *ec2tokens.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *oauth1.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + default: + tao = opts + } + client.ReauthFunc = func() error { + err := v3auth(&tac, endpoint, tao, eo) + if err != nil { + return err + } + client.CopyTokenFrom(&tac) + return nil + } + } + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V3EndpointURL(catalog, opts) + } + + return nil +} + +// NewIdentityV2 creates a ServiceClient that may be used to interact with the +// v2 identity service. +func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v2.0/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) { + eo.ApplyDefaults(clientType) + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 +// identity service. +func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v3/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) { + eo.ApplyDefaults(clientType) + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + // Ensure endpoint still has a suffix of v3. + // This is because EndpointLocator might have found a versionless + // endpoint or the published endpoint is still /v2.0. In both + // cases, we need to fix the endpoint to point to /v3. + base, err := utils.BaseEndpoint(endpoint) + if err != nil { + return nil, err + } + + base = gophercloud.NormalizeURL(base) + + endpoint = base + "v3/" + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string) (*gophercloud.ServiceClient, error) { + sc := new(gophercloud.ServiceClient) + eo.ApplyDefaults(clientType) + url, err := client.EndpointLocator(eo) + if err != nil { + return sc, err + } + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +// NewBareMetalV1 creates a ServiceClient that may be used with the v1 +// bare metal package. +func NewBareMetalV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "baremetal") +} + +// NewBareMetalIntrospectionV1 creates a ServiceClient that may be used with the v1 +// bare metal introspection package. +func NewBareMetalIntrospectionV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "baremetal-introspection") +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 +// object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "object-store") +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute +// package. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "compute") +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network +// package. +func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "network") + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} + +// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 +// block storage service. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "volume") +} + +// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 +// block storage service. +func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "volumev2") +} + +// NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service. +func NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "volumev3") +} + +// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service. +func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "sharev2") +} + +// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1 +// CDN service. +func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "cdn") +} + +// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 +// orchestration service. +func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "orchestration") +} + +// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service. +func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "database") +} + +// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS +// service. +func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "dns") + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewImageServiceV2 creates a ServiceClient that may be used to access the v2 +// image service. +func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "image") + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2 +// load balancer service. +func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "load-balancer") + + // Fixes edge case having an OpenStack lb endpoint with trailing version number. + endpoint := strings.Replace(sc.Endpoint, "v2.0/", "", -1) + + sc.ResourceBase = endpoint + "v2.0/" + return sc, err +} + +// NewClusteringV1 creates a ServiceClient that may be used with the v1 clustering +// package. +func NewClusteringV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "clustering") +} + +// NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging +// service. +func NewMessagingV2(client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "messaging") + sc.MoreHeaders = map[string]string{"Client-ID": clientID} + return sc, err +} + +// NewContainerV1 creates a ServiceClient that may be used with v1 container package +func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "container") +} + +// NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key +// manager service. +func NewKeyManagerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "key-manager") + sc.ResourceBase = sc.Endpoint + "v1/" + return sc, err +} + +// NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management +// package. +func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "container-infra") +} + +// NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package. +func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "workflowv2") +} + +// NewPlacementV1 creates a ServiceClient that may be used with the placement package. +func NewPlacementV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "placement") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go new file mode 100644 index 00000000000..04d9887a143 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go @@ -0,0 +1,36 @@ +/* +Package quotasets enables retrieving and managing Compute quotas. + +Example to Get a Quota Set + + quotaset, err := quotasets.Get(computeClient, "tenant-id").Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", quotaset) + +Example to Get a Detailed Quota Set + + quotaset, err := quotasets.GetDetail(computeClient, "tenant-id").Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", quotaset) + +Example to Update a Quota Set + + updateOpts := quotasets.UpdateOpts{ + FixedIPs: gophercloud.IntToPointer(100), + Cores: gophercloud.IntToPointer(64), + } + + quotaset, err := quotasets.Update(computeClient, "tenant-id", updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", quotaset) +*/ +package quotasets diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go new file mode 100644 index 00000000000..bb99a1085de --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go @@ -0,0 +1,103 @@ +package quotasets + +import ( + "github.com/gophercloud/gophercloud" +) + +// Get returns public data about a previously created QuotaSet. +func Get(client *gophercloud.ServiceClient, tenantID string) (r GetResult) { + resp, err := client.Get(getURL(client, tenantID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetDetail returns detailed public data about a previously created QuotaSet. +func GetDetail(client *gophercloud.ServiceClient, tenantID string) (r GetDetailResult) { + resp, err := client.Get(getDetailURL(client, tenantID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Updates the quotas for the given tenantID and returns the new QuotaSet. +func Update(client *gophercloud.ServiceClient, tenantID string, opts UpdateOptsBuilder) (r UpdateResult) { + reqBody, err := opts.ToComputeQuotaUpdateMap() + if err != nil { + r.Err = err + return + } + + resp, err := client.Put(updateURL(client, tenantID), reqBody, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}}) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Resets the quotas for the given tenant to their default values. +func Delete(client *gophercloud.ServiceClient, tenantID string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, tenantID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Options for Updating the quotas of a Tenant. +// All int-values are pointers so they can be nil if they are not needed. +// You can use gopercloud.IntToPointer() for convenience +type UpdateOpts struct { + // FixedIPs is number of fixed ips allotted this quota_set. + FixedIPs *int `json:"fixed_ips,omitempty"` + + // FloatingIPs is number of floating ips allotted this quota_set. + FloatingIPs *int `json:"floating_ips,omitempty"` + + // InjectedFileContentBytes is content bytes allowed for each injected file. + InjectedFileContentBytes *int `json:"injected_file_content_bytes,omitempty"` + + // InjectedFilePathBytes is allowed bytes for each injected file path. + InjectedFilePathBytes *int `json:"injected_file_path_bytes,omitempty"` + + // InjectedFiles is injected files allowed for each project. + InjectedFiles *int `json:"injected_files,omitempty"` + + // KeyPairs is number of ssh keypairs. + KeyPairs *int `json:"key_pairs,omitempty"` + + // MetadataItems is number of metadata items allowed for each instance. + MetadataItems *int `json:"metadata_items,omitempty"` + + // RAM is megabytes allowed for each instance. + RAM *int `json:"ram,omitempty"` + + // SecurityGroupRules is rules allowed for each security group. + SecurityGroupRules *int `json:"security_group_rules,omitempty"` + + // SecurityGroups security groups allowed for each project. + SecurityGroups *int `json:"security_groups,omitempty"` + + // Cores is number of instance cores allowed for each project. + Cores *int `json:"cores,omitempty"` + + // Instances is number of instances allowed for each project. + Instances *int `json:"instances,omitempty"` + + // Number of ServerGroups allowed for the project. + ServerGroups *int `json:"server_groups,omitempty"` + + // Max number of Members for each ServerGroup. + ServerGroupMembers *int `json:"server_group_members,omitempty"` + + // Force will update the quotaset even if the quota has already been used + // and the reserved quota exceeds the new quota. + Force bool `json:"force,omitempty"` +} + +// UpdateOptsBuilder enables extensins to add parameters to the update request. +type UpdateOptsBuilder interface { + // Extra specific name to prevent collisions with interfaces for other quotas + // (e.g. neutron) + ToComputeQuotaUpdateMap() (map[string]interface{}, error) +} + +// ToComputeQuotaUpdateMap builds the update options into a serializable +// format. +func (opts UpdateOpts) ToComputeQuotaUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "quota_set") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go new file mode 100644 index 00000000000..07fb49c1274 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go @@ -0,0 +1,198 @@ +package quotasets + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// QuotaSet is a set of operational limits that allow for control of compute +// usage. +type QuotaSet struct { + // ID is tenant associated with this QuotaSet. + ID string `json:"id"` + + // FixedIPs is number of fixed ips allotted this QuotaSet. + FixedIPs int `json:"fixed_ips"` + + // FloatingIPs is number of floating ips allotted this QuotaSet. + FloatingIPs int `json:"floating_ips"` + + // InjectedFileContentBytes is the allowed bytes for each injected file. + InjectedFileContentBytes int `json:"injected_file_content_bytes"` + + // InjectedFilePathBytes is allowed bytes for each injected file path. + InjectedFilePathBytes int `json:"injected_file_path_bytes"` + + // InjectedFiles is the number of injected files allowed for each project. + InjectedFiles int `json:"injected_files"` + + // KeyPairs is number of ssh keypairs. + KeyPairs int `json:"key_pairs"` + + // MetadataItems is number of metadata items allowed for each instance. + MetadataItems int `json:"metadata_items"` + + // RAM is megabytes allowed for each instance. + RAM int `json:"ram"` + + // SecurityGroupRules is number of security group rules allowed for each + // security group. + SecurityGroupRules int `json:"security_group_rules"` + + // SecurityGroups is the number of security groups allowed for each project. + SecurityGroups int `json:"security_groups"` + + // Cores is number of instance cores allowed for each project. + Cores int `json:"cores"` + + // Instances is number of instances allowed for each project. + Instances int `json:"instances"` + + // ServerGroups is the number of ServerGroups allowed for the project. + ServerGroups int `json:"server_groups"` + + // ServerGroupMembers is the number of members for each ServerGroup. + ServerGroupMembers int `json:"server_group_members"` +} + +// QuotaDetailSet represents details of both operational limits of compute +// resources and the current usage of those resources. +type QuotaDetailSet struct { + // ID is the tenant ID associated with this QuotaDetailSet. + ID string `json:"id"` + + // FixedIPs is number of fixed ips allotted this QuotaDetailSet. + FixedIPs QuotaDetail `json:"fixed_ips"` + + // FloatingIPs is number of floating ips allotted this QuotaDetailSet. + FloatingIPs QuotaDetail `json:"floating_ips"` + + // InjectedFileContentBytes is the allowed bytes for each injected file. + InjectedFileContentBytes QuotaDetail `json:"injected_file_content_bytes"` + + // InjectedFilePathBytes is allowed bytes for each injected file path. + InjectedFilePathBytes QuotaDetail `json:"injected_file_path_bytes"` + + // InjectedFiles is the number of injected files allowed for each project. + InjectedFiles QuotaDetail `json:"injected_files"` + + // KeyPairs is number of ssh keypairs. + KeyPairs QuotaDetail `json:"key_pairs"` + + // MetadataItems is number of metadata items allowed for each instance. + MetadataItems QuotaDetail `json:"metadata_items"` + + // RAM is megabytes allowed for each instance. + RAM QuotaDetail `json:"ram"` + + // SecurityGroupRules is number of security group rules allowed for each + // security group. + SecurityGroupRules QuotaDetail `json:"security_group_rules"` + + // SecurityGroups is the number of security groups allowed for each project. + SecurityGroups QuotaDetail `json:"security_groups"` + + // Cores is number of instance cores allowed for each project. + Cores QuotaDetail `json:"cores"` + + // Instances is number of instances allowed for each project. + Instances QuotaDetail `json:"instances"` + + // ServerGroups is the number of ServerGroups allowed for the project. + ServerGroups QuotaDetail `json:"server_groups"` + + // ServerGroupMembers is the number of members for each ServerGroup. + ServerGroupMembers QuotaDetail `json:"server_group_members"` +} + +// QuotaDetail is a set of details about a single operational limit that allows +// for control of compute usage. +type QuotaDetail struct { + // InUse is the current number of provisioned/allocated resources of the + // given type. + InUse int `json:"in_use"` + + // Reserved is a transitional state when a claim against quota has been made + // but the resource is not yet fully online. + Reserved int `json:"reserved"` + + // Limit is the maximum number of a given resource that can be + // allocated/provisioned. This is what "quota" usually refers to. + Limit int `json:"limit"` +} + +// QuotaSetPage stores a single page of all QuotaSet results from a List call. +type QuotaSetPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a QuotaSetsetPage is empty. +func (page QuotaSetPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + ks, err := ExtractQuotaSets(page) + return len(ks) == 0, err +} + +// ExtractQuotaSets interprets a page of results as a slice of QuotaSets. +func ExtractQuotaSets(r pagination.Page) ([]QuotaSet, error) { + var s struct { + QuotaSets []QuotaSet `json:"quotas"` + } + err := (r.(QuotaSetPage)).ExtractInto(&s) + return s.QuotaSets, err +} + +type quotaResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any QuotaSet resource response +// as a QuotaSet struct. +func (r quotaResult) Extract() (*QuotaSet, error) { + var s struct { + QuotaSet *QuotaSet `json:"quota_set"` + } + err := r.ExtractInto(&s) + return s.QuotaSet, err +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a QuotaSet. +type GetResult struct { + quotaResult +} + +// UpdateResult is the response from a Update operation. Call its Extract method +// to interpret it as a QuotaSet. +type UpdateResult struct { + quotaResult +} + +// DeleteResult is the response from a Delete operation. Call its Extract method +// to interpret it as a QuotaSet. +type DeleteResult struct { + quotaResult +} + +type quotaDetailResult struct { + gophercloud.Result +} + +// GetDetailResult is the response from a Get operation. Call its Extract +// method to interpret it as a QuotaSet. +type GetDetailResult struct { + quotaDetailResult +} + +// Extract is a method that attempts to interpret any QuotaDetailSet +// resource response as a set of QuotaDetailSet structs. +func (r quotaDetailResult) Extract() (QuotaDetailSet, error) { + var s struct { + QuotaData QuotaDetailSet `json:"quota_set"` + } + err := r.ExtractInto(&s) + return s.QuotaData, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go new file mode 100644 index 00000000000..37e50215b51 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go @@ -0,0 +1,25 @@ +package quotasets + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-quota-sets" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func getURL(c *gophercloud.ServiceClient, tenantID string) string { + return c.ServiceURL(resourcePath, tenantID) +} + +func getDetailURL(c *gophercloud.ServiceClient, tenantID string) string { + return c.ServiceURL(resourcePath, tenantID, "detail") +} + +func updateURL(c *gophercloud.ServiceClient, tenantID string) string { + return getURL(c, tenantID) +} + +func deleteURL(c *gophercloud.ServiceClient, tenantID string) string { + return getURL(c, tenantID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go new file mode 100644 index 00000000000..eedabaf05e4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/doc.go @@ -0,0 +1,111 @@ +/* +Package secgroups provides the ability to manage security groups through the +Nova API. + +This API has been deprecated and will be removed from a future release of the +Nova API service. + +For environments that support this extension, this package can be used +regardless of if either Neutron or nova-network is used as the cloud's network +service. + +Example to List Security Groups + + allPages, err := secroups.List(computeClient).AllPages() + if err != nil { + panic(err) + } + + allSecurityGroups, err := secgroups.ExtractSecurityGroups(allPages) + if err != nil { + panic(err) + } + + for _, sg := range allSecurityGroups { + fmt.Printf("%+v\n", sg) + } + +Example to List Security Groups by Server + + serverID := "aab3ad01-9956-4623-a29b-24afc89a7d36" + + allPages, err := secroups.ListByServer(computeClient, serverID).AllPages() + if err != nil { + panic(err) + } + + allSecurityGroups, err := secgroups.ExtractSecurityGroups(allPages) + if err != nil { + panic(err) + } + + for _, sg := range allSecurityGroups { + fmt.Printf("%+v\n", sg) + } + +Example to Create a Security Group + + createOpts := secgroups.CreateOpts{ + Name: "group_name", + Description: "A Security Group", + } + + sg, err := secgroups.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Security Group Rule + + sgID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + + createOpts := secgroups.CreateRuleOpts{ + ParentGroupID: sgID, + FromPort: 22, + ToPort: 22, + IPProtocol: "tcp", + CIDR: "0.0.0.0/0", + } + + rule, err := secgroups.CreateRule(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Add a Security Group to a Server + + serverID := "aab3ad01-9956-4623-a29b-24afc89a7d36" + sgID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + + err := secgroups.AddServer(computeClient, serverID, sgID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Remove a Security Group from a Server + + serverID := "aab3ad01-9956-4623-a29b-24afc89a7d36" + sgID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + + err := secgroups.RemoveServer(computeClient, serverID, sgID).ExtractErr() + if err != nil { + panic(err) + } + +# Example to Delete a Security Group + + sgID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + err := secgroups.Delete(computeClient, sgID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Delete a Security Group Rule + + ruleID := "6221fe3e-383d-46c9-a3a6-845e66c1e8b4" + err := secgroups.DeleteRule(computeClient, ruleID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package secgroups diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go new file mode 100644 index 00000000000..6f174bb95dd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/requests.go @@ -0,0 +1,191 @@ +package secgroups + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +func commonList(client *gophercloud.ServiceClient, url string) pagination.Pager { + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SecurityGroupPage{pagination.SinglePageBase(r)} + }) +} + +// List will return a collection of all the security groups for a particular +// tenant. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return commonList(client, rootURL(client)) +} + +// ListByServer will return a collection of all the security groups which are +// associated with a particular server. +func ListByServer(client *gophercloud.ServiceClient, serverID string) pagination.Pager { + return commonList(client, listByServerURL(client, serverID)) +} + +// CreateOpts is the struct responsible for creating a security group. +type CreateOpts struct { + // the name of your security group. + Name string `json:"name" required:"true"` + // the description of your security group. + Description string `json:"description,omitempty"` +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecGroupCreateMap() (map[string]interface{}, error) +} + +// ToSecGroupCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSecGroupCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group") +} + +// Create will create a new security group. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecGroupCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(rootURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOpts is the struct responsible for updating an existing security group. +type UpdateOpts struct { + // the name of your security group. + Name string `json:"name,omitempty"` + // the description of your security group. + Description *string `json:"description,omitempty"` +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSecGroupUpdateMap() (map[string]interface{}, error) +} + +// ToSecGroupUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group") +} + +// Update will modify the mutable properties of a security group, notably its +// name and description. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSecGroupUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(resourceURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get will return details for a particular security group. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(resourceURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a security group from the project. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(resourceURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateRuleOpts represents the configuration for adding a new rule to an +// existing security group. +type CreateRuleOpts struct { + // ID is the ID of the group that this rule will be added to. + ParentGroupID string `json:"parent_group_id" required:"true"` + + // FromPort is the lower bound of the port range that will be opened. + // Use -1 to allow all ICMP traffic. + FromPort int `json:"from_port"` + + // ToPort is the upper bound of the port range that will be opened. + // Use -1 to allow all ICMP traffic. + ToPort int `json:"to_port"` + + // IPProtocol the protocol type that will be allowed, e.g. TCP. + IPProtocol string `json:"ip_protocol" required:"true"` + + // CIDR is the network CIDR to allow traffic from. + // This is ONLY required if FromGroupID is blank. This represents the IP + // range that will be the source of network traffic to your security group. + // Use 0.0.0.0/0 to allow all IP addresses. + CIDR string `json:"cidr,omitempty" or:"FromGroupID"` + + // FromGroupID represents another security group to allow access. + // This is ONLY required if CIDR is blank. This value represents the ID of a + // group that forwards traffic to the parent group. So, instead of accepting + // network traffic from an entire IP range, you can instead refine the + // inbound source by an existing security group. + FromGroupID string `json:"group_id,omitempty" or:"CIDR"` +} + +// CreateRuleOptsBuilder allows extensions to add additional parameters to the +// CreateRule request. +type CreateRuleOptsBuilder interface { + ToRuleCreateMap() (map[string]interface{}, error) +} + +// ToRuleCreateMap builds a request body from CreateRuleOpts. +func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "security_group_rule") +} + +// CreateRule will add a new rule to an existing security group (whose ID is +// specified in CreateRuleOpts). You have the option of controlling inbound +// traffic from either an IP range (CIDR) or from another security group. +func CreateRule(client *gophercloud.ServiceClient, opts CreateRuleOptsBuilder) (r CreateRuleResult) { + b, err := opts.ToRuleCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(rootRuleURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteRule will permanently delete a rule from a security group. +func DeleteRule(client *gophercloud.ServiceClient, id string) (r DeleteRuleResult) { + resp, err := client.Delete(resourceRuleURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +func actionMap(prefix, groupName string) map[string]map[string]string { + return map[string]map[string]string{ + prefix + "SecurityGroup": {"name": groupName}, + } +} + +// AddServer will associate a server and a security group, enforcing the +// rules of the group on the server. +func AddServer(client *gophercloud.ServiceClient, serverID, groupName string) (r AddServerResult) { + resp, err := client.Post(serverActionURL(client, serverID), actionMap("add", groupName), nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RemoveServer will disassociate a server from a security group. +func RemoveServer(client *gophercloud.ServiceClient, serverID, groupName string) (r RemoveServerResult) { + resp, err := client.Post(serverActionURL(client, serverID), actionMap("remove", groupName), nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/results.go new file mode 100644 index 00000000000..40c4e94e751 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/results.go @@ -0,0 +1,218 @@ +package secgroups + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// SecurityGroup represents a security group. +type SecurityGroup struct { + // The unique ID of the group. If Neutron is installed, this ID will be + // represented as a string UUID; if Neutron is not installed, it will be a + // numeric ID. For the sake of consistency, we always cast it to a string. + ID string `json:"-"` + + // The human-readable name of the group, which needs to be unique. + Name string `json:"name"` + + // The human-readable description of the group. + Description string `json:"description"` + + // The rules which determine how this security group operates. + Rules []Rule `json:"rules"` + + // The ID of the tenant to which this security group belongs. + TenantID string `json:"tenant_id"` +} + +func (r *SecurityGroup) UnmarshalJSON(b []byte) error { + type tmp SecurityGroup + var s struct { + tmp + ID interface{} `json:"id"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = SecurityGroup(s.tmp) + + switch t := s.ID.(type) { + case float64: + r.ID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ID = t + } + + return err +} + +// Rule represents a security group rule, a policy which determines how a +// security group operates and what inbound traffic it allows in. +type Rule struct { + // The unique ID. If Neutron is installed, this ID will be + // represented as a string UUID; if Neutron is not installed, it will be a + // numeric ID. For the sake of consistency, we always cast it to a string. + ID string `json:"-"` + + // The lower bound of the port range which this security group should open up. + FromPort int `json:"from_port"` + + // The upper bound of the port range which this security group should open up. + ToPort int `json:"to_port"` + + // The IP protocol (e.g. TCP) which the security group accepts. + IPProtocol string `json:"ip_protocol"` + + // The CIDR IP range whose traffic can be received. + IPRange IPRange `json:"ip_range"` + + // The security group ID to which this rule belongs. + ParentGroupID string `json:"-"` + + // Not documented. + Group Group +} + +func (r *Rule) UnmarshalJSON(b []byte) error { + type tmp Rule + var s struct { + tmp + ID interface{} `json:"id"` + ParentGroupID interface{} `json:"parent_group_id"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Rule(s.tmp) + + switch t := s.ID.(type) { + case float64: + r.ID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ID = t + } + + switch t := s.ParentGroupID.(type) { + case float64: + r.ParentGroupID = strconv.FormatFloat(t, 'f', -1, 64) + case string: + r.ParentGroupID = t + } + + return err +} + +// IPRange represents the IP range whose traffic will be accepted by the +// security group. +type IPRange struct { + CIDR string +} + +// Group represents a group. +type Group struct { + TenantID string `json:"tenant_id"` + Name string +} + +// SecurityGroupPage is a single page of a SecurityGroup collection. +type SecurityGroupPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a page of Security Groups contains any +// results. +func (page SecurityGroupPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + users, err := ExtractSecurityGroups(page) + return len(users) == 0, err +} + +// ExtractSecurityGroups returns a slice of SecurityGroups contained in a +// single page of results. +func ExtractSecurityGroups(r pagination.Page) ([]SecurityGroup, error) { + var s struct { + SecurityGroups []SecurityGroup `json:"security_groups"` + } + err := (r.(SecurityGroupPage)).ExtractInto(&s) + return s.SecurityGroups, err +} + +type commonResult struct { + gophercloud.Result +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret the result as a SecurityGroup. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret the result as a SecurityGroup. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret the result as a SecurityGroup. +type UpdateResult struct { + commonResult +} + +// Extract will extract a SecurityGroup struct from most responses. +func (r commonResult) Extract() (*SecurityGroup, error) { + var s struct { + SecurityGroup *SecurityGroup `json:"security_group"` + } + err := r.ExtractInto(&s) + return s.SecurityGroup, err +} + +// CreateRuleResult represents the result when adding rules to a security group. +// Call its Extract method to interpret the result as a Rule. +type CreateRuleResult struct { + gophercloud.Result +} + +// Extract will extract a Rule struct from a CreateRuleResult. +func (r CreateRuleResult) Extract() (*Rule, error) { + var s struct { + Rule *Rule `json:"security_group_rule"` + } + err := r.ExtractInto(&s) + return s.Rule, err +} + +// DeleteResult is the response from delete operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// DeleteRuleResult is the response from a DeleteRule operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteRuleResult struct { + gophercloud.ErrResult +} + +// AddServerResult is the response from an AddServer operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type AddServerResult struct { + gophercloud.ErrResult +} + +// RemoveServerResult is the response from a RemoveServer operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type RemoveServerResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go new file mode 100644 index 00000000000..d99746cae92 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups/urls.go @@ -0,0 +1,32 @@ +package secgroups + +import "github.com/gophercloud/gophercloud" + +const ( + secgrouppath = "os-security-groups" + rulepath = "os-security-group-rules" +) + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(secgrouppath, id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(secgrouppath) +} + +func listByServerURL(c *gophercloud.ServiceClient, serverID string) string { + return c.ServiceURL("servers", serverID, secgrouppath) +} + +func rootRuleURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rulepath) +} + +func resourceRuleURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rulepath, id) +} + +func serverActionURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("servers", id, "action") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/doc.go new file mode 100644 index 00000000000..df9d79e0b97 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/doc.go @@ -0,0 +1,58 @@ +/* +Package usage provides information and interaction with the +SimpleTenantUsage extension for the OpenStack Compute service. + +Due to the way the API responses are formatted, it is not recommended to +query by using the AllPages convenience method. Instead, use the EachPage +method to view each result page-by-page. + +This is because the usage calculations are done _per page_ and not as +an aggregated total of the entire usage set. + +Example to Retrieve Usage for a Single Tenant: + + start := time.Date(2017, 01, 21, 10, 4, 20, 0, time.UTC) + end := time.Date(2017, 01, 21, 10, 4, 20, 0, time.UTC) + + singleTenantOpts := usage.SingleTenantOpts{ + Start: &start, + End: &end, + } + + err := usage.SingleTenant(computeClient, tenantID, singleTenantOpts).EachPage(func(page pagination.Page) (bool, error) { + tenantUsage, err := usage.ExtractSingleTenant(page) + if err != nil { + return false, err + } + + fmt.Printf("%+v\n", tenantUsage) + + return true, nil + }) + + if err != nil { + panic(err) + } + +Example to Retrieve Usage for All Tenants: + + allTenantsOpts := usage.AllTenantsOpts{ + Detailed: true, + } + + err := usage.AllTenants(computeClient, allTenantsOpts).EachPage(func(page pagination.Page) (bool, error) { + allTenantsUsage, err := usage.ExtractAllTenants(page) + if err != nil { + return false, err + } + + fmt.Printf("%+v\n", allTenantsUsage) + + return true, nil + }) + + if err != nil { + panic(err) + } +*/ +package usage diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/requests.go new file mode 100644 index 00000000000..eb36f59b7e9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/requests.go @@ -0,0 +1,134 @@ +package usage + +import ( + "net/url" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// SingleTenantOpts are options for fetching usage of a single tenant. +type SingleTenantOpts struct { + // The ending time to calculate usage statistics on compute and storage resources. + End *time.Time `q:"end"` + + // The beginning time to calculate usage statistics on compute and storage resources. + Start *time.Time `q:"start"` + + // Limit limits the amount of results returned by the API. + // This requires the client to be set to microversion 2.40 or later. + Limit int `q:"limit"` + + // Marker instructs the API call where to start listing from. + // This requires the client to be set to microversion 2.40 or later. + Marker string `q:"marker"` +} + +// SingleTenantOptsBuilder allows extensions to add additional parameters to the +// SingleTenant request. +type SingleTenantOptsBuilder interface { + ToUsageSingleTenantQuery() (string, error) +} + +// ToUsageSingleTenantQuery formats a SingleTenantOpts into a query string. +func (opts SingleTenantOpts) ToUsageSingleTenantQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + + params := q.Query() + + if opts.Start != nil { + params.Add("start", opts.Start.Format(gophercloud.RFC3339MilliNoZ)) + } + + if opts.End != nil { + params.Add("end", opts.End.Format(gophercloud.RFC3339MilliNoZ)) + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), nil +} + +// SingleTenant returns usage data about a single tenant. +func SingleTenant(client *gophercloud.ServiceClient, tenantID string, opts SingleTenantOptsBuilder) pagination.Pager { + url := getTenantURL(client, tenantID) + if opts != nil { + query, err := opts.ToUsageSingleTenantQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SingleTenantPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// AllTenantsOpts are options for fetching usage of all tenants. +type AllTenantsOpts struct { + // Detailed will return detailed results. + Detailed bool + + // The ending time to calculate usage statistics on compute and storage resources. + End *time.Time `q:"end"` + + // The beginning time to calculate usage statistics on compute and storage resources. + Start *time.Time `q:"start"` + + // Limit limits the amount of results returned by the API. + // This requires the client to be set to microversion 2.40 or later. + Limit int `q:"limit"` + + // Marker instructs the API call where to start listing from. + // This requires the client to be set to microversion 2.40 or later. + Marker string `q:"marker"` +} + +// AllTenantsOptsBuilder allows extensions to add additional parameters to the +// AllTenants request. +type AllTenantsOptsBuilder interface { + ToUsageAllTenantsQuery() (string, error) +} + +// ToUsageAllTenantsQuery formats a AllTenantsOpts into a query string. +func (opts AllTenantsOpts) ToUsageAllTenantsQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + + params := q.Query() + + if opts.Start != nil { + params.Add("start", opts.Start.Format(gophercloud.RFC3339MilliNoZ)) + } + + if opts.End != nil { + params.Add("end", opts.End.Format(gophercloud.RFC3339MilliNoZ)) + } + + if opts.Detailed == true { + params.Add("detailed", "1") + } + + q = &url.URL{RawQuery: params.Encode()} + return q.String(), nil +} + +// AllTenants returns usage data about all tenants. +func AllTenants(client *gophercloud.ServiceClient, opts AllTenantsOptsBuilder) pagination.Pager { + url := allTenantsURL(client) + if opts != nil { + query, err := opts.ToUsageAllTenantsQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AllTenantsPage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/results.go new file mode 100644 index 00000000000..8c36dde8f21 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/results.go @@ -0,0 +1,191 @@ +package usage + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// TenantUsage is a set of usage information about a tenant over the sampling window +type TenantUsage struct { + // ServerUsages is an array of ServerUsage maps + ServerUsages []ServerUsage `json:"server_usages"` + + // Start is the beginning time to calculate usage statistics on compute and storage resources + Start time.Time `json:"-"` + + // Stop is the ending time to calculate usage statistics on compute and storage resources + Stop time.Time `json:"-"` + + // TenantID is the ID of the tenant whose usage is being reported on + TenantID string `json:"tenant_id"` + + // TotalHours is the total duration that servers exist (in hours) + TotalHours float64 `json:"total_hours"` + + // TotalLocalGBUsage multiplies the server disk size (in GiB) by hours the server exists, and then adding that all together for each server + TotalLocalGBUsage float64 `json:"total_local_gb_usage"` + + // TotalMemoryMBUsage multiplies the server memory size (in MB) by hours the server exists, and then adding that all together for each server + TotalMemoryMBUsage float64 `json:"total_memory_mb_usage"` + + // TotalVCPUsUsage multiplies the number of virtual CPUs of the server by hours the server exists, and then adding that all together for each server + TotalVCPUsUsage float64 `json:"total_vcpus_usage"` +} + +// UnmarshalJSON sets *u to a copy of data. +func (u *TenantUsage) UnmarshalJSON(b []byte) error { + type tmp TenantUsage + var s struct { + tmp + Start gophercloud.JSONRFC3339MilliNoZ `json:"start"` + Stop gophercloud.JSONRFC3339MilliNoZ `json:"stop"` + } + + if err := json.Unmarshal(b, &s); err != nil { + return err + } + *u = TenantUsage(s.tmp) + + u.Start = time.Time(s.Start) + u.Stop = time.Time(s.Stop) + + return nil +} + +// ServerUsage is a detailed set of information about a specific instance inside a tenant +type ServerUsage struct { + // EndedAt is the date and time when the server was deleted + EndedAt time.Time `json:"-"` + + // Flavor is the display name of a flavor + Flavor string `json:"flavor"` + + // Hours is the duration that the server exists in hours + Hours float64 `json:"hours"` + + // InstanceID is the UUID of the instance + InstanceID string `json:"instance_id"` + + // LocalGB is the sum of the root disk size of the server and the ephemeral disk size of it (in GiB) + LocalGB int `json:"local_gb"` + + // MemoryMB is the memory size of the server (in MB) + MemoryMB int `json:"memory_mb"` + + // Name is the name assigned to the server when it was created + Name string `json:"name"` + + // StartedAt is the date and time when the server was started + StartedAt time.Time `json:"-"` + + // State is the VM power state + State string `json:"state"` + + // TenantID is the UUID of the tenant in a multi-tenancy cloud + TenantID string `json:"tenant_id"` + + // Uptime is the uptime of the server in seconds + Uptime int `json:"uptime"` + + // VCPUs is the number of virtual CPUs that the server uses + VCPUs int `json:"vcpus"` +} + +// UnmarshalJSON sets *u to a copy of data. +func (u *ServerUsage) UnmarshalJSON(b []byte) error { + type tmp ServerUsage + var s struct { + tmp + EndedAt gophercloud.JSONRFC3339MilliNoZ `json:"ended_at"` + StartedAt gophercloud.JSONRFC3339MilliNoZ `json:"started_at"` + } + + if err := json.Unmarshal(b, &s); err != nil { + return err + } + *u = ServerUsage(s.tmp) + + u.EndedAt = time.Time(s.EndedAt) + u.StartedAt = time.Time(s.StartedAt) + + return nil +} + +// SingleTenantPage stores a single, only page of TenantUsage results from a +// SingleTenant call. +type SingleTenantPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a SingleTenantPage is empty. +func (r SingleTenantPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + ks, err := ExtractSingleTenant(r) + return ks == nil, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r SingleTenantPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"tenant_usage_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractSingleTenant interprets a SingleTenantPage as a TenantUsage result. +func ExtractSingleTenant(page pagination.Page) (*TenantUsage, error) { + var s struct { + TenantUsage *TenantUsage `json:"tenant_usage"` + } + err := (page.(SingleTenantPage)).ExtractInto(&s) + return s.TenantUsage, err +} + +// AllTenantsPage stores a single, only page of TenantUsage results from a +// AllTenants call. +type AllTenantsPage struct { + pagination.LinkedPageBase +} + +// ExtractAllTenants interprets a AllTenantsPage as a TenantUsage result. +func ExtractAllTenants(page pagination.Page) ([]TenantUsage, error) { + var s struct { + TenantUsages []TenantUsage `json:"tenant_usages"` + } + err := (page.(AllTenantsPage)).ExtractInto(&s) + return s.TenantUsages, err +} + +// IsEmpty determines whether or not an AllTenantsPage is empty. +func (r AllTenantsPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + usages, err := ExtractAllTenants(r) + return len(usages) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r AllTenantsPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"tenant_usages_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/urls.go new file mode 100644 index 00000000000..50636107010 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/urls.go @@ -0,0 +1,13 @@ +package usage + +import "github.com/gophercloud/gophercloud" + +const resourcePath = "os-simple-tenant-usage" + +func allTenantsURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL(resourcePath) +} + +func getTenantURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL(resourcePath, tenantID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go new file mode 100644 index 00000000000..747966d8d99 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go @@ -0,0 +1,150 @@ +/* +Package flavors provides information and interaction with the flavor API +in the OpenStack Compute service. + +A flavor is an available hardware configuration for a server. Each flavor +has a unique combination of disk space, memory capacity and priority for CPU +time. + +Example to List Flavors + + listOpts := flavors.ListOpts{ + AccessType: flavors.PublicAccess, + } + + allPages, err := flavors.ListDetail(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + panic(err) + } + + for _, flavor := range allFlavors { + fmt.Printf("%+v\n", flavor) + } + +Example to Create a Flavor + + createOpts := flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: gophercloud.IntToPointer(1), + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + + flavor, err := flavors.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + updateOpts := flavors.UpdateOpts{ + Description: "This is a good description" + } + + flavor, err := flavors.Update(computeClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to List Flavor Access + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + allPages, err := flavors.ListAccesses(computeClient, flavorID).AllPages() + if err != nil { + panic(err) + } + + allAccesses, err := flavors.ExtractAccesses(allPages) + if err != nil { + panic(err) + } + + for _, access := range allAccesses { + fmt.Printf("%+v", access) + } + +Example to Grant Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.AddAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.AddAccess(computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove/Revoke Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.RemoveAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.RemoveAccess(computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + createOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_policy": "CPU-POLICY", + "hw:cpu_thread_policy": "CPU-THREAD-POLICY", + } + createdExtraSpecs, err := flavors.CreateExtraSpecs(computeClient, flavorID, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", createdExtraSpecs) + +Example to Get Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + extraSpecs, err := flavors.ListExtraSpecs(computeClient, flavorID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", extraSpecs) + +Example to Update Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + updateOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_thread_policy": "CPU-THREAD-POLICY-UPDATED", + } + updatedExtraSpec, err := flavors.UpdateExtraSpec(computeClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", updatedExtraSpec) + +Example to Delete an Extra Spec for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + err := flavors.DeleteExtraSpec(computeClient, flavorID, "hw:cpu_thread_policy").ExtractErr() + if err != nil { + panic(err) + } +*/ +package flavors diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go new file mode 100644 index 00000000000..3887cdfdca8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go @@ -0,0 +1,364 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +/* +AccessType maps to OpenStack's Flavor.is_public field. Although the is_public +field is boolean, the request options are ternary, which is why AccessType is +a string. The following values are allowed: + +The AccessType arguement is optional, and if it is not supplied, OpenStack +returns the PublicAccess flavors. +*/ +type AccessType string + +const ( + // PublicAccess returns public flavors and private flavors associated with + // that project. + PublicAccess AccessType = "true" + + // PrivateAccess (admin only) returns private flavors, across all projects. + PrivateAccess AccessType = "false" + + // AllAccess (admin only) returns public and private flavors across all + // projects. + AllAccess AccessType = "None" +) + +/* +ListOpts filters the results returned by the List() function. +For example, a flavor with a minDisk field of 10 will not be returned if you +specify MinDisk set to 20. + +Typically, software will use the last ID of the previous call to List to set +the Marker for the current call. +*/ +type ListOpts struct { + // ChangesSince, if provided, instructs List to return only those things which + // have changed since the timestamp provided. + ChangesSince string `q:"changes-since"` + + // MinDisk and MinRAM, if provided, elides flavors which do not meet your + // criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // SortDir allows to select sort direction. + // It can be "asc" or "desc" (default). + SortDir string `q:"sort_dir"` + + // SortKey allows to sort by one of the flavors attributes. + // Default is flavorid. + SortKey string `q:"sort_key"` + + // Marker and Limit control paging. + // Marker instructs List where to start listing from. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of + // flavors. + Limit int `q:"limit"` + + // AccessType, if provided, instructs List which set of flavors to return. + // If IsPublic not provided, flavors for the current project are returned. + AccessType AccessType `q:"is_public"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail instructs OpenStack to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier +// processing. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type CreateOptsBuilder interface { + ToFlavorCreateMap() (map[string]interface{}, error) +} + +// CreateOpts specifies parameters used for creating a flavor. +type CreateOpts struct { + // Name is the name of the flavor. + Name string `json:"name" required:"true"` + + // RAM is the memory of the flavor, measured in MB. + RAM int `json:"ram" required:"true"` + + // VCPUs is the number of vcpus for the flavor. + VCPUs int `json:"vcpus" required:"true"` + + // Disk the amount of root disk space, measured in GB. + Disk *int `json:"disk" required:"true"` + + // ID is a unique ID for the flavor. + ID string `json:"id,omitempty"` + + // Swap is the amount of swap space for the flavor, measured in MB. + Swap *int `json:"swap,omitempty"` + + // RxTxFactor alters the network bandwidth of a flavor. + RxTxFactor float64 `json:"rxtx_factor,omitempty"` + + // IsPublic flags a flavor as being available to all projects or not. + IsPublic *bool `json:"os-flavor-access:is_public,omitempty"` + + // Ephemeral is the amount of ephemeral disk space, measured in GB. + Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"` + + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description,omitempty"` +} + +// ToFlavorCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "flavor") +} + +// Create requests the creation of a new flavor. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFlavorCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +type UpdateOptsBuilder interface { + ToFlavorUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies parameters used for updating a flavor. +type UpdateOpts struct { + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description,omitempty"` +} + +// ToFlavorUpdateMap constructs a request body from UpdateOpts. +func (opts UpdateOpts) ToFlavorUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "flavor") +} + +// Update requests the update of a new flavor. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFlavorUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves details of a single flavor. Use Extract to convert its +// result into a Flavor. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete deletes the specified flavor ID. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAccesses retrieves the tenants which have access to a flavor. +func ListAccesses(client *gophercloud.ServiceClient, id string) pagination.Pager { + url := accessURL(client, id) + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessPage{pagination.SinglePageBase(r)} + }) +} + +// AddAccessOptsBuilder allows extensions to add additional parameters to the +// AddAccess requests. +type AddAccessOptsBuilder interface { + ToFlavorAddAccessMap() (map[string]interface{}, error) +} + +// AddAccessOpts represents options for adding access to a flavor. +type AddAccessOpts struct { + // Tenant is the project/tenant ID to grant access. + Tenant string `json:"tenant"` +} + +// ToFlavorAddAccessMap constructs a request body from AddAccessOpts. +func (opts AddAccessOpts) ToFlavorAddAccessMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "addTenantAccess") +} + +// AddAccess grants a tenant/project access to a flavor. +func AddAccess(client *gophercloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) { + b, err := opts.ToFlavorAddAccessMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RemoveAccessOptsBuilder allows extensions to add additional parameters to the +// RemoveAccess requests. +type RemoveAccessOptsBuilder interface { + ToFlavorRemoveAccessMap() (map[string]interface{}, error) +} + +// RemoveAccessOpts represents options for removing access to a flavor. +type RemoveAccessOpts struct { + // Tenant is the project/tenant ID to grant access. + Tenant string `json:"tenant"` +} + +// ToFlavorRemoveAccessMap constructs a request body from RemoveAccessOpts. +func (opts RemoveAccessOpts) ToFlavorRemoveAccessMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "removeTenantAccess") +} + +// RemoveAccess removes/revokes a tenant/project access to a flavor. +func RemoveAccess(client *gophercloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) { + b, err := opts.ToFlavorRemoveAccessMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ExtraSpecs requests all the extra-specs for the given flavor ID. +func ListExtraSpecs(client *gophercloud.ServiceClient, flavorID string) (r ListExtraSpecsResult) { + resp, err := client.Get(extraSpecsListURL(client, flavorID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +func GetExtraSpec(client *gophercloud.ServiceClient, flavorID string, key string) (r GetExtraSpecResult) { + resp, err := client.Get(extraSpecsGetURL(client, flavorID, key), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateExtraSpecsOptsBuilder allows extensions to add additional parameters to the +// CreateExtraSpecs requests. +type CreateExtraSpecsOptsBuilder interface { + ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) +} + +// ExtraSpecsOpts is a map that contains key-value pairs. +type ExtraSpecsOpts map[string]string + +// ToFlavorExtraSpecsCreateMap assembles a body for a Create request based on +// the contents of ExtraSpecsOpts. +func (opts ExtraSpecsOpts) ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) { + return map[string]interface{}{"extra_specs": opts}, nil +} + +// CreateExtraSpecs will create or update the extra-specs key-value pairs for +// the specified Flavor. +func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts CreateExtraSpecsOptsBuilder) (r CreateExtraSpecsResult) { + b, err := opts.ToFlavorExtraSpecsCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(extraSpecsCreateURL(client, flavorID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateExtraSpecOptsBuilder interface { + ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) +} + +// ToFlavorExtraSpecUpdateMap assembles a body for an Update request based on +// the contents of a ExtraSpecOpts. +func (opts ExtraSpecsOpts) ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) { + if len(opts) != 1 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "flavors.ExtraSpecOpts" + err.Info = "Must have 1 and only one key-value pair" + return nil, "", err + } + + var key string + for k := range opts { + key = k + } + + return opts, key, nil +} + +// UpdateExtraSpec will updates the value of the specified flavor's extra spec +// for the key in opts. +func UpdateExtraSpec(client *gophercloud.ServiceClient, flavorID string, opts UpdateExtraSpecOptsBuilder) (r UpdateExtraSpecResult) { + b, key, err := opts.ToFlavorExtraSpecUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(extraSpecUpdateURL(client, flavorID, key), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteExtraSpec will delete the key-value pair with the given key for the given +// flavor ID. +func DeleteExtraSpec(client *gophercloud.ServiceClient, flavorID, key string) (r DeleteExtraSpecResult) { + resp, err := client.Delete(extraSpecDeleteURL(client, flavorID, key), &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go new file mode 100644 index 00000000000..4da14118a35 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go @@ -0,0 +1,271 @@ +package flavors + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// CreateResult is the response of a Get operations. Call its Extract method to +// interpret it as a Flavor. +type CreateResult struct { + commonResult +} + +// UpdateResult is the response of a Put operation. Call its Extract method to +// interpret it as a Flavor. +type UpdateResult struct { + commonResult +} + +// GetResult is the response of a Get operations. Call its Extract method to +// interpret it as a Flavor. +type GetResult struct { + commonResult +} + +// DeleteResult is the result from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Extract provides access to the individual Flavor returned by the Get and +// Create functions. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor represent (virtual) hardware configurations for server resources +// in a region. +type Flavor struct { + // ID is the flavor's unique ID. + ID string `json:"id"` + + // Disk is the amount of root disk, measured in GB. + Disk int `json:"disk"` + + // RAM is the amount of memory, measured in MB. + RAM int `json:"ram"` + + // Name is the name of the flavor. + Name string `json:"name"` + + // RxTxFactor describes bandwidth alterations of the flavor. + RxTxFactor float64 `json:"rxtx_factor"` + + // Swap is the amount of swap space, measured in MB. + Swap int `json:"-"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `json:"vcpus"` + + // IsPublic indicates whether the flavor is public. + IsPublic bool `json:"os-flavor-access:is_public"` + + // Ephemeral is the amount of ephemeral disk space, measured in GB. + Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"` + + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description"` +} + +func (r *Flavor) UnmarshalJSON(b []byte) error { + type tmp Flavor + var s struct { + tmp + Swap interface{} `json:"swap"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Flavor(s.tmp) + + switch t := s.Swap.(type) { + case float64: + r.Swap = int(t) + case string: + switch t { + case "": + r.Swap = 0 + default: + swap, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Swap = int(swap) + } + } + + return nil +} + +// FlavorPage contains a single page of all flavors from a ListDetails call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a FlavorPage contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractFlavors provides access to the list of flavors in a page acquired +// from the ListDetail operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} + +// AccessPage contains a single page of all FlavorAccess entries for a flavor. +type AccessPage struct { + pagination.SinglePageBase +} + +// IsEmpty indicates whether an AccessPage is empty. +func (page AccessPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + v, err := ExtractAccesses(page) + return len(v) == 0, err +} + +// ExtractAccesses interprets a page of results as a slice of FlavorAccess. +func ExtractAccesses(r pagination.Page) ([]FlavorAccess, error) { + var s struct { + FlavorAccesses []FlavorAccess `json:"flavor_access"` + } + err := (r.(AccessPage)).ExtractInto(&s) + return s.FlavorAccesses, err +} + +type accessResult struct { + gophercloud.Result +} + +// AddAccessResult is the response of an AddAccess operation. Call its +// Extract method to interpret it as a slice of FlavorAccess. +type AddAccessResult struct { + accessResult +} + +// RemoveAccessResult is the response of a RemoveAccess operation. Call its +// Extract method to interpret it as a slice of FlavorAccess. +type RemoveAccessResult struct { + accessResult +} + +// Extract provides access to the result of an access create or delete. +// The result will be all accesses that the flavor has. +func (r accessResult) Extract() ([]FlavorAccess, error) { + var s struct { + FlavorAccesses []FlavorAccess `json:"flavor_access"` + } + err := r.ExtractInto(&s) + return s.FlavorAccesses, err +} + +// FlavorAccess represents an ACL of tenant access to a specific Flavor. +type FlavorAccess struct { + // FlavorID is the unique ID of the flavor. + FlavorID string `json:"flavor_id"` + + // TenantID is the unique ID of the tenant. + TenantID string `json:"tenant_id"` +} + +// Extract interprets any extraSpecsResult as ExtraSpecs, if possible. +func (r extraSpecsResult) Extract() (map[string]string, error) { + var s struct { + ExtraSpecs map[string]string `json:"extra_specs"` + } + err := r.ExtractInto(&s) + return s.ExtraSpecs, err +} + +// extraSpecsResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type extraSpecsResult struct { + gophercloud.Result +} + +// ListExtraSpecsResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type ListExtraSpecsResult struct { + extraSpecsResult +} + +// CreateExtraSpecResult contains the result of a Create operation. Call its +// Extract method to interpret it as a map[string]interface. +type CreateExtraSpecsResult struct { + extraSpecsResult +} + +// extraSpecResult contains the result of a call for individual a single +// key-value pair. +type extraSpecResult struct { + gophercloud.Result +} + +// GetExtraSpecResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetExtraSpecResult struct { + extraSpecResult +} + +// UpdateExtraSpecResult contains the result of an Update operation. Call its +// Extract method to interpret it as a map[string]interface. +type UpdateExtraSpecResult struct { + extraSpecResult +} + +// DeleteExtraSpecResult contains the result of a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DeleteExtraSpecResult struct { + gophercloud.ErrResult +} + +// Extract interprets any extraSpecResult as an ExtraSpec, if possible. +func (r extraSpecResult) Extract() (map[string]string, error) { + var s map[string]string + err := r.ExtractInto(&s) + return s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go new file mode 100644 index 00000000000..65bbb654012 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go @@ -0,0 +1,53 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud" +) + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors") +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func accessURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-flavor-access") +} + +func accessActionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "action") +} + +func extraSpecsListURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +func extraSpecsGetURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} + +func extraSpecsCreateURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +func extraSpecUpdateURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} + +func extraSpecDeleteURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go new file mode 100644 index 00000000000..bab72c1524e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/doc.go @@ -0,0 +1,135 @@ +/* +Package servers provides information and interaction with the server API +resource in the OpenStack Compute service. + +A server is a virtual machine instance in the compute system. In order for +one to be provisioned, a valid flavor and image are required. + +Example to List Servers + + listOpts := servers.ListOpts{ + AllTenants: true, + } + + allPages, err := servers.ListSimple(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to List Detail Servers + + listOpts := servers.ListOpts{ + AllTenants: true, + } + + allPages, err := servers.List(computeClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to Create a Server + + createOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + server, err := servers.Create(computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + err := servers.Delete(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Force Delete a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + err := servers.ForceDelete(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Reboot a Server + + rebootOpts := servers.RebootOpts{ + Type: servers.SoftReboot, + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + err := servers.Reboot(computeClient, serverID, rebootOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Rebuild a Server + + rebuildOpts := servers.RebuildOpts{ + Name: "new_name", + ImageID: "image-uuid", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + server, err := servers.Rebuilt(computeClient, serverID, rebuildOpts).Extract() + if err != nil { + panic(err) + } + +Example to Resize a Server + + resizeOpts := servers.ResizeOpts{ + FlavorRef: "flavor-uuid", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + err := servers.Resize(computeClient, serverID, resizeOpts).ExtractErr() + if err != nil { + panic(err) + } + + err = servers.ConfirmResize(computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Snapshot a Server + + snapshotOpts := servers.CreateImageOpts{ + Name: "snapshot_name", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + image, err := servers.CreateImage(computeClient, serverID, snapshotOpts).ExtractImageID() + if err != nil { + panic(err) + } +*/ +package servers diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go new file mode 100644 index 00000000000..c9f0e3c20b5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/errors.go @@ -0,0 +1,71 @@ +package servers + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" +) + +// ErrNeitherImageIDNorImageNameProvided is the error when neither the image +// ID nor the image name is provided for a server operation +type ErrNeitherImageIDNorImageNameProvided struct{ gophercloud.ErrMissingInput } + +func (e ErrNeitherImageIDNorImageNameProvided) Error() string { + return "One and only one of the image ID and the image name must be provided." +} + +// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor +// ID nor the flavor name is provided for a server operation +type ErrNeitherFlavorIDNorFlavorNameProvided struct{ gophercloud.ErrMissingInput } + +func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string { + return "One and only one of the flavor ID and the flavor name must be provided." +} + +type ErrNoClientProvidedForIDByName struct{ gophercloud.ErrMissingInput } + +func (e ErrNoClientProvidedForIDByName) Error() string { + return "A service client must be provided to find a resource ID by name." +} + +// ErrInvalidHowParameterProvided is the error when an unknown value is given +// for the `how` argument +type ErrInvalidHowParameterProvided struct{ gophercloud.ErrInvalidInput } + +// ErrNoAdminPassProvided is the error when an administrative password isn't +// provided for a server operation +type ErrNoAdminPassProvided struct{ gophercloud.ErrMissingInput } + +// ErrNoImageIDProvided is the error when an image ID isn't provided for a server +// operation +type ErrNoImageIDProvided struct{ gophercloud.ErrMissingInput } + +// ErrNoIDProvided is the error when a server ID isn't provided for a server +// operation +type ErrNoIDProvided struct{ gophercloud.ErrMissingInput } + +// ErrServer is a generic error type for servers HTTP operations. +type ErrServer struct { + gophercloud.ErrUnexpectedResponseCode + ID string +} + +func (se ErrServer) Error() string { + return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID) +} + +// Error404 overrides the generic 404 error message. +func (se ErrServer) Error404(e gophercloud.ErrUnexpectedResponseCode) error { + se.ErrUnexpectedResponseCode = e + return &ErrServerNotFound{se} +} + +// ErrServerNotFound is the error when a 404 is received during server HTTP +// operations. +type ErrServerNotFound struct { + ErrServer +} + +func (e ErrServerNotFound) Error() string { + return fmt.Sprintf("I couldn't find server [%s]", e.ID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go new file mode 100644 index 00000000000..d6a903aab9a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/requests.go @@ -0,0 +1,784 @@ +package servers + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // ChangesSince is a time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Image is the name of the image in URL format. + Image string `q:"image"` + + // Flavor is the name of the flavor in URL format. + Flavor string `q:"flavor"` + + // IP is a regular expression to match the IPv4 address of the server. + IP string `q:"ip"` + + // This requires the client to be set to microversion 2.5 or later, unless + // the user is an admin. + // IP is a regular expression to match the IPv6 address of the server. + IP6 string `q:"ip6"` + + // Name of the server as a string; can be queried with regular expressions. + // Realize that ?name=bob returns both bob and bobb. If you need to match bob + // only, you can use a regular expression matching the syntax of the + // underlying database server implemented for Compute. + Name string `q:"name"` + + // Status is the value of the status of the server so that you can filter on + // "ACTIVE" for example. + Status string `q:"status"` + + // Host is the name of the host as a string. + Host string `q:"host"` + + // Marker is a UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Limit is an integer value for the limit of values to return. + Limit int `q:"limit"` + + // AllTenants is a bool to show all tenants. + AllTenants bool `q:"all_tenants"` + + // TenantID lists servers for a particular tenant. + // Setting "AllTenants = true" is required. + TenantID string `q:"tenant_id"` + + // This requires the client to be set to microversion 2.83 or later, unless + // the user is an admin. + // UserID lists servers for a particular user. + UserID string `q:"user_id"` + + // This requires the client to be set to microversion 2.26 or later. + // Tags filters on specific server tags. All tags must be present for the server. + Tags string `q:"tags"` + + // This requires the client to be set to microversion 2.26 or later. + // TagsAny filters on specific server tags. At least one of the tags must be present for the server. + TagsAny string `q:"tags-any"` + + // This requires the client to be set to microversion 2.26 or later. + // NotTags filters on specific server tags. All tags must be absent for the server. + NotTags string `q:"not-tags"` + + // This requires the client to be set to microversion 2.26 or later. + // NotTagsAny filters on specific server tags. At least one of the tags must be absent for the server. + NotTagsAny string `q:"not-tags-any"` + + // Display servers based on their availability zone (Admin only until microversion 2.82). + AvailabilityZone string `q:"availability_zone"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListSimple makes a request against the API to list servers accessible to you. +func ListSimple(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// List makes a request against the API to list servers details accessible to you. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]interface{}, error) +} + +// Network is used within CreateOpts to control a new server's network +// attachments. +type Network struct { + // UUID of a network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP specifies a fixed IPv4 address to be used on this network. + FixedIP string + + // Tag may contain an optional device role tag for the server's virtual + // network interface. This can be used to identify network interfaces when + // multiple networks are connected to one server. + // + // Requires microversion 2.32 through 2.36 or 2.42 or later. + Tag string +} + +// Personality is an array of files that are injected into the server at launch. +type Personality []*File + +// File is used within CreateOpts and RebuildOpts to inject a file into the +// server at launch. +// File implements the json.Marshaler interface, so when a Create or Rebuild +// operation is requested, json.Marshal will call File's MarshalJSON method. +type File struct { + // Path of the file. + Path string + + // Contents of the file. Maximum content size is 255 bytes. + Contents []byte +} + +// MarshalJSON marshals the escaped file, base64 encoding the contents. +func (f *File) MarshalJSON() ([]byte, error) { + file := struct { + Path string `json:"path"` + Contents string `json:"contents"` + }{ + Path: f.Path, + Contents: base64.StdEncoding.EncodeToString(f.Contents), + } + return json.Marshal(file) +} + +// CreateOpts specifies server creation parameters. +type CreateOpts struct { + // Name is the name to assign to the newly launched server. + Name string `json:"name" required:"true"` + + // ImageRef is the ID or full URL to the image that contains the + // server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageRef string `json:"imageRef"` + + // FlavorRef is the ID or full URL to the flavor that describes the server's specs. + FlavorRef string `json:"flavorRef"` + + // SecurityGroups lists the names of the security groups to which this server + // should belong. + SecurityGroups []string `json:"-"` + + // UserData contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you, if it isn't already. + UserData []byte `json:"-"` + + // AvailabilityZone in which to launch the server. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // Networks dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the + // tenant. + // Starting with microversion 2.37 networks can also be an "auto" or "none" + // string. + Networks interface{} `json:"-"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to the + // server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` + + // ConfigDrive enables metadata injection through a configuration drive. + ConfigDrive *bool `json:"config_drive,omitempty"` + + // AdminPass sets the root user password. If not set, a randomly-generated + // password will be created and returned in the response. + AdminPass string `json:"adminPass,omitempty"` + + // AccessIPv4 specifies an IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 specifies an IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + + // Min specifies Minimum number of servers to launch. + Min int `json:"min_count,omitempty"` + + // Max specifies Maximum number of servers to launch. + Max int `json:"max_count,omitempty"` + + // Tags allows a server to be tagged with single-word metadata. + // Requires microversion 2.52 or later. + Tags []string `json:"tags,omitempty"` +} + +// ToServerCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.UserData != nil { + var userData string + if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil { + userData = base64.StdEncoding.EncodeToString(opts.UserData) + } else { + userData = string(opts.UserData) + } + b["user_data"] = &userData + } + + if len(opts.SecurityGroups) > 0 { + securityGroups := make([]map[string]interface{}, len(opts.SecurityGroups)) + for i, groupName := range opts.SecurityGroups { + securityGroups[i] = map[string]interface{}{"name": groupName} + } + b["security_groups"] = securityGroups + } + + switch v := opts.Networks.(type) { + case []Network: + if len(v) > 0 { + networks := make([]map[string]interface{}, len(v)) + for i, net := range v { + networks[i] = make(map[string]interface{}) + if net.UUID != "" { + networks[i]["uuid"] = net.UUID + } + if net.Port != "" { + networks[i]["port"] = net.Port + } + if net.FixedIP != "" { + networks[i]["fixed_ip"] = net.FixedIP + } + if net.Tag != "" { + networks[i]["tag"] = net.Tag + } + } + b["networks"] = networks + } + case string: + if v == "auto" || v == "none" { + b["networks"] = v + } else { + return nil, fmt.Errorf(`networks must be a slice of Network struct or a string with "auto" or "none" values, current value is %q`, v) + } + } + + if opts.Min != 0 { + b["min_count"] = opts.Min + } + + if opts.Max != 0 { + b["max_count"] = opts.Max + } + + return map[string]interface{}{"server": b}, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + reqBody, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(listURL(client), reqBody, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete requests that a server previously provisioned be removed from your +// account. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ForceDelete forces the deletion of a server. +func ForceDelete(client *gophercloud.ServiceClient, id string) (r ActionResult) { + resp, err := client.Post(actionURL(client, id), map[string]interface{}{"forceDelete": ""}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get requests details on a single server, by ID. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(getURL(client, id), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// server. +type UpdateOpts struct { + // Name changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name string `json:"name,omitempty"` + + // AccessIPv4 provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "server") +} + +// Update requests that various attributes of the indicated server be changed. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToServerUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ChangeAdminPassword alters the administrator or root password for a specified +// server. +func ChangeAdminPassword(client *gophercloud.ServiceClient, id, newPassword string) (r ActionResult) { + b := map[string]interface{}{ + "changePassword": map[string]string{ + "adminPass": newPassword, + }, + } + resp, err := client.Post(actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RebootMethod describes the mechanisms by which a server reboot can be requested. +type RebootMethod string + +// These constants determine how a server should be rebooted. +// See the Reboot() function for further details. +const ( + SoftReboot RebootMethod = "SOFT" + HardReboot RebootMethod = "HARD" + OSReboot = SoftReboot + PowerCycle = HardReboot +) + +// RebootOptsBuilder allows extensions to add additional parameters to the +// reboot request. +type RebootOptsBuilder interface { + ToServerRebootMap() (map[string]interface{}, error) +} + +// RebootOpts provides options to the reboot request. +type RebootOpts struct { + // Type is the type of reboot to perform on the server. + Type RebootMethod `json:"type" required:"true"` +} + +// ToServerRebootMap builds a body for the reboot request. +func (opts RebootOpts) ToServerRebootMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "reboot") +} + +/* +Reboot requests that a given server reboot. + +Two methods exist for rebooting a server: + +HardReboot (aka PowerCycle) starts the server instance by physically cutting +power to the machine, or if a VM, terminating it at the hypervisor level. +It's done. Caput. Full stop. +Then, after a brief while, power is restored or the VM instance restarted. + +SoftReboot (aka OSReboot) simply tells the OS to restart under its own +procedure. +E.g., in Linux, asking it to enter runlevel 6, or executing +"sudo shutdown -r now", or by asking Windows to rtart the machine. +*/ +func Reboot(client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder) (r ActionResult) { + b, err := opts.ToServerRebootMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RebuildOptsBuilder allows extensions to provide additional parameters to the +// rebuild request. +type RebuildOptsBuilder interface { + ToServerRebuildMap() (map[string]interface{}, error) +} + +// RebuildOpts represents the configuration options used in a server rebuild +// operation. +type RebuildOpts struct { + // AdminPass is the server's admin password + AdminPass string `json:"adminPass,omitempty"` + + // ImageRef is the ID of the image you want your server to be provisioned on. + ImageRef string `json:"imageRef"` + + // Name to set the server to + Name string `json:"name,omitempty"` + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) + // to attach to the server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality [optional] includes files to inject into the server at launch. + // Rebuild will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` +} + +// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON +func (opts RebuildOpts) ToServerRebuildMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return map[string]interface{}{"rebuild": b}, nil +} + +// Rebuild will reprovision the server according to the configuration options +// provided in the RebuildOpts struct. +func Rebuild(client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) (r RebuildResult) { + b, err := opts.ToServerRebuildMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ResizeOptsBuilder allows extensions to add additional parameters to the +// resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]interface{}, error) +} + +// ResizeOpts represents the configuration options used to control a Resize +// operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string `json:"flavorRef" required:"true"` +} + +// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON +// request body for the Resize request. +func (opts ResizeOpts) ToServerResizeMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "resize") +} + +// Resize instructs the provider to change the flavor of the server. +// +// Note that this implies rebuilding it. +// +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in VERIFY_RESIZE state. +// While in this state, you can explore the use of the new server's +// configuration. If you like it, call ConfirmResize() to commit the resize +// permanently. Otherwise, call RevertResize() to restore the old configuration. +func Resize(client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) { + b, err := opts.ToServerResizeMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ConfirmResize confirms a previous resize operation on a server. +// See Resize() for more details. +func ConfirmResize(client *gophercloud.ServiceClient, id string) (r ActionResult) { + resp, err := client.Post(actionURL(client, id), map[string]interface{}{"confirmResize": nil}, nil, &gophercloud.RequestOpts{ + OkCodes: []int{201, 202, 204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RevertResize cancels a previous resize operation on a server. +// See Resize() for more details. +func RevertResize(client *gophercloud.ServiceClient, id string) (r ActionResult) { + resp, err := client.Post(actionURL(client, id), map[string]interface{}{"revertResize": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ResetMetadataOptsBuilder allows extensions to add additional parameters to +// the Reset request. +type ResetMetadataOptsBuilder interface { + ToMetadataResetMap() (map[string]interface{}, error) +} + +// MetadataOpts is a map that contains key-value pairs. +type MetadataOpts map[string]string + +// ToMetadataResetMap assembles a body for a Reset request based on the contents +// of a MetadataOpts. +func (opts MetadataOpts) ToMetadataResetMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ToMetadataUpdateMap assembles a body for an Update request based on the +// contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]interface{}, error) { + return map[string]interface{}{"metadata": opts}, nil +} + +// ResetMetadata will create multiple new key-value pairs for the given server +// ID. +// Note: Using this operation will erase any already-existing metadata and +// create the new metadata provided. To keep any already-existing metadata, +// use the UpdateMetadatas or UpdateMetadata function. +func ResetMetadata(client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) (r ResetMetadataResult) { + b, err := opts.ToMetadataResetMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(client *gophercloud.ServiceClient, id string) (r GetMetadataResult) { + resp, err := client.Get(metadataURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Create request. +type UpdateMetadataOptsBuilder interface { + ToMetadataUpdateMap() (map[string]interface{}, error) +} + +// UpdateMetadata updates (or creates) all the metadata specified by opts for +// the given server ID. This operation does not affect already-existing metadata +// that is not specified by opts. +func UpdateMetadata(client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToMetadataUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// MetadatumOptsBuilder allows extensions to add additional parameters to the +// Create request. +type MetadatumOptsBuilder interface { + ToMetadatumCreateMap() (map[string]interface{}, string, error) +} + +// MetadatumOpts is a map of length one that contains a key-value pair. +type MetadatumOpts map[string]string + +// ToMetadatumCreateMap assembles a body for a Create request based on the +// contents of a MetadataumOpts. +func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]interface{}, string, error) { + if len(opts) != 1 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.MetadatumOpts" + err.Info = "Must have 1 and only 1 key-value pair" + return nil, "", err + } + metadatum := map[string]interface{}{"meta": opts} + var key string + for k := range metadatum["meta"].(MetadatumOpts) { + key = k + } + return metadatum, key, nil +} + +// CreateMetadatum will create or update the key-value pair with the given key +// for the given server ID. +func CreateMetadatum(client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) (r CreateMetadatumResult) { + b, key, err := opts.ToMetadatumCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(metadatumURL(client, id, key), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Metadatum requests the key-value pair with the given key for the given +// server ID. +func Metadatum(client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) { + resp, err := client.Get(metadatumURL(client, id, key), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteMetadatum will delete the key-value pair with the given key for the +// given server ID. +func DeleteMetadatum(client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) { + resp, err := client.Delete(metadatumURL(client, id, key), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAddresses makes a request against the API to list the servers IP +// addresses. +func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager { + return pagination.NewPager(client, listAddressesURL(client, id), func(r pagination.PageResult) pagination.Page { + return AddressPage{pagination.SinglePageBase(r)} + }) +} + +// ListAddressesByNetwork makes a request against the API to list the servers IP +// addresses for the given network. +func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager { + return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), func(r pagination.PageResult) pagination.Page { + return NetworkAddressPage{pagination.SinglePageBase(r)} + }) +} + +// CreateImageOptsBuilder allows extensions to add additional parameters to the +// CreateImage request. +type CreateImageOptsBuilder interface { + ToServerCreateImageMap() (map[string]interface{}, error) +} + +// CreateImageOpts provides options to pass to the CreateImage request. +type CreateImageOpts struct { + // Name of the image/snapshot. + Name string `json:"name" required:"true"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to + // the created image. + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToServerCreateImageMap formats a CreateImageOpts structure into a request +// body. +func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "createImage") +} + +// CreateImage makes a request against the nova API to schedule an image to be +// created of the server +func CreateImage(client *gophercloud.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) { + b, err := opts.ToServerCreateImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetPassword makes a request against the nova API to get the encrypted +// administrative password. +func GetPassword(client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) { + resp, err := client.Get(passwordURL(client, serverId), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ShowConsoleOutputOptsBuilder is the interface types must satisfy in order to be +// used as ShowConsoleOutput options +type ShowConsoleOutputOptsBuilder interface { + ToServerShowConsoleOutputMap() (map[string]interface{}, error) +} + +// ShowConsoleOutputOpts satisfies the ShowConsoleOutputOptsBuilder +type ShowConsoleOutputOpts struct { + // The number of lines to fetch from the end of console log. + // All lines will be returned if this is not specified. + Length int `json:"length,omitempty"` +} + +// ToServerShowConsoleOutputMap formats a ShowConsoleOutputOpts structure into a request body. +func (opts ShowConsoleOutputOpts) ToServerShowConsoleOutputMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "os-getConsoleOutput") +} + +// ShowConsoleOutput makes a request against the nova API to get console log from the server +func ShowConsoleOutput(client *gophercloud.ServiceClient, id string, opts ShowConsoleOutputOptsBuilder) (r ShowConsoleOutputResult) { + b, err := opts.ToServerShowConsoleOutputMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go new file mode 100644 index 00000000000..2c22a3c4d1f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/results.go @@ -0,0 +1,444 @@ +package servers + +import ( + "crypto/rsa" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "path" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type serverResult struct { + gophercloud.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + var s Server + err := r.ExtractInto(&s) + return &s, err +} + +func (r serverResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "server") +} + +func ExtractServersInto(r pagination.Page, v interface{}) error { + return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers") +} + +// CreateResult is the response from a Create operation. Call its Extract +// method to interpret it as a Server. +type CreateResult struct { + serverResult +} + +// GetResult is the response from a Get operation. Call its Extract +// method to interpret it as a Server. +type GetResult struct { + serverResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Server. +type UpdateResult struct { + serverResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// RebuildResult is the response from a Rebuild operation. Call its Extract +// method to interpret it as a Server. +type RebuildResult struct { + serverResult +} + +// ActionResult represents the result of server action operations, like reboot. +// Call its ExtractErr method to determine if the action succeeded or failed. +type ActionResult struct { + gophercloud.ErrResult +} + +// CreateImageResult is the response from a CreateImage operation. Call its +// ExtractImageID method to retrieve the ID of the newly created image. +type CreateImageResult struct { + gophercloud.Result +} + +// ShowConsoleOutputResult represents the result of console output from a server +type ShowConsoleOutputResult struct { + gophercloud.Result +} + +// Extract will return the console output from a ShowConsoleOutput request. +func (r ShowConsoleOutputResult) Extract() (string, error) { + var s struct { + Output string `json:"output"` + } + + err := r.ExtractInto(&s) + return s.Output, err +} + +// GetPasswordResult represent the result of a get os-server-password operation. +// Call its ExtractPassword method to retrieve the password. +type GetPasswordResult struct { + gophercloud.Result +} + +// ExtractPassword gets the encrypted password. +// If privateKey != nil the password is decrypted with the private key. +// If privateKey == nil the encrypted password is returned and can be decrypted +// with: +// +// echo '' | base64 -D | openssl rsautl -decrypt -inkey +func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) { + var s struct { + Password string `json:"password"` + } + err := r.ExtractInto(&s) + if err == nil && privateKey != nil && s.Password != "" { + return decryptPassword(s.Password, privateKey) + } + return s.Password, err +} + +func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) { + b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword))) + + n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword)) + if err != nil { + return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err) + } + password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n]) + if err != nil { + return "", fmt.Errorf("Failed to decrypt password: %s", err) + } + + return string(password), nil +} + +// ExtractImageID gets the ID of the newly created server image from the header. +func (r CreateImageResult) ExtractImageID() (string, error) { + if r.Err != nil { + return "", r.Err + } + // Get the image id from the header + u, err := url.ParseRequestURI(r.Header.Get("Location")) + if err != nil { + return "", err + } + imageID := path.Base(u.Path) + if imageID == "." || imageID == "/" { + return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u) + } + return imageID, nil +} + +// Server represents a server/instance in the OpenStack cloud. +type Server struct { + // ID uniquely identifies this server amongst all other servers, + // including those not accessible to the current tenant. + ID string `json:"id"` + + // TenantID identifies the tenant owning this server resource. + TenantID string `json:"tenant_id"` + + // UserID uniquely identifies the user account owning the tenant. + UserID string `json:"user_id"` + + // Name contains the human-readable name for the server. + Name string `json:"name"` + + // Updated and Created contain ISO-8601 timestamps of when the state of the + // server last changed, and when it was created. + Updated time.Time `json:"updated"` + Created time.Time `json:"created"` + + // HostID is the host where the server is located in the cloud. + HostID string `json:"hostid"` + + // Status contains the current operational status of the server, + // such as IN_PROGRESS or ACTIVE. + Status string `json:"status"` + + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int `json:"progress"` + + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, + // suitable for remote access for administration. + AccessIPv4 string `json:"accessIPv4"` + AccessIPv6 string `json:"accessIPv6"` + + // Image refers to a JSON object, which itself indicates the OS image used to + // deploy the server. + Image map[string]interface{} `json:"-"` + + // Flavor refers to a JSON object, which itself indicates the hardware + // configuration of the deployed server. + Flavor map[string]interface{} `json:"flavor"` + + // Addresses includes a list of all IP addresses assigned to the server, + // keyed by pool. + Addresses map[string]interface{} `json:"addresses"` + + // Metadata includes a list of all user-specified key-value pairs attached + // to the server. + Metadata map[string]string `json:"metadata"` + + // Links includes HTTP references to the itself, useful for passing along to + // other APIs that might want a server reference. + Links []interface{} `json:"links"` + + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name"` + + // AdminPass will generally be empty (""). However, it will contain the + // administrative password chosen when provisioning a new server without a + // set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass"` + + // SecurityGroups includes the security groups that this instance has applied + // to it. + SecurityGroups []map[string]interface{} `json:"security_groups"` + + // AttachedVolumes includes the volume attachments of this instance + AttachedVolumes []AttachedVolume `json:"os-extended-volumes:volumes_attached"` + + // Fault contains failure information about a server. + Fault Fault `json:"fault"` + + // Tags is a slice/list of string tags in a server. + // The requires microversion 2.26 or later. + Tags *[]string `json:"tags"` + + // ServerGroups is a slice of strings containing the UUIDs of the + // server groups to which the server belongs. Currently this can + // contain at most one entry. + // New in microversion 2.71 + ServerGroups *[]string `json:"server_groups"` +} + +type AttachedVolume struct { + ID string `json:"id"` +} + +type Fault struct { + Code int `json:"code"` + Created time.Time `json:"created"` + Details string `json:"details"` + Message string `json:"message"` +} + +func (r *Server) UnmarshalJSON(b []byte) error { + type tmp Server + var s struct { + tmp + Image interface{} `json:"image"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Server(s.tmp) + + switch t := s.Image.(type) { + case map[string]interface{}: + r.Image = t + case string: + switch t { + case "": + r.Image = nil + } + } + + return err +} + +// ServerPage abstracts the raw results of making a List() request against +// the API. As OpenStack extensions may freely alter the response bodies of +// structures returned to the client, you may only safely access the data +// provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r ServerPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + s, err := ExtractServers(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r ServerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"servers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, +// producing a slice of Server entities. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s []Server + err := ExtractServersInto(r, &s) + return s, err +} + +// MetadataResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type MetadataResult struct { + gophercloud.Result +} + +// GetMetadataResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult contains the result of a Reset operation. Call its +// Extract method to interpret it as a map[string]interface. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult contains the result of an Update operation. Call its +// Extract method to interpret it as a map[string]interface. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single +// key-value pair. +type MetadatumResult struct { + gophercloud.Result +} + +// GetMetadatumResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult contains the result of a Create operation. Call its +// Extract method to interpret it as a map[string]interface. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult contains the result of a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DeleteMetadatumResult struct { + gophercloud.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + var s struct { + Metadata map[string]string `json:"metadata"` + } + err := r.ExtractInto(&s) + return s.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + var s struct { + Metadatum map[string]string `json:"meta"` + } + err := r.ExtractInto(&s) + return s.Metadatum, err +} + +// Address represents an IP address. +type Address struct { + Version int `json:"version"` + Address string `json:"addr"` +} + +// AddressPage abstracts the raw results of making a ListAddresses() request +// against the API. As OpenStack extensions may freely alter the response bodies +// of structures returned to the client, you may only safely access the data +// provided through the ExtractAddresses call. +type AddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if an AddressPage contains no networks. +func (r AddressPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + addresses, err := ExtractAddresses(r) + return len(addresses) == 0, err +} + +// ExtractAddresses interprets the results of a single page from a +// ListAddresses() call, producing a map of addresses. +func ExtractAddresses(r pagination.Page) (map[string][]Address, error) { + var s struct { + Addresses map[string][]Address `json:"addresses"` + } + err := (r.(AddressPage)).ExtractInto(&s) + return s.Addresses, err +} + +// NetworkAddressPage abstracts the raw results of making a +// ListAddressesByNetwork() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures +// returned to the client, you may only safely access the data provided through +// the ExtractAddresses call. +type NetworkAddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a NetworkAddressPage contains no addresses. +func (r NetworkAddressPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + addresses, err := ExtractNetworkAddresses(r) + return len(addresses) == 0, err +} + +// ExtractNetworkAddresses interprets the results of a single page from a +// ListAddressesByNetwork() call, producing a slice of addresses. +func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) { + var s map[string][]Address + err := (r.(NetworkAddressPage)).ExtractInto(&s) + if err != nil { + return nil, err + } + + var key string + for k := range s { + key = k + } + + return s[key], err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go new file mode 100644 index 00000000000..e892e8d9259 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/urls.go @@ -0,0 +1,51 @@ +package servers + +import "github.com/gophercloud/gophercloud" + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *gophercloud.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +func metadatumURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("servers", id, "metadata", key) +} + +func metadataURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "metadata") +} + +func listAddressesURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "ips") +} + +func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string { + return client.ServiceURL("servers", id, "ips", network) +} + +func passwordURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "os-server-password") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go new file mode 100644 index 00000000000..cadef054506 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/servers/util.go @@ -0,0 +1,21 @@ +package servers + +import "github.com/gophercloud/gophercloud" + +// WaitForStatus will continually poll a server until it successfully +// transitions to a specified status. It will do this for at most the number +// of seconds specified. +func WaitForStatus(c *gophercloud.ServiceClient, id, status string, secs int) error { + return gophercloud.WaitFor(secs, func() (bool, error) { + current, err := Get(c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/doc.go new file mode 100644 index 00000000000..af4bd512bf5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/doc.go @@ -0,0 +1,14 @@ +/* +Package openstack contains resources for the individual OpenStack projects +supported in Gophercloud. It also includes functions to authenticate to an +OpenStack cloud and for provisioning various service-level clients. + +Example of Creating a Service Client + + ao, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(ao) + client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +*/ +package openstack diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go b/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go new file mode 100644 index 00000000000..509700790ef --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/endpoint_location.go @@ -0,0 +1,111 @@ +package openstack + +import ( + "github.com/gophercloud/gophercloud" + tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" +) + +/* +V2EndpointURL discovers the endpoint URL for a specific service from a +ServiceCatalog acquired during the v2 identity service. + +The specified EndpointOpts are used to identify a unique, unambiguous endpoint +to return. It's an error both when multiple endpoints match the provided +criteria and when none do. The minimum that can be specified is a Type, but you +will also often need to specify a Name and/or a Region depending on what's +available on your OpenStack deployment. +*/ +func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. + var endpoints = make([]tokens2.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Region == "" || endpoint.Region == opts.Region { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // If multiple endpoints were found, use the first result + // and disregard the other endpoints. + // + // This behavior matches the Python library. See GH-1764. + if len(endpoints) > 1 { + endpoints = endpoints[0:1] + } + + // Extract the appropriate URL from the matching Endpoint. + for _, endpoint := range endpoints { + switch opts.Availability { + case gophercloud.AvailabilityPublic: + return gophercloud.NormalizeURL(endpoint.PublicURL), nil + case gophercloud.AvailabilityInternal: + return gophercloud.NormalizeURL(endpoint.InternalURL), nil + case gophercloud.AvailabilityAdmin: + return gophercloud.NormalizeURL(endpoint.AdminURL), nil + default: + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} + +/* +V3EndpointURL discovers the endpoint URL for a specific service from a Catalog +acquired during the v3 identity service. + +The specified EndpointOpts are used to identify a unique, unambiguous endpoint +to return. It's an error both when multiple endpoints match the provided +criteria and when none do. The minimum that can be specified is a Type, but you +will also often need to specify a Name and/or a Region depending on what's +available on your OpenStack deployment. +*/ +func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Interface, + // Name if provided, and Region if provided. + var endpoints = make([]tokens3.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Availability != gophercloud.AvailabilityAdmin && + opts.Availability != gophercloud.AvailabilityPublic && + opts.Availability != gophercloud.AvailabilityInternal { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + if (opts.Availability == gophercloud.Availability(endpoint.Interface)) && + (opts.Region == "" || endpoint.Region == opts.Region || endpoint.RegionID == opts.Region) { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // If multiple endpoints were found, use the first result + // and disregard the other endpoints. + // + // This behavior matches the Python library. See GH-1764. + if len(endpoints) > 1 { + endpoints = endpoints[0:1] + } + + // Extract the URL from the matching Endpoint. + for _, endpoint := range endpoints { + return gophercloud.NormalizeURL(endpoint.URL), nil + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/errors.go b/vendor/github.com/gophercloud/gophercloud/openstack/errors.go new file mode 100644 index 00000000000..cba6ae5f00c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/errors.go @@ -0,0 +1,47 @@ +package openstack + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" +) + +// ErrEndpointNotFound is the error when no suitable endpoint can be found +// in the user's catalog +type ErrEndpointNotFound struct{ gophercloud.BaseError } + +func (e ErrEndpointNotFound) Error() string { + return "No suitable endpoint could be found in the service catalog." +} + +// ErrInvalidAvailabilityProvided is the error when an invalid endpoint +// availability is provided +type ErrInvalidAvailabilityProvided struct{ gophercloud.ErrInvalidInput } + +func (e ErrInvalidAvailabilityProvided) Error() string { + return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value) +} + +// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not +// found +type ErrNoAuthURL struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoAuthURL) Error() string { + return "Environment variable OS_AUTH_URL needs to be set." +} + +// ErrNoUsername is the error when the OS_USERNAME environment variable is not +// found +type ErrNoUsername struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoUsername) Error() string { + return "Environment variable OS_USERNAME needs to be set." +} + +// ErrNoPassword is the error when the OS_PASSWORD environment variable is not +// found +type ErrNoPassword struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoPassword) Error() string { + return "Environment variable OS_PASSWORD needs to be set." +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go new file mode 100644 index 00000000000..348dd208396 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/doc.go @@ -0,0 +1,65 @@ +/* +Package tenants provides information and interaction with the +tenants API resource for the OpenStack Identity service. + +See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants +for more information. + +Example to List Tenants + + listOpts := &tenants.ListOpts{ + Limit: 2, + } + + allPages, err := tenants.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allTenants, err := tenants.ExtractTenants(allPages) + if err != nil { + panic(err) + } + + for _, tenant := range allTenants { + fmt.Printf("%+v\n", tenant) + } + +Example to Create a Tenant + + createOpts := tenants.CreateOpts{ + Name: "tenant_name", + Description: "this is a tenant", + Enabled: gophercloud.Enabled, + } + + tenant, err := tenants.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Tenant + + tenantID := "e6db6ed6277c461a853458589063b295" + + updateOpts := tenants.UpdateOpts{ + Description: "this is a new description", + Enabled: gophercloud.Disabled, + } + + tenant, err := tenants.Update(identityClient, tenantID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Tenant + + tenantID := "e6db6ed6277c461a853458589063b295" + + err := tenants.Delete(identitYClient, tenantID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package tenants diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go new file mode 100644 index 00000000000..f16df38e5ec --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/requests.go @@ -0,0 +1,120 @@ +package tenants + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOpts filters the Tenants that are returned by the List call. +type ListOpts struct { + // Marker is the ID of the last Tenant on the previous page. + Marker string `q:"marker"` + + // Limit specifies the page size. + Limit int `q:"limit"` +} + +// List enumerates the Tenants to which the current token has access. +func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager { + url := listURL(client) + if opts != nil { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return pagination.Pager{Err: err} + } + url += q.String() + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOpts represents the options needed when creating new tenant. +type CreateOpts struct { + // Name is the name of the tenant. + Name string `json:"name" required:"true"` + + // Description is the description of the tenant. + Description string `json:"description,omitempty"` + + // Enabled sets the tenant status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` +} + +// CreateOptsBuilder enables extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToTenantCreateMap() (map[string]interface{}, error) +} + +// ToTenantCreateMap assembles a request body based on the contents of +// a CreateOpts. +func (opts CreateOpts) ToTenantCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "tenant") +} + +// Create is the operation responsible for creating new tenant. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTenantCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get requests details on a single tenant by ID. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToTenantUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// tenant. +type UpdateOpts struct { + // Name is the name of the tenant. + Name string `json:"name,omitempty"` + + // Description is the description of the tenant. + Description *string `json:"description,omitempty"` + + // Enabled sets the tenant status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` +} + +// ToTenantUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToTenantUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "tenant") +} + +// Update is the operation responsible for updating exist tenants by their TenantID. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToTenantUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(updateURL(client, id), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete is the operation responsible for permanently deleting a tenant. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go new file mode 100644 index 00000000000..2daff984031 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/results.go @@ -0,0 +1,95 @@ +package tenants + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Tenant is a grouping of users in the identity service. +type Tenant struct { + // ID is a unique identifier for this tenant. + ID string `json:"id"` + + // Name is a friendlier user-facing name for this tenant. + Name string `json:"name"` + + // Description is a human-readable explanation of this Tenant's purpose. + Description string `json:"description"` + + // Enabled indicates whether or not a tenant is active. + Enabled bool `json:"enabled"` +} + +// TenantPage is a single page of Tenant results. +type TenantPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (r TenantPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + tenants, err := ExtractTenants(r) + return len(tenants) == 0, err +} + +// NextPageURL extracts the "next" link from the tenants_links section of the result. +func (r TenantPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"tenants_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractTenants returns a slice of Tenants contained in a single page of +// results. +func ExtractTenants(r pagination.Page) ([]Tenant, error) { + var s struct { + Tenants []Tenant `json:"tenants"` + } + err := (r.(TenantPage)).ExtractInto(&s) + return s.Tenants, err +} + +type tenantResult struct { + gophercloud.Result +} + +// Extract interprets any tenantResults as a Tenant. +func (r tenantResult) Extract() (*Tenant, error) { + var s struct { + Tenant *Tenant `json:"tenant"` + } + err := r.ExtractInto(&s) + return s.Tenant, err +} + +// GetResult is the response from a Get request. Call its Extract method to +// interpret it as a Tenant. +type GetResult struct { + tenantResult +} + +// CreateResult is the response from a Create request. Call its Extract method +// to interpret it as a Tenant. +type CreateResult struct { + tenantResult +} + +// DeleteResult is the response from a Get request. Call its ExtractErr method +// to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult is the response from a Update request. Call its Extract method +// to interpret it as a Tenant. +type UpdateResult struct { + tenantResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go new file mode 100644 index 00000000000..0f026690790 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tenants/urls.go @@ -0,0 +1,23 @@ +package tenants + +import "github.com/gophercloud/gophercloud" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func getURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func deleteURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func updateURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go new file mode 100644 index 00000000000..5375eea8726 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/doc.go @@ -0,0 +1,46 @@ +/* +Package tokens provides information and interaction with the token API +resource for the OpenStack Identity service. + +For more information, see: +http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 + +Example to Create an Unscoped Token from a Password + + authOpts := gophercloud.AuthOptions{ + Username: "user", + Password: "pass" + } + + token, err := tokens.Create(identityClient, authOpts).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Tenant ID and Password + + authOpts := gophercloud.AuthOptions{ + Username: "user", + Password: "password", + TenantID: "fc394f2ab2df4114bde39905f800dc57" + } + + token, err := tokens.Create(identityClient, authOpts).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Tenant Name and Password + + authOpts := gophercloud.AuthOptions{ + Username: "user", + Password: "password", + TenantName: "tenantname" + } + + token, err := tokens.Create(identityClient, authOpts).ExtractToken() + if err != nil { + panic(err) + } +*/ +package tokens diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go new file mode 100644 index 00000000000..84f16c3fc28 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/requests.go @@ -0,0 +1,105 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +// PasswordCredentialsV2 represents the required options to authenticate +// with a username and password. +type PasswordCredentialsV2 struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` +} + +// TokenCredentialsV2 represents the required options to authenticate +// with a token. +type TokenCredentialsV2 struct { + ID string `json:"id,omitempty" required:"true"` +} + +// AuthOptionsV2 wraps a gophercloud AuthOptions in order to adhere to the +// AuthOptionsBuilder interface. +type AuthOptionsV2 struct { + PasswordCredentials *PasswordCredentialsV2 `json:"passwordCredentials,omitempty" xor:"TokenCredentials"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // Some providers allow you to specify a TenantName instead of the TenantId. + // Some require both. Your provider's authentication policies will determine + // how these fields influence authentication. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // TokenCredentials allows users to authenticate (possibly as another user) + // with an authentication token ID. + TokenCredentials *TokenCredentialsV2 `json:"token,omitempty" xor:"PasswordCredentials"` +} + +// AuthOptionsBuilder allows extensions to add additional parameters to the +// token create request. +type AuthOptionsBuilder interface { + // ToTokenCreateMap assembles the Create request body, returning an error + // if parameters are missing or inconsistent. + ToTokenV2CreateMap() (map[string]interface{}, error) +} + +// AuthOptions are the valid options for Openstack Identity v2 authentication. +// For field descriptions, see gophercloud.AuthOptions. +type AuthOptions struct { + IdentityEndpoint string `json:"-"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + AllowReauth bool `json:"-"` + TokenID string +} + +// ToTokenV2CreateMap builds a token request body from the given AuthOptions. +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { + v2Opts := AuthOptionsV2{ + TenantID: opts.TenantID, + TenantName: opts.TenantName, + } + + if opts.Password != "" { + v2Opts.PasswordCredentials = &PasswordCredentialsV2{ + Username: opts.Username, + Password: opts.Password, + } + } else { + v2Opts.TokenCredentials = &TokenCredentialsV2{ + ID: opts.TokenID, + } + } + + b, err := gophercloud.BuildRequestBody(v2Opts, "auth") + if err != nil { + return nil, err + } + return b, nil +} + +// Create authenticates to the identity service and attempts to acquire a Token. +// Generally, rather than interact with this call directly, end users should +// call openstack.AuthenticatedClient(), which abstracts all of the gory details +// about navigating service catalogs and such. +func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) (r CreateResult) { + b, err := auth.ToTokenV2CreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(CreateURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + OmitHeaders: []string{"X-Auth-Token"}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get validates and retrieves information for user's token. +func Get(client *gophercloud.ServiceClient, token string) (r GetResult) { + resp, err := client.Get(GetURL(client, token), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go new file mode 100644 index 00000000000..ee5da37f465 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/results.go @@ -0,0 +1,174 @@ +package tokens + +import ( + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v2/tenants" +) + +// Token provides only the most basic information related to an authentication +// token. +type Token struct { + // ID provides the primary means of identifying a user to the OpenStack API. + // OpenStack defines this field as an opaque value, so do not depend on its + // content. It is safe, however, to compare for equality. + ID string + + // ExpiresAt provides a timestamp in ISO 8601 format, indicating when the + // authentication token becomes invalid. After this point in time, future + // API requests made using this authentication token will respond with + // errors. Either the caller will need to reauthenticate manually, or more + // preferably, the caller should exploit automatic re-authentication. + // See the AuthOptions structure for more details. + ExpiresAt time.Time + + // Tenant provides information about the tenant to which this token grants + // access. + Tenant tenants.Tenant +} + +// Role is a role for a user. +type Role struct { + Name string `json:"name"` +} + +// User is an OpenStack user. +type User struct { + ID string `json:"id"` + Name string `json:"name"` + UserName string `json:"username"` + Roles []Role `json:"roles"` +} + +// Endpoint represents a single API endpoint offered by a service. +// It provides the public and internal URLs, if supported, along with a region +// specifier, again if provided. +// +// The significance of the Region field will depend upon your provider. +// +// In addition, the interface offered by the service will have version +// information associated with it through the VersionId, VersionInfo, and +// VersionList fields, if provided or supported. +// +// In all cases, fields which aren't supported by the provider and service +// combined will assume a zero-value (""). +type Endpoint struct { + TenantID string `json:"tenantId"` + PublicURL string `json:"publicURL"` + InternalURL string `json:"internalURL"` + AdminURL string `json:"adminURL"` + Region string `json:"region"` + VersionID string `json:"versionId"` + VersionInfo string `json:"versionInfo"` + VersionList string `json:"versionList"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V2 service +// catalog listing. +// +// Each class of service, such as cloud DNS or block storage services, will have +// a single CatalogEntry representing it. +// +// Note: when looking for the desired service, try, whenever possible, to key +// off the type field. Otherwise, you'll tie the representation of the service +// to a specific provider. +type CatalogEntry struct { + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + + // Type will contain a type string if OpenStack defines a type for the + // service. Otherwise, for provider-specific services, the provider may assign + // their own type strings. + Type string `json:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that + // may exist for the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, +// successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry +} + +// CreateResult is the response from a Create request. Use ExtractToken() to +// interpret it as a Token, or ExtractServiceCatalog() to interpret it as a +// service catalog. +type CreateResult struct { + gophercloud.Result +} + +// GetResult is the deferred response from a Get call, which is the same with a +// Created token. Use ExtractUser() to interpret it as a User. +type GetResult struct { + CreateResult +} + +// ExtractToken returns the just-created Token from a CreateResult. +func (r CreateResult) ExtractToken() (*Token, error) { + var s struct { + Access struct { + Token struct { + Expires string `json:"expires"` + ID string `json:"id"` + Tenant tenants.Tenant `json:"tenant"` + } `json:"token"` + } `json:"access"` + } + + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + expiresTs, err := time.Parse(gophercloud.RFC3339Milli, s.Access.Token.Expires) + if err != nil { + return nil, err + } + + return &Token{ + ID: s.Access.Token.ID, + ExpiresAt: expiresTs, + Tenant: s.Access.Token.Tenant, + }, nil +} + +// ExtractTokenID implements the gophercloud.AuthResult interface. The returned +// string is the same as the ID field of the Token struct returned from +// ExtractToken(). +func (r CreateResult) ExtractTokenID() (string, error) { + var s struct { + Access struct { + Token struct { + ID string `json:"id"` + } `json:"token"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return s.Access.Token.ID, err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along +// with the user's Token. +func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s struct { + Access struct { + Entries []CatalogEntry `json:"serviceCatalog"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return &ServiceCatalog{Entries: s.Access.Entries}, err +} + +// ExtractUser returns the User from a GetResult. +func (r GetResult) ExtractUser() (*User, error) { + var s struct { + Access struct { + User User `json:"user"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return &s.Access.User, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go new file mode 100644 index 00000000000..ee0a28f2004 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v2/tokens/urls.go @@ -0,0 +1,13 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +// CreateURL generates the URL used to create new Tokens. +func CreateURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tokens") +} + +// GetURL generates the URL used to Validate Tokens. +func GetURL(client *gophercloud.ServiceClient, token string) string { + return client.ServiceURL("tokens", token) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/doc.go new file mode 100644 index 00000000000..a30d0faf3a9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/doc.go @@ -0,0 +1,40 @@ +/* +Package tokens provides information and interaction with the EC2 token API +resource for the OpenStack Identity service. + +For more information, see: +https://docs.openstack.org/api-ref/identity/v2-ext/ + +Example to Create a Token From an EC2 access and secret keys + + var authOptions tokens.AuthOptionsBuilder + authOptions = &ec2tokens.AuthOptions{ + Access: "a7f1e798b7c2417cba4a02de97dc3cdc", + Secret: "18f4f6761ada4e3795fa5273c30349b9", + } + + token, err := ec2tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to auth a client using EC2 access and secret keys + + client, err := openstack.NewClient("http://localhost:5000/v3") + if err != nil { + panic(err) + } + + var authOptions tokens.AuthOptionsBuilder + authOptions = &ec2tokens.AuthOptions{ + Access: "a7f1e798b7c2417cba4a02de97dc3cdc", + Secret: "18f4f6761ada4e3795fa5273c30349b9", + AllowReauth: true, + } + + err = openstack.AuthenticateV3(client, authOptions, gophercloud.EndpointOpts{}) + if err != nil { + panic(err) + } +*/ +package ec2tokens diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/requests.go new file mode 100644 index 00000000000..32ba0e621d9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/requests.go @@ -0,0 +1,377 @@ +package ec2tokens + +import ( + "crypto/hmac" + "crypto/sha1" + "crypto/sha256" + "encoding/hex" + "fmt" + "math/rand" + "net/url" + "sort" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" +) + +const ( + // EC2CredentialsAwsRequestV4 is a constant, used to generate AWS + // Credential V4. + EC2CredentialsAwsRequestV4 = "aws4_request" + // EC2CredentialsHmacSha1V2 is a HMAC SHA1 signature method. Used to + // generate AWS Credential V2. + EC2CredentialsHmacSha1V2 = "HmacSHA1" + // EC2CredentialsHmacSha256V2 is a HMAC SHA256 signature method. Used + // to generate AWS Credential V2. + EC2CredentialsHmacSha256V2 = "HmacSHA256" + // EC2CredentialsAwsHmacV4 is an AWS signature V4 signing method. + // More details: + // https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html + EC2CredentialsAwsHmacV4 = "AWS4-HMAC-SHA256" + // EC2CredentialsTimestampFormatV4 is an AWS signature V4 timestamp + // format. + EC2CredentialsTimestampFormatV4 = "20060102T150405Z" + // EC2CredentialsDateFormatV4 is an AWS signature V4 date format. + EC2CredentialsDateFormatV4 = "20060102" +) + +// AuthOptions represents options for authenticating a user using EC2 credentials. +type AuthOptions struct { + // Access is the EC2 Credential Access ID. + Access string `json:"access" required:"true"` + // Secret is the EC2 Credential Secret, used to calculate signature. + // Not used, when a Signature is is. + Secret string `json:"-"` + // Host is a HTTP request Host header. Used to calculate an AWS + // signature V2. For signature V4 set the Host inside Headers map. + // Optional. + Host string `json:"host"` + // Path is a HTTP request path. Optional. + Path string `json:"path"` + // Verb is a HTTP request method. Optional. + Verb string `json:"verb"` + // Headers is a map of HTTP request headers. Optional. + Headers map[string]string `json:"headers"` + // Region is a region name to calculate an AWS signature V4. Optional. + Region string `json:"-"` + // Service is a service name to calculate an AWS signature V4. Optional. + Service string `json:"-"` + // Params is a map of GET method parameters. Optional. + Params map[string]string `json:"params"` + // AllowReauth allows Gophercloud to re-authenticate automatically + // if/when your token expires. + AllowReauth bool `json:"-"` + // Signature can be either a []byte (encoded to base64 automatically) or + // a string. You can set the singature explicitly, when you already know + // it. In this case default Params won't be automatically set. Optional. + Signature interface{} `json:"signature"` + // BodyHash is a HTTP request body sha256 hash. When nil and Signature + // is not set, a random hash is generated. Optional. + BodyHash *string `json:"body_hash"` + // Timestamp is a timestamp to calculate a V4 signature. Optional. + Timestamp *time.Time `json:"-"` + // Token is a []byte string (encoded to base64 automatically) which was + // signed by an EC2 secret key. Used by S3 tokens for validation only. + // Token must be set with a Signature. If a Signature is not provided, + // a Token will be generated automatically along with a Signature. + Token []byte `json:"token,omitempty"` +} + +// EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string +// for an AWS signature V2. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L133 +func EC2CredentialsBuildCanonicalQueryStringV2(params map[string]string) string { + var keys []string + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + var pairs []string + for _, k := range keys { + pairs = append(pairs, fmt.Sprintf("%s=%s", k, url.QueryEscape(params[k]))) + } + + return strings.Join(pairs, "&") +} + +// EC2CredentialsBuildStringToSignV2 builds a string to sign an AWS signature +// V2. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L148 +func EC2CredentialsBuildStringToSignV2(opts AuthOptions) []byte { + stringToSign := strings.Join([]string{ + opts.Verb, + opts.Host, + opts.Path, + }, "\n") + + return []byte(strings.Join([]string{ + stringToSign, + EC2CredentialsBuildCanonicalQueryStringV2(opts.Params), + }, "\n")) +} + +// EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string +// for an AWS signature V4. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L244 +func EC2CredentialsBuildCanonicalQueryStringV4(verb string, params map[string]string) string { + if verb == "POST" { + return "" + } + return EC2CredentialsBuildCanonicalQueryStringV2(params) +} + +// EC2CredentialsBuildCanonicalHeadersV4 builds a canonical string based on +// "headers" map and "signedHeaders" string parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L216 +func EC2CredentialsBuildCanonicalHeadersV4(headers map[string]string, signedHeaders string) string { + headersLower := make(map[string]string, len(headers)) + for k, v := range headers { + headersLower[strings.ToLower(k)] = v + } + + var headersList []string + for _, h := range strings.Split(signedHeaders, ";") { + if v, ok := headersLower[h]; ok { + headersList = append(headersList, h+":"+v) + } + } + + return strings.Join(headersList, "\n") + "\n" +} + +// EC2CredentialsBuildSignatureKeyV4 builds a HMAC 256 signature key based on +// input parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L169 +func EC2CredentialsBuildSignatureKeyV4(secret, region, service string, date time.Time) []byte { + kDate := sumHMAC256([]byte("AWS4"+secret), []byte(date.Format(EC2CredentialsDateFormatV4))) + kRegion := sumHMAC256(kDate, []byte(region)) + kService := sumHMAC256(kRegion, []byte(service)) + return sumHMAC256(kService, []byte(EC2CredentialsAwsRequestV4)) +} + +// EC2CredentialsBuildStringToSignV4 builds an AWS v4 signature string to sign +// based on input parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L251 +func EC2CredentialsBuildStringToSignV4(opts AuthOptions, signedHeaders string, bodyHash string, date time.Time) []byte { + scope := strings.Join([]string{ + date.Format(EC2CredentialsDateFormatV4), + opts.Region, + opts.Service, + EC2CredentialsAwsRequestV4, + }, "/") + + canonicalRequest := strings.Join([]string{ + opts.Verb, + opts.Path, + EC2CredentialsBuildCanonicalQueryStringV4(opts.Verb, opts.Params), + EC2CredentialsBuildCanonicalHeadersV4(opts.Headers, signedHeaders), + signedHeaders, + bodyHash, + }, "\n") + hash := sha256.Sum256([]byte(canonicalRequest)) + + return []byte(strings.Join([]string{ + EC2CredentialsAwsHmacV4, + date.Format(EC2CredentialsTimestampFormatV4), + scope, + hex.EncodeToString(hash[:]), + }, "\n")) +} + +// EC2CredentialsBuildSignatureV4 builds an AWS v4 signature based on input +// parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L285..L286 +func EC2CredentialsBuildSignatureV4(key []byte, stringToSign []byte) string { + return hex.EncodeToString(sumHMAC256(key, stringToSign)) +} + +// EC2CredentialsBuildAuthorizationHeaderV4 builds an AWS v4 Authorization +// header based on auth parameters, date and signature +func EC2CredentialsBuildAuthorizationHeaderV4(opts AuthOptions, signedHeaders string, signature string, date time.Time) string { + return fmt.Sprintf("%s Credential=%s/%s/%s/%s/%s, SignedHeaders=%s, Signature=%s", + EC2CredentialsAwsHmacV4, + opts.Access, + date.Format(EC2CredentialsDateFormatV4), + opts.Region, + opts.Service, + EC2CredentialsAwsRequestV4, + signedHeaders, + signature) +} + +// ToTokenV3ScopeMap is a dummy method to satisfy tokens.AuthOptionsBuilder +// interface. +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + return nil, nil +} + +// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v3 tokens package. +func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) { + return nil, nil +} + +// CanReauth is a method method to satisfy tokens.AuthOptionsBuilder interface +func (opts *AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +// ToTokenV3CreateMap formats an AuthOptions into a create request. +func (opts *AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "credentials") + if err != nil { + return nil, err + } + + if opts.Signature != nil { + return b, nil + } + + // calculate signature, when it is not set + c, _ := b["credentials"].(map[string]interface{}) + h := interfaceToMap(c, "headers") + p := interfaceToMap(c, "params") + + // detect and process a signature v2 + if v, ok := p["SignatureVersion"]; ok && v == "2" { + if _, ok := c["body_hash"]; ok { + delete(c, "body_hash") + } + if _, ok := c["headers"]; ok { + delete(c, "headers") + } + if v, ok := p["SignatureMethod"]; ok { + // params is a map of strings + strToSign := EC2CredentialsBuildStringToSignV2(*opts) + switch v { + case EC2CredentialsHmacSha1V2: + // keystone uses this method only when HmacSHA256 is not available on the server side + // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L151..L156 + c["signature"] = sumHMAC1([]byte(opts.Secret), strToSign) + return b, nil + case EC2CredentialsHmacSha256V2: + c["signature"] = sumHMAC256([]byte(opts.Secret), strToSign) + return b, nil + } + return nil, fmt.Errorf("unsupported signature method: %s", v) + } + return nil, fmt.Errorf("signature method must be provided") + } else if ok { + return nil, fmt.Errorf("unsupported signature version: %s", v) + } + + // it is not a signature v2, but a signature v4 + date := time.Now().UTC() + if opts.Timestamp != nil { + date = *opts.Timestamp + } + if v, _ := c["body_hash"]; v == nil { + // when body_hash is not set, generate a random one + c["body_hash"] = randomBodyHash() + } + + signedHeaders, _ := h["X-Amz-SignedHeaders"] + + stringToSign := EC2CredentialsBuildStringToSignV4(*opts, signedHeaders, c["body_hash"].(string), date) + key := EC2CredentialsBuildSignatureKeyV4(opts.Secret, opts.Region, opts.Service, date) + c["signature"] = EC2CredentialsBuildSignatureV4(key, stringToSign) + h["X-Amz-Date"] = date.Format(EC2CredentialsTimestampFormatV4) + h["Authorization"] = EC2CredentialsBuildAuthorizationHeaderV4(*opts, signedHeaders, c["signature"].(string), date) + + // token is only used for S3 tokens validation and will be removed when using EC2 validation + c["token"] = stringToSign + + return b, nil +} + +// Create authenticates and either generates a new token from EC2 credentials +func Create(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { + b, err := opts.ToTokenV3CreateMap(nil) + if err != nil { + r.Err = err + return + } + + // delete "token" element, since it is used in s3tokens + deleteBodyElements(b, "token") + + resp, err := c.Post(ec2tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"X-Auth-Token": ""}, + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ValidateS3Token authenticates an S3 request using EC2 credentials. Doesn't +// generate a new token ID, but returns a tokens.CreateResult. +func ValidateS3Token(c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { + b, err := opts.ToTokenV3CreateMap(nil) + if err != nil { + r.Err = err + return + } + + // delete unused element, since it is used in ec2tokens only + deleteBodyElements(b, "body_hash", "headers", "host", "params", "path", "verb") + + resp, err := c.Post(s3tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"X-Auth-Token": ""}, + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// The following are small helper functions used to help build the signature. + +// sumHMAC1 is a func to implement the HMAC SHA1 signature method. +func sumHMAC1(key []byte, data []byte) []byte { + hash := hmac.New(sha1.New, key) + hash.Write(data) + return hash.Sum(nil) +} + +// sumHMAC256 is a func to implement the HMAC SHA256 signature method. +func sumHMAC256(key []byte, data []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(data) + return hash.Sum(nil) +} + +// randomBodyHash is a func to generate a random sha256 hexdigest. +func randomBodyHash() string { + h := make([]byte, 64) + rand.Read(h) + return hex.EncodeToString(h) +} + +// interfaceToMap is a func used to represent a "credentials" map element as a +// "map[string]string" +func interfaceToMap(c map[string]interface{}, key string) map[string]string { + // convert map[string]interface{} to map[string]string + m := make(map[string]string) + if v, _ := c[key].(map[string]interface{}); v != nil { + for k, v := range v { + m[k] = v.(string) + } + } + + c[key] = m + + return m +} + +// deleteBodyElements deletes map body elements +func deleteBodyElements(b map[string]interface{}, elements ...string) { + if c, ok := b["credentials"].(map[string]interface{}); ok { + for _, k := range elements { + if _, ok := c[k]; ok { + delete(c, k) + } + } + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/urls.go new file mode 100644 index 00000000000..84b33b282ea --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens/urls.go @@ -0,0 +1,11 @@ +package ec2tokens + +import "github.com/gophercloud/gophercloud" + +func ec2tokensURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ec2tokens") +} + +func s3tokensURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("s3tokens") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/doc.go new file mode 100644 index 00000000000..4294ef6c898 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/doc.go @@ -0,0 +1,122 @@ +/* +Package oauth1 enables management of OpenStack OAuth1 tokens and Authentication. + +Example to Create an OAuth1 Consumer + + createConsumerOpts := oauth1.CreateConsumerOpts{ + Description: "My consumer", + } + consumer, err := oauth1.CreateConsumer(identityClient, createConsumerOpts).Extract() + if err != nil { + panic(err) + } + + // NOTE: Consumer secret is available only on create response + fmt.Printf("Consumer: %+v\n", consumer) + +Example to Request an unauthorized OAuth1 token + + requestTokenOpts := oauth1.RequestTokenOpts{ + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + OAuthSignatureMethod: oauth1.HMACSHA1, + RequestedProjectID: projectID, + } + requestToken, err := oauth1.RequestToken(identityClient, requestTokenOpts).Extract() + if err != nil { + panic(err) + } + + // NOTE: Request token secret is available only on request response + fmt.Printf("Request token: %+v\n", requestToken) + +Example to Authorize an unauthorized OAuth1 token + + authorizeTokenOpts := oauth1.AuthorizeTokenOpts{ + Roles: []oauth1.Role{ + {Name: "member"}, + }, + } + authToken, err := oauth1.AuthorizeToken(identityClient, requestToken.OAuthToken, authorizeTokenOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("Verifier ID of the unauthorized Token: %+v\n", authToken.OAuthVerifier) + +Example to Create an OAuth1 Access Token + + accessTokenOpts := oauth1.CreateAccessTokenOpts{ + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + OAuthToken: requestToken.OAuthToken, + OAuthTokenSecret: requestToken.OAuthTokenSecret, + OAuthVerifier: authToken.OAuthVerifier, + OAuthSignatureMethod: oauth1.HMACSHA1, + } + accessToken, err := oauth1.CreateAccessToken(identityClient, accessTokenOpts).Extract() + if err != nil { + panic(err) + } + + // NOTE: Access token secret is available only on create response + fmt.Printf("OAuth1 Access Token: %+v\n", accessToken) + +Example to List User's OAuth1 Access Tokens + + allPages, err := oauth1.ListAccessTokens(identityClient, userID).AllPages() + if err != nil { + panic(err) + } + accessTokens, err := oauth1.ExtractAccessTokens(allPages) + if err != nil { + panic(err) + } + + for _, accessToken := range accessTokens { + fmt.Printf("Access Token: %+v\n", accessToken) + } + +Example to Authenticate a client using OAuth1 method + + client, err := openstack.NewClient("http://localhost:5000/v3") + if err != nil { + panic(err) + } + + authOptions := &oauth1.AuthOptions{ + // consumer token, created earlier + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + // access token, created earlier + OAuthToken: accessToken.OAuthToken, + OAuthTokenSecret: accessToken.OAuthTokenSecret, + OAuthSignatureMethod: oauth1.HMACSHA1, + } + err = openstack.AuthenticateV3(client, authOptions, gophercloud.EndpointOpts{}) + if err != nil { + panic(err) + } + +Example to Create a Token using OAuth1 method + + var oauth1Token struct { + tokens.Token + oauth1.TokenExt + } + + createOpts := &oauth1.AuthOptions{ + // consumer token, created earlier + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + // access token, created earlier + OAuthToken: accessToken.OAuthToken, + OAuthTokenSecret: accessToken.OAuthTokenSecret, + OAuthSignatureMethod: oauth1.HMACSHA1, + } + err := tokens.Create(identityClient, createOpts).ExtractInto(&oauth1Token) + if err != nil { + panic(err) + } +*/ +package oauth1 diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/requests.go new file mode 100644 index 00000000000..028b5a45bd7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/requests.go @@ -0,0 +1,587 @@ +package oauth1 + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" + "io/ioutil" + "math/rand" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/pagination" +) + +// Type SignatureMethod is a OAuth1 SignatureMethod type. +type SignatureMethod string + +const ( + // HMACSHA1 is a recommended OAuth1 signature method. + HMACSHA1 SignatureMethod = "HMAC-SHA1" + + // PLAINTEXT signature method is not recommended to be used in + // production environment. + PLAINTEXT SignatureMethod = "PLAINTEXT" + + // OAuth1TokenContentType is a supported content type for an OAuth1 + // token. + OAuth1TokenContentType = "application/x-www-form-urlencoded" +) + +// AuthOptions represents options for authenticating a user using OAuth1 tokens. +type AuthOptions struct { + // OAuthConsumerKey is the OAuth1 Consumer Key. + OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"` + + // OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate + // an OAuth1 request signature. + OAuthConsumerSecret string `required:"true"` + + // OAuthToken is the OAuth1 Request Token. + OAuthToken string `q:"oauth_token" required:"true"` + + // OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate + // an OAuth1 request signature. + OAuthTokenSecret string `required:"true"` + + // OAuthSignatureMethod is the OAuth1 signature method the Consumer used + // to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT". + // "PLAINTEXT" is not recommended for production usage. + OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"` + + // OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix + // timestamp will be used. + OAuthTimestamp *time.Time + + // OAuthNonce is an OAuth1 request nonce. Nonce must be a random string, + // uniquely generated for each request. Will be generated automatically + // when it is not set. + OAuthNonce string `q:"oauth_nonce"` + + // AllowReauth allows Gophercloud to re-authenticate automatically + // if/when your token expires. + AllowReauth bool +} + +// ToTokenV3HeadersMap builds the headers required for an OAuth1-based create +// request. +func (opts AuthOptions) ToTokenV3HeadersMap(headerOpts map[string]interface{}) (map[string]string, error) { + q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "") + if err != nil { + return nil, err + } + + signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret} + + method := headerOpts["method"].(string) + u := headerOpts["url"].(string) + stringToSign := buildStringToSign(method, u, q.Query()) + signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys)) + + authHeader := buildAuthHeader(q.Query(), signature) + + headers := map[string]string{ + "Authorization": authHeader, + "X-Auth-Token": "", + } + + return headers, nil +} + +// ToTokenV3ScopeMap allows AuthOptions to satisfy the tokens.AuthOptionsBuilder +// interface. +func (opts AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + return nil, nil +} + +// CanReauth allows AuthOptions to satisfy the tokens.AuthOptionsBuilder +// interface. +func (opts AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +// ToTokenV3CreateMap builds a create request body. +func (opts AuthOptions) ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) { + // identityReq defines the "identity" portion of an OAuth1-based authentication + // create request body. + type identityReq struct { + Methods []string `json:"methods"` + OAuth1 struct{} `json:"oauth1"` + } + + // authReq defines the "auth" portion of an OAuth1-based authentication + // create request body. + type authReq struct { + Identity identityReq `json:"identity"` + } + + // oauth1Request defines how an OAuth1-based authentication create + // request body looks. + type oauth1Request struct { + Auth authReq `json:"auth"` + } + + var req oauth1Request + + req.Auth.Identity.Methods = []string{"oauth1"} + return gophercloud.BuildRequestBody(req, "") +} + +// Create authenticates and either generates a new OpenStack token from an +// OAuth1 token. +func Create(client *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { + b, err := opts.ToTokenV3CreateMap(nil) + if err != nil { + r.Err = err + return + } + + headerOpts := map[string]interface{}{ + "method": "POST", + "url": authURL(client), + } + + h, err := opts.ToTokenV3HeadersMap(headerOpts) + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(authURL(client), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateConsumerOptsBuilder allows extensions to add additional parameters to +// the CreateConsumer request. +type CreateConsumerOptsBuilder interface { + ToOAuth1CreateConsumerMap() (map[string]interface{}, error) +} + +// CreateConsumerOpts provides options used to create a new Consumer. +type CreateConsumerOpts struct { + // Description is the consumer description. + Description string `json:"description"` +} + +// ToOAuth1CreateConsumerMap formats a CreateConsumerOpts into a create request. +func (opts CreateConsumerOpts) ToOAuth1CreateConsumerMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "consumer") +} + +// Create creates a new Consumer. +func CreateConsumer(client *gophercloud.ServiceClient, opts CreateConsumerOptsBuilder) (r CreateConsumerResult) { + b, err := opts.ToOAuth1CreateConsumerMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(consumersURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete deletes a Consumer. +func DeleteConsumer(client *gophercloud.ServiceClient, id string) (r DeleteConsumerResult) { + resp, err := client.Delete(consumerURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// List enumerates Consumers. +func ListConsumers(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, consumersURL(client), func(r pagination.PageResult) pagination.Page { + return ConsumersPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetConsumer retrieves details on a single Consumer by ID. +func GetConsumer(client *gophercloud.ServiceClient, id string) (r GetConsumerResult) { + resp, err := client.Get(consumerURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateConsumerOpts provides options used to update a consumer. +type UpdateConsumerOpts struct { + // Description is the consumer description. + Description string `json:"description"` +} + +// ToOAuth1UpdateConsumerMap formats an UpdateConsumerOpts into a consumer update +// request. +func (opts UpdateConsumerOpts) ToOAuth1UpdateConsumerMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "consumer") +} + +// UpdateConsumer updates an existing Consumer. +func UpdateConsumer(client *gophercloud.ServiceClient, id string, opts UpdateConsumerOpts) (r UpdateConsumerResult) { + b, err := opts.ToOAuth1UpdateConsumerMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Patch(consumerURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RequestTokenOptsBuilder allows extensions to add additional parameters to the +// RequestToken request. +type RequestTokenOptsBuilder interface { + ToOAuth1RequestTokenHeaders(string, string) (map[string]string, error) +} + +// RequestTokenOpts provides options used to get a consumer unauthorized +// request token. +type RequestTokenOpts struct { + // OAuthConsumerKey is the OAuth1 Consumer Key. + OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"` + + // OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate + // an OAuth1 request signature. + OAuthConsumerSecret string `required:"true"` + + // OAuthSignatureMethod is the OAuth1 signature method the Consumer used + // to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT". + // "PLAINTEXT" is not recommended for production usage. + OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"` + + // OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix + // timestamp will be used. + OAuthTimestamp *time.Time + + // OAuthNonce is an OAuth1 request nonce. Nonce must be a random string, + // uniquely generated for each request. Will be generated automatically + // when it is not set. + OAuthNonce string `q:"oauth_nonce"` + + // RequestedProjectID is a Project ID a consumer user requested an + // access to. + RequestedProjectID string `h:"Requested-Project-Id"` +} + +// ToOAuth1RequestTokenHeaders formats a RequestTokenOpts into a map of request +// headers. +func (opts RequestTokenOpts) ToOAuth1RequestTokenHeaders(method, u string) (map[string]string, error) { + q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "oob") + if err != nil { + return nil, err + } + + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + + signatureKeys := []string{opts.OAuthConsumerSecret} + stringToSign := buildStringToSign(method, u, q.Query()) + signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys)) + authHeader := buildAuthHeader(q.Query(), signature) + + h["Authorization"] = authHeader + + return h, nil +} + +// RequestToken requests an unauthorized OAuth1 Token. +func RequestToken(client *gophercloud.ServiceClient, opts RequestTokenOptsBuilder) (r TokenResult) { + h, err := opts.ToOAuth1RequestTokenHeaders("POST", requestTokenURL(client)) + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(requestTokenURL(client), nil, nil, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + KeepResponseBody: true, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + if r.Err != nil { + return + } + defer resp.Body.Close() + if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType { + r.Err = fmt.Errorf("unsupported Content-Type: %q", v) + return + } + r.Body, r.Err = ioutil.ReadAll(resp.Body) + return +} + +// AuthorizeTokenOptsBuilder allows extensions to add additional parameters to +// the AuthorizeToken request. +type AuthorizeTokenOptsBuilder interface { + ToOAuth1AuthorizeTokenMap() (map[string]interface{}, error) +} + +// AuthorizeTokenOpts provides options used to authorize a request token. +type AuthorizeTokenOpts struct { + Roles []Role `json:"roles"` +} + +// Role is a struct representing a role object in a AuthorizeTokenOpts struct. +type Role struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +// ToOAuth1AuthorizeTokenMap formats an AuthorizeTokenOpts into an authorize token +// request. +func (opts AuthorizeTokenOpts) ToOAuth1AuthorizeTokenMap() (map[string]interface{}, error) { + for _, r := range opts.Roles { + if r == (Role{}) { + return nil, fmt.Errorf("role must not be empty") + } + } + return gophercloud.BuildRequestBody(opts, "") +} + +// AuthorizeToken authorizes an unauthorized consumer token. +func AuthorizeToken(client *gophercloud.ServiceClient, id string, opts AuthorizeTokenOptsBuilder) (r AuthorizeTokenResult) { + b, err := opts.ToOAuth1AuthorizeTokenMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(authorizeTokenURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateAccessTokenOptsBuilder allows extensions to add additional parameters +// to the CreateAccessToken request. +type CreateAccessTokenOptsBuilder interface { + ToOAuth1CreateAccessTokenHeaders(string, string) (map[string]string, error) +} + +// CreateAccessTokenOpts provides options used to create an OAuth1 token. +type CreateAccessTokenOpts struct { + // OAuthConsumerKey is the OAuth1 Consumer Key. + OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"` + + // OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate + // an OAuth1 request signature. + OAuthConsumerSecret string `required:"true"` + + // OAuthToken is the OAuth1 Request Token. + OAuthToken string `q:"oauth_token" required:"true"` + + // OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate + // an OAuth1 request signature. + OAuthTokenSecret string `required:"true"` + + // OAuthVerifier is the OAuth1 verification code. + OAuthVerifier string `q:"oauth_verifier" required:"true"` + + // OAuthSignatureMethod is the OAuth1 signature method the Consumer used + // to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT". + // "PLAINTEXT" is not recommended for production usage. + OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"` + + // OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix + // timestamp will be used. + OAuthTimestamp *time.Time + + // OAuthNonce is an OAuth1 request nonce. Nonce must be a random string, + // uniquely generated for each request. Will be generated automatically + // when it is not set. + OAuthNonce string `q:"oauth_nonce"` +} + +// ToOAuth1CreateAccessTokenHeaders formats a CreateAccessTokenOpts into a map of +// request headers. +func (opts CreateAccessTokenOpts) ToOAuth1CreateAccessTokenHeaders(method, u string) (map[string]string, error) { + q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "") + if err != nil { + return nil, err + } + + signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret} + stringToSign := buildStringToSign(method, u, q.Query()) + signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys)) + authHeader := buildAuthHeader(q.Query(), signature) + + headers := map[string]string{ + "Authorization": authHeader, + } + + return headers, nil +} + +// CreateAccessToken creates a new OAuth1 Access Token +func CreateAccessToken(client *gophercloud.ServiceClient, opts CreateAccessTokenOptsBuilder) (r TokenResult) { + h, err := opts.ToOAuth1CreateAccessTokenHeaders("POST", createAccessTokenURL(client)) + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(createAccessTokenURL(client), nil, nil, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + KeepResponseBody: true, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + if r.Err != nil { + return + } + defer resp.Body.Close() + if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType { + r.Err = fmt.Errorf("unsupported Content-Type: %q", v) + return + } + r.Body, r.Err = ioutil.ReadAll(resp.Body) + return +} + +// GetAccessToken retrieves details on a single OAuth1 access token by an ID. +func GetAccessToken(client *gophercloud.ServiceClient, userID string, id string) (r GetAccessTokenResult) { + resp, err := client.Get(userAccessTokenURL(client, userID, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RevokeAccessToken revokes an OAuth1 access token. +func RevokeAccessToken(client *gophercloud.ServiceClient, userID string, id string) (r RevokeAccessTokenResult) { + resp, err := client.Delete(userAccessTokenURL(client, userID, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAccessTokens enumerates authorized access tokens. +func ListAccessTokens(client *gophercloud.ServiceClient, userID string) pagination.Pager { + url := userAccessTokensURL(client, userID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessTokensPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListAccessTokenRoles enumerates authorized access token roles. +func ListAccessTokenRoles(client *gophercloud.ServiceClient, userID string, id string) pagination.Pager { + url := userAccessTokenRolesURL(client, userID, id) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessTokenRolesPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetAccessTokenRole retrieves details on a single OAuth1 access token role by +// an ID. +func GetAccessTokenRole(client *gophercloud.ServiceClient, userID string, id string, roleID string) (r GetAccessTokenRoleResult) { + resp, err := client.Get(userAccessTokenRoleURL(client, userID, id, roleID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// The following are small helper functions used to help build the signature. + +// buildOAuth1QueryString builds a URLEncoded parameters string specific for +// OAuth1-based requests. +func buildOAuth1QueryString(opts interface{}, timestamp *time.Time, callback string) (*url.URL, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return nil, err + } + + query := q.Query() + + if timestamp != nil { + // use provided timestamp + query.Set("oauth_timestamp", strconv.FormatInt(timestamp.Unix(), 10)) + } else { + // use current timestamp + query.Set("oauth_timestamp", strconv.FormatInt(time.Now().UTC().Unix(), 10)) + } + + if query.Get("oauth_nonce") == "" { + // when nonce is not set, generate a random one + query.Set("oauth_nonce", strconv.FormatInt(rand.Int63(), 10)+query.Get("oauth_timestamp")) + } + + if callback != "" { + query.Set("oauth_callback", callback) + } + query.Set("oauth_version", "1.0") + + return &url.URL{RawQuery: query.Encode()}, nil +} + +// buildStringToSign builds a string to be signed. +func buildStringToSign(method string, u string, query url.Values) []byte { + parsedURL, _ := url.Parse(u) + p := parsedURL.Port() + s := parsedURL.Scheme + + // Default scheme port must be stripped + if s == "http" && p == "80" || s == "https" && p == "443" { + parsedURL.Host = strings.TrimSuffix(parsedURL.Host, ":"+p) + } + + // Ensure that URL doesn't contain queries + parsedURL.RawQuery = "" + + v := strings.Join( + []string{method, url.QueryEscape(parsedURL.String()), url.QueryEscape(query.Encode())}, "&") + + return []byte(v) +} + +// signString signs a string using an OAuth1 signature method. +func signString(signatureMethod SignatureMethod, strToSign []byte, signatureKeys []string) string { + var key []byte + for i, k := range signatureKeys { + key = append(key, []byte(url.QueryEscape(k))...) + if i == 0 { + key = append(key, '&') + } + } + + var signedString string + switch signatureMethod { + case PLAINTEXT: + signedString = string(key) + default: + h := hmac.New(sha1.New, key) + h.Write(strToSign) + signedString = base64.StdEncoding.EncodeToString(h.Sum(nil)) + } + + return signedString +} + +// buildAuthHeader generates an OAuth1 Authorization header with a signature +// calculated using an OAuth1 signature method. +func buildAuthHeader(query url.Values, signature string) string { + var authHeader []string + var keys []string + for k := range query { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + for _, v := range query[k] { + authHeader = append(authHeader, fmt.Sprintf("%s=%q", k, url.QueryEscape(v))) + } + } + + authHeader = append(authHeader, fmt.Sprintf("oauth_signature=%q", signature)) + + return "OAuth " + strings.Join(authHeader, ", ") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/results.go new file mode 100644 index 00000000000..2a370616276 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/results.go @@ -0,0 +1,317 @@ +package oauth1 + +import ( + "encoding/json" + "net/url" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Consumer represents a delegated authorization request between two +// identities. +type Consumer struct { + ID string `json:"id"` + Secret string `json:"secret"` + Description string `json:"description"` +} + +type consumerResult struct { + gophercloud.Result +} + +// CreateConsumerResult is the response from a Create operation. Call its +// Extract method to interpret it as a Consumer. +type CreateConsumerResult struct { + consumerResult +} + +// UpdateConsumerResult is the response from a Create operation. Call its +// Extract method to interpret it as a Consumer. +type UpdateConsumerResult struct { + consumerResult +} + +// DeleteConsumerResult is the response from a Delete operation. Call its +// ExtractErr to determine if the request succeeded or failed. +type DeleteConsumerResult struct { + gophercloud.ErrResult +} + +// ConsumersPage is a single page of Region results. +type ConsumersPage struct { + pagination.LinkedPageBase +} + +// GetConsumerResult is the response from a Get operation. Call its Extract +// method to interpret it as a Consumer. +type GetConsumerResult struct { + consumerResult +} + +// IsEmpty determines whether or not a page of Consumers contains any results. +func (c ConsumersPage) IsEmpty() (bool, error) { + if c.StatusCode == 204 { + return true, nil + } + + consumers, err := ExtractConsumers(c) + return len(consumers) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (c ConsumersPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := c.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractConsumers returns a slice of Consumers contained in a single page of +// results. +func ExtractConsumers(r pagination.Page) ([]Consumer, error) { + var s struct { + Consumers []Consumer `json:"consumers"` + } + err := (r.(ConsumersPage)).ExtractInto(&s) + return s.Consumers, err +} + +// Extract interprets any consumer result as a Consumer. +func (c consumerResult) Extract() (*Consumer, error) { + var s struct { + Consumer *Consumer `json:"consumer"` + } + err := c.ExtractInto(&s) + return s.Consumer, err +} + +// Token contains an OAuth1 token. +type Token struct { + // OAuthToken is the key value for the oauth token that the Identity API returns. + OAuthToken string `q:"oauth_token"` + // OAuthTokenSecret is the secret value associated with the OAuth Token. + OAuthTokenSecret string `q:"oauth_token_secret"` + // OAuthExpiresAt is the date and time when an OAuth token expires. + OAuthExpiresAt *time.Time `q:"-"` +} + +// TokenResult is a struct to handle +// "Content-Type: application/x-www-form-urlencoded" response. +type TokenResult struct { + gophercloud.Result + Body []byte +} + +// Extract interprets any OAuth1 token result as a Token. +func (r TokenResult) Extract() (*Token, error) { + if r.Err != nil { + return nil, r.Err + } + + values, err := url.ParseQuery(string(r.Body)) + if err != nil { + return nil, err + } + + token := &Token{ + OAuthToken: values.Get("oauth_token"), + OAuthTokenSecret: values.Get("oauth_token_secret"), + } + + if v := values.Get("oauth_expires_at"); v != "" { + if t, err := time.Parse(gophercloud.RFC3339Milli, v); err != nil { + return nil, err + } else { + token.OAuthExpiresAt = &t + } + } + + return token, nil +} + +// AuthorizedToken contains an OAuth1 authorized token info. +type AuthorizedToken struct { + // OAuthVerifier is the ID of the token verifier. + OAuthVerifier string `json:"oauth_verifier"` +} + +type AuthorizeTokenResult struct { + gophercloud.Result +} + +// Extract interprets AuthorizeTokenResult result as a AuthorizedToken. +func (r AuthorizeTokenResult) Extract() (*AuthorizedToken, error) { + var s struct { + AuthorizedToken *AuthorizedToken `json:"token"` + } + err := r.ExtractInto(&s) + return s.AuthorizedToken, err +} + +// AccessToken represents an AccessToken response as a struct. +type AccessToken struct { + ID string `json:"id"` + ConsumerID string `json:"consumer_id"` + ProjectID string `json:"project_id"` + AuthorizingUserID string `json:"authorizing_user_id"` + ExpiresAt *time.Time `json:"-"` +} + +func (r *AccessToken) UnmarshalJSON(b []byte) error { + type tmp AccessToken + var s struct { + tmp + ExpiresAt *gophercloud.JSONRFC3339Milli `json:"expires_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = AccessToken(s.tmp) + + if s.ExpiresAt != nil { + t := time.Time(*s.ExpiresAt) + r.ExpiresAt = &t + } + + return nil +} + +type GetAccessTokenResult struct { + gophercloud.Result +} + +// Extract interprets any GetAccessTokenResult result as an AccessToken. +func (r GetAccessTokenResult) Extract() (*AccessToken, error) { + var s struct { + AccessToken *AccessToken `json:"access_token"` + } + err := r.ExtractInto(&s) + return s.AccessToken, err +} + +// RevokeAccessTokenResult is the response from a Delete operation. Call its +// ExtractErr to determine if the request succeeded or failed. +type RevokeAccessTokenResult struct { + gophercloud.ErrResult +} + +// AccessTokensPage is a single page of Access Tokens results. +type AccessTokensPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a an AccessTokensPage contains any results. +func (r AccessTokensPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + accessTokens, err := ExtractAccessTokens(r) + return len(accessTokens) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r AccessTokensPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractAccessTokens returns a slice of AccessTokens contained in a single +// page of results. +func ExtractAccessTokens(r pagination.Page) ([]AccessToken, error) { + var s struct { + AccessTokens []AccessToken `json:"access_tokens"` + } + err := (r.(AccessTokensPage)).ExtractInto(&s) + return s.AccessTokens, err +} + +// AccessTokenRole represents an Access Token Role struct. +type AccessTokenRole struct { + ID string `json:"id"` + Name string `json:"name"` + DomainID string `json:"domain_id"` +} + +// AccessTokenRolesPage is a single page of Access Token roles results. +type AccessTokenRolesPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a an AccessTokensPage contains any results. +func (r AccessTokenRolesPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + accessTokenRoles, err := ExtractAccessTokenRoles(r) + return len(accessTokenRoles) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r AccessTokenRolesPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractAccessTokenRoles returns a slice of AccessTokenRole contained in a +// single page of results. +func ExtractAccessTokenRoles(r pagination.Page) ([]AccessTokenRole, error) { + var s struct { + AccessTokenRoles []AccessTokenRole `json:"roles"` + } + err := (r.(AccessTokenRolesPage)).ExtractInto(&s) + return s.AccessTokenRoles, err +} + +type GetAccessTokenRoleResult struct { + gophercloud.Result +} + +// Extract interprets any GetAccessTokenRoleResult result as an AccessTokenRole. +func (r GetAccessTokenRoleResult) Extract() (*AccessTokenRole, error) { + var s struct { + AccessTokenRole *AccessTokenRole `json:"role"` + } + err := r.ExtractInto(&s) + return s.AccessTokenRole, err +} + +// OAuth1 is an OAuth1 object, returned in OAuth1 token result. +type OAuth1 struct { + AccessTokenID string `json:"access_token_id"` + ConsumerID string `json:"consumer_id"` +} + +// TokenExt represents an extension of the base token result. +type TokenExt struct { + OAuth1 OAuth1 `json:"OS-OAUTH1"` +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/urls.go new file mode 100644 index 00000000000..9b51d53b31d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1/urls.go @@ -0,0 +1,43 @@ +package oauth1 + +import "github.com/gophercloud/gophercloud" + +func consumersURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("OS-OAUTH1", "consumers") +} + +func consumerURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("OS-OAUTH1", "consumers", id) +} + +func requestTokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("OS-OAUTH1", "request_token") +} + +func authorizeTokenURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("OS-OAUTH1", "authorize", id) +} + +func createAccessTokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("OS-OAUTH1", "access_token") +} + +func userAccessTokensURL(c *gophercloud.ServiceClient, userID string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens") +} + +func userAccessTokenURL(c *gophercloud.ServiceClient, userID string, id string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id) +} + +func userAccessTokenRolesURL(c *gophercloud.ServiceClient, userID string, id string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id, "roles") +} + +func userAccessTokenRoleURL(c *gophercloud.ServiceClient, userID string, id string, roleID string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id, "roles", roleID) +} + +func authURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go new file mode 100644 index 00000000000..de74c82ecd2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/doc.go @@ -0,0 +1,107 @@ +/* +Package tokens provides information and interaction with the token API +resource for the OpenStack Identity service. + +For more information, see: +http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3 + +Example to Create a Token From a Username and Password + + authOptions := tokens.AuthOptions{ + UserID: "username", + Password: "password", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token From a Username, Password, and Domain + + authOptions := tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainID: "default", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + + authOptions = tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainName: "default", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token From a Token + + authOptions := tokens.AuthOptions{ + TokenID: "token_id", + } + + token, err := tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Project ID Scope + + scope := tokens.Scope{ + ProjectID: "0fe36e73809d46aeae6705c39077b1b3", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Domain ID Scope + + scope := tokens.Scope{ + DomainID: "default", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Project Name Scope + + scope := tokens.Scope{ + ProjectName: "project_name", + DomainID: "default", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } +*/ +package tokens diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go new file mode 100644 index 00000000000..1af55d81377 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/requests.go @@ -0,0 +1,174 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +// Scope allows a created token to be limited to a specific domain or project. +type Scope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string + System bool +} + +// AuthOptionsBuilder provides the ability for extensions to add additional +// parameters to AuthOptions. Extensions must satisfy all required methods. +type AuthOptionsBuilder interface { + // ToTokenV3CreateMap assembles the Create request body, returning an error + // if parameters are missing or inconsistent. + ToTokenV3CreateMap(map[string]interface{}) (map[string]interface{}, error) + ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) + ToTokenV3ScopeMap() (map[string]interface{}, error) + CanReauth() bool +} + +// AuthOptions represents options for authenticating a user. +type AuthOptions struct { + // IdentityEndpoint specifies the HTTP endpoint that is required to work with + // the Identity API of the appropriate version. While it's ultimately needed + // by all of the identity services, it will often be populated by a + // provider-level function. + IdentityEndpoint string `json:"-"` + + // Username is required if using Identity V2 API. Consult with your provider's + // control panel to discover your account's username. In Identity V3, either + // UserID or a combination of Username and DomainID or DomainName are needed. + Username string `json:"username,omitempty"` + UserID string `json:"id,omitempty"` + + Password string `json:"password,omitempty"` + + // Passcode is used in TOTP authentication method + Passcode string `json:"passcode,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"-"` + DomainName string `json:"name,omitempty"` + + // AllowReauth should be set to true if you grant permission for Gophercloud + // to cache your credentials in memory, and to allow Gophercloud to attempt + // to re-authenticate automatically if/when your token expires. If you set + // it to false, it will not cache these settings, but re-authentication will + // not be possible. This setting defaults to false. + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + // Authentication through Application Credentials requires supplying name, project and secret + // For project we can use TenantID + ApplicationCredentialID string `json:"-"` + ApplicationCredentialName string `json:"-"` + ApplicationCredentialSecret string `json:"-"` + + Scope Scope `json:"-"` +} + +// ToTokenV3CreateMap builds a request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + gophercloudAuthOpts := gophercloud.AuthOptions{ + Username: opts.Username, + UserID: opts.UserID, + Password: opts.Password, + Passcode: opts.Passcode, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + AllowReauth: opts.AllowReauth, + TokenID: opts.TokenID, + ApplicationCredentialID: opts.ApplicationCredentialID, + ApplicationCredentialName: opts.ApplicationCredentialName, + ApplicationCredentialSecret: opts.ApplicationCredentialSecret, + } + + return gophercloudAuthOpts.ToTokenV3CreateMap(scope) +} + +// ToTokenV3ScopeMap builds a scope request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + scope := gophercloud.AuthScope(opts.Scope) + + gophercloudAuthOpts := gophercloud.AuthOptions{ + Scope: &scope, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + } + + return gophercloudAuthOpts.ToTokenV3ScopeMap() +} + +func (opts *AuthOptions) CanReauth() bool { + if opts.Passcode != "" { + // cannot reauth using TOTP passcode + return false + } + + return opts.AllowReauth +} + +// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v3 tokens package. +func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]interface{}) (map[string]string, error) { + return nil, nil +} + +func subjectTokenHeaders(subjectToken string) map[string]string { + return map[string]string{ + "X-Subject-Token": subjectToken, + } +} + +// Create authenticates and either generates a new token, or changes the Scope +// of an existing token. +func Create(c *gophercloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) { + scope, err := opts.ToTokenV3ScopeMap() + if err != nil { + r.Err = err + return + } + + b, err := opts.ToTokenV3CreateMap(scope) + if err != nil { + r.Err = err + return + } + + resp, err := c.Post(tokenURL(c), b, &r.Body, &gophercloud.RequestOpts{ + OmitHeaders: []string{"X-Auth-Token"}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get validates and retrieves information about another token. +func Get(c *gophercloud.ServiceClient, token string) (r GetResult) { + resp, err := c.Get(tokenURL(c), &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(token), + OkCodes: []int{200, 203}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Validate determines if a specified token is valid or not. +func Validate(c *gophercloud.ServiceClient, token string) (bool, error) { + resp, err := c.Head(tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(token), + OkCodes: []int{200, 204, 404}, + }) + if err != nil { + return false, err + } + + return resp.StatusCode == 200 || resp.StatusCode == 204, nil +} + +// Revoke immediately makes specified token invalid. +func Revoke(c *gophercloud.ServiceClient, token string) (r RevokeResult) { + resp, err := c.Delete(tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(token), + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go new file mode 100644 index 00000000000..f1e17e9f75a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/results.go @@ -0,0 +1,194 @@ +package tokens + +import ( + "time" + + "github.com/gophercloud/gophercloud" +) + +// Endpoint represents a single API endpoint offered by a service. +// It matches either a public, internal or admin URL. +// If supported, it contains a region specifier, again if provided. +// The significance of the Region field will depend upon your provider. +type Endpoint struct { + ID string `json:"id"` + Region string `json:"region"` + RegionID string `json:"region_id"` + Interface string `json:"interface"` + URL string `json:"url"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V3 service +// catalog listing. Each class of service, such as cloud DNS or block storage +// services, could have multiple CatalogEntry representing it (one by interface +// type, e.g public, admin or internal). +// +// Note: when looking for the desired service, try, whenever possible, to key +// off the type field. Otherwise, you'll tie the representation of the service +// to a specific provider. +type CatalogEntry struct { + // Service ID + ID string `json:"id"` + + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + + // Type will contain a type string if OpenStack defines a type for the + // service. Otherwise, for provider-specific services, the provider may + // assign their own type strings. + Type string `json:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that + // may exist for the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, +// successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry `json:"catalog"` +} + +// Domain provides information about the domain to which this token grants +// access. +type Domain struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// User represents a user resource that exists in the Identity Service. +type User struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// Role provides information about roles to which User is authorized. +type Role struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Project provides information about project to which User is authorized. +type Project struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// commonResult is the response from a request. A commonResult has various +// methods which can be used to extract different details about the result. +type commonResult struct { + gophercloud.Result +} + +// Extract is a shortcut for ExtractToken. +// This function is deprecated and still present for backward compatibility. +func (r commonResult) Extract() (*Token, error) { + return r.ExtractToken() +} + +// ExtractToken interprets a commonResult as a Token. +func (r commonResult) ExtractToken() (*Token, error) { + var s Token + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + // Parse the token itself from the stored headers. + s.ID = r.Header.Get("X-Subject-Token") + + return &s, err +} + +// ExtractTokenID implements the gophercloud.AuthResult interface. The returned +// string is the same as the ID field of the Token struct returned from +// ExtractToken(). +func (r CreateResult) ExtractTokenID() (string, error) { + return r.Header.Get("X-Subject-Token"), r.Err +} + +// ExtractTokenID implements the gophercloud.AuthResult interface. The returned +// string is the same as the ID field of the Token struct returned from +// ExtractToken(). +func (r GetResult) ExtractTokenID() (string, error) { + return r.Header.Get("X-Subject-Token"), r.Err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along +// with the user's Token. +func (r commonResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s ServiceCatalog + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractUser returns the User that is the owner of the Token. +func (r commonResult) ExtractUser() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} + +// ExtractRoles returns Roles to which User is authorized. +func (r commonResult) ExtractRoles() ([]Role, error) { + var s struct { + Roles []Role `json:"roles"` + } + err := r.ExtractInto(&s) + return s.Roles, err +} + +// ExtractProject returns Project to which User is authorized. +func (r commonResult) ExtractProject() (*Project, error) { + var s struct { + Project *Project `json:"project"` + } + err := r.ExtractInto(&s) + return s.Project, err +} + +// ExtractDomain returns Domain to which User is authorized. +func (r commonResult) ExtractDomain() (*Domain, error) { + var s struct { + Domain *Domain `json:"domain"` + } + err := r.ExtractInto(&s) + return s.Domain, err +} + +// CreateResult is the response from a Create request. Use ExtractToken() +// to interpret it as a Token, or ExtractServiceCatalog() to interpret it +// as a service catalog. +type CreateResult struct { + commonResult +} + +// GetResult is the response from a Get request. Use ExtractToken() +// to interpret it as a Token, or ExtractServiceCatalog() to interpret it +// as a service catalog. +type GetResult struct { + commonResult +} + +// RevokeResult is response from a Revoke request. +type RevokeResult struct { + commonResult +} + +// Token is a string that grants a user access to a controlled set of services +// in an OpenStack provider. Each Token is valid for a set length of time. +type Token struct { + // ID is the issued token. + ID string `json:"id"` + + // ExpiresAt is the timestamp at which this token will no longer be accepted. + ExpiresAt time.Time `json:"expires_at"` +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.ExtractIntoStructPtr(v, "token") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go new file mode 100644 index 00000000000..2f864a31c8b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/identity/v3/tokens/urls.go @@ -0,0 +1,7 @@ +package tokens + +import "github.com/gophercloud/gophercloud" + +func tokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go new file mode 100644 index 00000000000..14da9ac90da --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/doc.go @@ -0,0 +1,60 @@ +/* +Package images enables management and retrieval of images from the OpenStack +Image Service. + +Example to List Images + + images.ListOpts{ + Owner: "a7509e1ae65945fda83f3e52c6296017", + } + + allPages, err := images.List(imagesClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + panic(err) + } + + for _, image := range allImages { + fmt.Printf("%+v\n", image) + } + +Example to Create an Image + + createOpts := images.CreateOpts{ + Name: "image_name", + Visibility: images.ImageVisibilityPrivate, + } + + image, err := images.Create(imageClient, createOpts) + if err != nil { + panic(err) + } + +Example to Update an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + + updateOpts := images.UpdateOpts{ + images.ReplaceImageName{ + NewName: "new_name", + }, + } + + image, err := images.Update(imageClient, imageID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + err := images.Delete(imageClient, imageID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package images diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go new file mode 100644 index 00000000000..2ab609cbca8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/requests.go @@ -0,0 +1,418 @@ +package images + +import ( + "fmt" + "net/url" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +// +// http://developer.openstack.org/api-ref-image-v2.html +type ListOpts struct { + // ID is the ID of the image. + // Multiple IDs can be specified by constructing a string + // such as "in:uuid1,uuid2,uuid3". + ID string `q:"id"` + + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Name filters on the name of the image. + // Multiple names can be specified by constructing a string + // such as "in:name1,name2,name3". + Name string `q:"name"` + + // Visibility filters on the visibility of the image. + Visibility ImageVisibility `q:"visibility"` + + // Hidden filters on the hidden status of the image. + Hidden bool `q:"os_hidden"` + + // MemberStatus filters on the member status of the image. + MemberStatus ImageMemberStatus `q:"member_status"` + + // Owner filters on the project ID of the image. + Owner string `q:"owner"` + + // Status filters on the status of the image. + // Multiple statuses can be specified by constructing a string + // such as "in:saving,queued". + Status ImageStatus `q:"status"` + + // SizeMin filters on the size_min image property. + SizeMin int64 `q:"size_min"` + + // SizeMax filters on the size_max image property. + SizeMax int64 `q:"size_max"` + + // Sort sorts the results using the new style of sorting. See the OpenStack + // Image API reference for the exact syntax. + // + // Sort cannot be used with the classic sort options (sort_key and sort_dir). + Sort string `q:"sort"` + + // SortKey will sort the results based on a specified image property. + SortKey string `q:"sort_key"` + + // SortDir will sort the list results either ascending or decending. + SortDir string `q:"sort_dir"` + + // Tags filters on specific image tags. + Tags []string `q:"tag"` + + // CreatedAtQuery filters images based on their creation date. + CreatedAtQuery *ImageDateQuery + + // UpdatedAtQuery filters images based on their updated date. + UpdatedAtQuery *ImageDateQuery + + // ContainerFormat filters images based on the container_format. + // Multiple container formats can be specified by constructing a + // string such as "in:bare,ami". + ContainerFormat string `q:"container_format"` + + // DiskFormat filters images based on the disk_format. + // Multiple disk formats can be specified by constructing a string + // such as "in:qcow2,iso". + DiskFormat string `q:"disk_format"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + params := q.Query() + + if opts.CreatedAtQuery != nil { + createdAt := opts.CreatedAtQuery.Date.Format(time.RFC3339) + if v := opts.CreatedAtQuery.Filter; v != "" { + createdAt = fmt.Sprintf("%s:%s", v, createdAt) + } + + params.Add("created_at", createdAt) + } + + if opts.UpdatedAtQuery != nil { + updatedAt := opts.UpdatedAtQuery.Date.Format(time.RFC3339) + if v := opts.UpdatedAtQuery.Filter; v != "" { + updatedAt = fmt.Sprintf("%s:%s", v, updatedAt) + } + + params.Add("updated_at", updatedAt) + } + + q = &url.URL{RawQuery: params.Encode()} + + return q.String(), err +} + +// List implements image list request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + imagePage := ImagePage{ + serviceURL: c.ServiceURL(), + LinkedPageBase: pagination.LinkedPageBase{PageResult: r}, + } + + return imagePage + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + // Returns value that can be passed to json.Marshal + ToImageCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create an image. +type CreateOpts struct { + // Name is the name of the new image. + Name string `json:"name" required:"true"` + + // Id is the the image ID. + ID string `json:"id,omitempty"` + + // Visibility defines who can see/use the image. + Visibility *ImageVisibility `json:"visibility,omitempty"` + + // Hidden is whether the image is listed in default image list or not. + Hidden *bool `json:"os_hidden,omitempty"` + + // Tags is a set of image tags. + Tags []string `json:"tags,omitempty"` + + // ContainerFormat is the format of the + // container. Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format,omitempty"` + + // DiskFormat is the format of the disk. If set, + // valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format,omitempty"` + + // MinDisk is the amount of disk space in + // GB that is required to boot the image. + MinDisk int `json:"min_disk,omitempty"` + + // MinRAM is the amount of RAM in MB that + // is required to boot the image. + MinRAM int `json:"min_ram,omitempty"` + + // protected is whether the image is not deletable. + Protected *bool `json:"protected,omitempty"` + + // properties is a set of properties, if any, that + // are associated with the image. + Properties map[string]string `json:"-"` +} + +// ToImageCreateMap assembles a request body based on the contents of +// a CreateOpts. +func (opts CreateOpts) ToImageCreateMap() (map[string]interface{}, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.Properties != nil { + for k, v := range opts.Properties { + b[k] = v + } + } + return b, nil +} + +// Create implements create image request. +func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return r + } + resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{201}}) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete implements image delete request. +func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get implements image get request. +func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Update implements image updated request. +func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToImageUpdateMap() + if err != nil { + r.Err = err + return r + } + resp, err := client.Patch(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + // returns value implementing json.Marshaler which when marshaled matches + // the patch schema: + // http://specs.openstack.org/openstack/glance-specs/specs/api/v2/http-patch-image-api-v2.html + ToImageUpdateMap() ([]interface{}, error) +} + +// UpdateOpts implements UpdateOpts +type UpdateOpts []Patch + +// ToImageUpdateMap assembles a request body based on the contents of +// UpdateOpts. +func (opts UpdateOpts) ToImageUpdateMap() ([]interface{}, error) { + m := make([]interface{}, len(opts)) + for i, patch := range opts { + patchJSON := patch.ToImagePatchMap() + m[i] = patchJSON + } + return m, nil +} + +// Patch represents a single update to an existing image. Multiple updates +// to an image can be submitted at the same time. +type Patch interface { + ToImagePatchMap() map[string]interface{} +} + +// UpdateVisibility represents an updated visibility property request. +type UpdateVisibility struct { + Visibility ImageVisibility +} + +// ToImagePatchMap assembles a request body based on UpdateVisibility. +func (r UpdateVisibility) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/visibility", + "value": r.Visibility, + } +} + +// ReplaceImageHidden represents an updated os_hidden property request. +type ReplaceImageHidden struct { + NewHidden bool +} + +// ToImagePatchMap assembles a request body based on ReplaceImageHidden. +func (r ReplaceImageHidden) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/os_hidden", + "value": r.NewHidden, + } +} + +// ReplaceImageName represents an updated image_name property request. +type ReplaceImageName struct { + NewName string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageName. +func (r ReplaceImageName) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/name", + "value": r.NewName, + } +} + +// ReplaceImageChecksum represents an updated checksum property request. +type ReplaceImageChecksum struct { + Checksum string +} + +// ReplaceImageChecksum assembles a request body based on ReplaceImageChecksum. +func (r ReplaceImageChecksum) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/checksum", + "value": r.Checksum, + } +} + +// ReplaceImageTags represents an updated tags property request. +type ReplaceImageTags struct { + NewTags []string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageTags) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/tags", + "value": r.NewTags, + } +} + +// ReplaceImageMinDisk represents an updated min_disk property request. +type ReplaceImageMinDisk struct { + NewMinDisk int +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageMinDisk) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/min_disk", + "value": r.NewMinDisk, + } +} + +// ReplaceImageMinRam represents an updated min_ram property request. +type ReplaceImageMinRam struct { + NewMinRam int +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageMinRam) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/min_ram", + "value": r.NewMinRam, + } +} + +// ReplaceImageProtected represents an updated protected property request. +type ReplaceImageProtected struct { + NewProtected bool +} + +// ToImagePatchMap assembles a request body based on ReplaceImageProtected +func (r ReplaceImageProtected) ToImagePatchMap() map[string]interface{} { + return map[string]interface{}{ + "op": "replace", + "path": "/protected", + "value": r.NewProtected, + } +} + +// UpdateOp represents a valid update operation. +type UpdateOp string + +const ( + AddOp UpdateOp = "add" + ReplaceOp UpdateOp = "replace" + RemoveOp UpdateOp = "remove" +) + +// UpdateImageProperty represents an update property request. +type UpdateImageProperty struct { + Op UpdateOp + Name string + Value string +} + +// ToImagePatchMap assembles a request body based on UpdateImageProperty. +func (r UpdateImageProperty) ToImagePatchMap() map[string]interface{} { + updateMap := map[string]interface{}{ + "op": r.Op, + "path": fmt.Sprintf("/%s", r.Name), + } + + if r.Op != RemoveOp { + updateMap["value"] = r.Value + } + + return updateMap +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go new file mode 100644 index 00000000000..96fd91a2cac --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/results.go @@ -0,0 +1,246 @@ +package images + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// Image represents an image found in the OpenStack Image service. +type Image struct { + // ID is the image UUID. + ID string `json:"id"` + + // Name is the human-readable display name for the image. + Name string `json:"name"` + + // Status is the image status. It can be "queued" or "active" + // See imageservice/v2/images/type.go + Status ImageStatus `json:"status"` + + // Tags is a list of image tags. Tags are arbitrarily defined strings + // attached to an image. + Tags []string `json:"tags"` + + // ContainerFormat is the format of the container. + // Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format"` + + // DiskFormat is the format of the disk. + // If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format"` + + // MinDiskGigabytes is the amount of disk space in GB that is required to + // boot the image. + MinDiskGigabytes int `json:"min_disk"` + + // MinRAMMegabytes [optional] is the amount of RAM in MB that is required to + // boot the image. + MinRAMMegabytes int `json:"min_ram"` + + // Owner is the tenant ID the image belongs to. + Owner string `json:"owner"` + + // Protected is whether the image is deletable or not. + Protected bool `json:"protected"` + + // Visibility defines who can see/use the image. + Visibility ImageVisibility `json:"visibility"` + + // Hidden is whether the image is listed in default image list or not. + Hidden bool `json:"os_hidden"` + + // Checksum is the checksum of the data that's associated with the image. + Checksum string `json:"checksum"` + + // SizeBytes is the size of the data that's associated with the image. + SizeBytes int64 `json:"-"` + + // Metadata is a set of metadata associated with the image. + // Image metadata allow for meaningfully define the image properties + // and tags. + // See http://docs.openstack.org/developer/glance/metadefs-concepts.html. + Metadata map[string]string `json:"metadata"` + + // Properties is a set of key-value pairs, if any, that are associated with + // the image. + Properties map[string]interface{} + + // CreatedAt is the date when the image has been created. + CreatedAt time.Time `json:"created_at"` + + // UpdatedAt is the date when the last change has been made to the image or + // its properties. + UpdatedAt time.Time `json:"updated_at"` + + // File is the trailing path after the glance endpoint that represent the + // location of the image or the path to retrieve it. + File string `json:"file"` + + // Schema is the path to the JSON-schema that represent the image or image + // entity. + Schema string `json:"schema"` + + // VirtualSize is the virtual size of the image + VirtualSize int64 `json:"virtual_size"` + + // OpenStackImageImportMethods is a slice listing the types of import + // methods available in the cloud. + OpenStackImageImportMethods []string `json:"-"` + // OpenStackImageStoreIDs is a slice listing the store IDs available in + // the cloud. + OpenStackImageStoreIDs []string `json:"-"` +} + +func (r *Image) UnmarshalJSON(b []byte) error { + type tmp Image + var s struct { + tmp + SizeBytes interface{} `json:"size"` + OpenStackImageImportMethods string `json:"openstack-image-import-methods"` + OpenStackImageStoreIDs string `json:"openstack-image-store-ids"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Image(s.tmp) + + switch t := s.SizeBytes.(type) { + case nil: + r.SizeBytes = 0 + case float32: + r.SizeBytes = int64(t) + case float64: + r.SizeBytes = int64(t) + default: + return fmt.Errorf("Unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t) + } + + // Bundle all other fields into Properties + var result interface{} + err = json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]interface{}); ok { + delete(resultMap, "self") + delete(resultMap, "size") + delete(resultMap, "openstack-image-import-methods") + delete(resultMap, "openstack-image-store-ids") + r.Properties = gophercloud.RemainingKeys(Image{}, resultMap) + } + + if v := strings.FieldsFunc(strings.TrimSpace(s.OpenStackImageImportMethods), splitFunc); len(v) > 0 { + r.OpenStackImageImportMethods = v + } + if v := strings.FieldsFunc(strings.TrimSpace(s.OpenStackImageStoreIDs), splitFunc); len(v) > 0 { + r.OpenStackImageStoreIDs = v + } + + return err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as an Image. +func (r commonResult) Extract() (*Image, error) { + var s *Image + if v, ok := r.Body.(map[string]interface{}); ok { + for k, h := range r.Header { + if strings.ToLower(k) == "openstack-image-import-methods" { + for _, s := range h { + v["openstack-image-import-methods"] = s + } + } + if strings.ToLower(k) == "openstack-image-store-ids" { + for _, s := range h { + v["openstack-image-store-ids"] = s + } + } + } + } + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret it as an Image. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret it as an Image. +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret it as an Image. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to interpret it as an Image. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ImagePage represents the results of a List request. +type ImagePage struct { + serviceURL string + pagination.LinkedPageBase +} + +// IsEmpty returns true if an ImagePage contains no Images results. +func (r ImagePage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + images, err := ExtractImages(r) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to +// the next page of results. +func (r ImagePage) NextPageURL() (string, error) { + var s struct { + Next string `json:"next"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + if s.Next == "" { + return "", nil + } + + return nextPageURL(r.serviceURL, s.Next) +} + +// ExtractImages interprets the results of a single page from a List() call, +// producing a slice of Image entities. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} + +// splitFunc is a helper function used to avoid a slice of empty strings. +func splitFunc(c rune) bool { + return c == ',' +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go new file mode 100644 index 00000000000..147be199271 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/types.go @@ -0,0 +1,108 @@ +package images + +import ( + "time" +) + +// ImageStatus image statuses +// http://docs.openstack.org/developer/glance/statuses.html +type ImageStatus string + +const ( + // ImageStatusQueued is a status for an image which identifier has + // been reserved for an image in the image registry. + ImageStatusQueued ImageStatus = "queued" + + // ImageStatusSaving denotes that an image’s raw data is currently being + // uploaded to Glance + ImageStatusSaving ImageStatus = "saving" + + // ImageStatusActive denotes an image that is fully available in Glance. + ImageStatusActive ImageStatus = "active" + + // ImageStatusKilled denotes that an error occurred during the uploading + // of an image’s data, and that the image is not readable. + ImageStatusKilled ImageStatus = "killed" + + // ImageStatusDeleted is used for an image that is no longer available to use. + // The image information is retained in the image registry. + ImageStatusDeleted ImageStatus = "deleted" + + // ImageStatusPendingDelete is similar to Delete, but the image is not yet + // deleted. + ImageStatusPendingDelete ImageStatus = "pending_delete" + + // ImageStatusDeactivated denotes that access to image data is not allowed to + // any non-admin user. + ImageStatusDeactivated ImageStatus = "deactivated" + + // ImageStatusImporting denotes that an import call has been made but that + // the image is not yet ready for use. + ImageStatusImporting ImageStatus = "importing" +) + +// ImageVisibility denotes an image that is fully available in Glance. +// This occurs when the image data is uploaded, or the image size is explicitly +// set to zero on creation. +// According to design +// https://wiki.openstack.org/wiki/Glance-v2-community-image-visibility-design +type ImageVisibility string + +const ( + // ImageVisibilityPublic all users + ImageVisibilityPublic ImageVisibility = "public" + + // ImageVisibilityPrivate users with tenantId == tenantId(owner) + ImageVisibilityPrivate ImageVisibility = "private" + + // ImageVisibilityShared images are visible to: + // - users with tenantId == tenantId(owner) + // - users with tenantId in the member-list of the image + // - users with tenantId in the member-list with member_status == 'accepted' + ImageVisibilityShared ImageVisibility = "shared" + + // ImageVisibilityCommunity images: + // - all users can see and boot it + // - users with tenantId in the member-list of the image with + // member_status == 'accepted' have this image in their default image-list. + ImageVisibilityCommunity ImageVisibility = "community" +) + +// MemberStatus is a status for adding a new member (tenant) to an image +// member list. +type ImageMemberStatus string + +const ( + // ImageMemberStatusAccepted is the status for an accepted image member. + ImageMemberStatusAccepted ImageMemberStatus = "accepted" + + // ImageMemberStatusPending shows that the member addition is pending + ImageMemberStatusPending ImageMemberStatus = "pending" + + // ImageMemberStatusAccepted is the status for a rejected image member + ImageMemberStatusRejected ImageMemberStatus = "rejected" + + // ImageMemberStatusAll + ImageMemberStatusAll ImageMemberStatus = "all" +) + +// ImageDateFilter represents a valid filter to use for filtering +// images by their date during a List. +type ImageDateFilter string + +const ( + FilterGT ImageDateFilter = "gt" + FilterGTE ImageDateFilter = "gte" + FilterLT ImageDateFilter = "lt" + FilterLTE ImageDateFilter = "lte" + FilterNEQ ImageDateFilter = "neq" + FilterEQ ImageDateFilter = "eq" +) + +// ImageDateQuery represents a date field to be used for listing images. +// If no filter is specified, the query will act as though FilterEQ was +// set. +type ImageDateQuery struct { + Date time.Time + Filter ImageDateFilter +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go new file mode 100644 index 00000000000..1780c3c6ca7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/imageservice/v2/images/urls.go @@ -0,0 +1,65 @@ +package images + +import ( + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack/utils" +) + +// `listURL` is a pure function. `listURL(c)` is a URL for which a GET +// request will respond with a list of images in the service `c`. +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("images") +} + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("images") +} + +// `imageURL(c,i)` is the URL for the image identified by ID `i` in +// the service `c`. +func imageURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID) +} + +// `getURL(c,i)` is a URL for which a GET request will respond with +// information about the image identified by ID `i` in the service +// `c`. +func getURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func updateURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func deleteURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +// builds next page full url based on current url +func nextPageURL(serviceURL, requestedNext string) (string, error) { + base, err := utils.BaseEndpoint(serviceURL) + if err != nil { + return "", err + } + + requestedNextURL, err := url.Parse(requestedNext) + if err != nil { + return "", err + } + + base = gophercloud.NormalizeURL(base) + nextPath := base + strings.TrimPrefix(requestedNextURL.Path, "/") + + nextURL, err := url.Parse(nextPath) + if err != nil { + return "", err + } + + nextURL.RawQuery = requestedNextURL.RawQuery + + return nextURL.String(), nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go new file mode 100644 index 00000000000..9d1dd5a7ea4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/doc.go @@ -0,0 +1,66 @@ +/* +Package networks contains functionality for working with Neutron network +resources. A network is an isolated virtual layer-2 broadcast domain that is +typically reserved for the tenant who created it (unless you configure the +network to be shared). Tenants can create multiple networks until the +thresholds per-tenant quota is reached. + +In the v2.0 Networking API, the network is the main entity. Ports and subnets +are always associated with a network. + +Example to List Networks + + listOpts := networks.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := networks.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allNetworks, err := networks.ExtractNetworks(allPages) + if err != nil { + panic(err) + } + + for _, network := range allNetworks { + fmt.Printf("%+v", network) + } + +Example to Create a Network + + iTrue := true + createOpts := networks.CreateOpts{ + Name: "network_1", + AdminStateUp: &iTrue, + } + + network, err := networks.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Network + + networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + + name := "new_name" + updateOpts := networks.UpdateOpts{ + Name: &name, + } + + network, err := networks.Update(networkClient, networkID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Network + + networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + err := networks.Delete(networkClient, networkID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package networks diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go new file mode 100644 index 00000000000..00c2eae77df --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/requests.go @@ -0,0 +1,165 @@ +package networks + +import ( + "fmt" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToNetworkListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the network attributes you want to see returned. SortKey allows you to sort +// by a particular network attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Shared *bool `q:"shared"` + ID string `q:"id"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToNetworkListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// networks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToNetworkListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific network based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(getURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToNetworkCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents options used to create a network. +type CreateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Shared *bool `json:"shared,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + AvailabilityZoneHints []string `json:"availability_zone_hints,omitempty"` +} + +// ToNetworkCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "network") +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +// +// The tenant ID that is contained in the URI is the tenant that creates the +// network. An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToNetworkCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(createURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToNetworkUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents options used to update a network. +type UpdateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Shared *bool `json:"shared,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` +} + +// ToNetworkUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "network") +} + +// Update accepts a UpdateOpts struct and updates an existing network using the +// values provided. For more information, see the Create function. +func Update(c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToNetworkUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + resp, err := c.Put(updateURL(c, networkID), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(c *gophercloud.ServiceClient, networkID string) (r DeleteResult) { + resp, err := c.Delete(deleteURL(c, networkID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go new file mode 100644 index 00000000000..2a020a13f05 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/results.go @@ -0,0 +1,177 @@ +package networks + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*Network, error) { + var s Network + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "network") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Network. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Network. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Network. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Network represents, well, a network. +type Network struct { + // UUID for the network + ID string `json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `json:"name"` + + // Description for the network + Description string `json:"description"` + + // The administrative state of network. If false (down), the network does not + // forward packets. + AdminStateUp bool `json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional + // values. + Status string `json:"status"` + + // Subnets associated with this network. + Subnets []string `json:"subnets"` + + // TenantID is the project owner of the network. + TenantID string `json:"tenant_id"` + + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // network last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + + // ProjectID is the project owner of the network. + ProjectID string `json:"project_id"` + + // Specifies whether the network resource can be accessed by any tenant. + Shared bool `json:"shared"` + + // Availability zone hints groups network nodes that run services like DHCP, L3, FW, and others. + // Used to make network resources highly available. + AvailabilityZoneHints []string `json:"availability_zone_hints"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` +} + +func (r *Network) UnmarshalJSON(b []byte) error { + type tmp Network + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Network(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Network(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// NetworkPage is the page returned by a pager when traversing over a +// collection of networks. +type NetworkPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of networks has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r NetworkPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"networks_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (r NetworkPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractNetworks(r) + return len(is) == 0, err +} + +// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct, +// and extracts the elements into a slice of Network structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractNetworks(r pagination.Page) ([]Network, error) { + var s []Network + err := ExtractNetworksInto(r, &s) + return s, err +} + +func ExtractNetworksInto(r pagination.Page, v interface{}) error { + return r.(NetworkPage).Result.ExtractIntoSlicePtr(v, "networks") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go new file mode 100644 index 00000000000..4a8fb1dc7d3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/networks/urls.go @@ -0,0 +1,31 @@ +package networks + +import "github.com/gophercloud/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("networks", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("networks") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go new file mode 100644 index 00000000000..cfb1774fb4b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/doc.go @@ -0,0 +1,73 @@ +/* +Package ports contains functionality for working with Neutron port resources. + +A port represents a virtual switch port on a logical network switch. Virtual +instances attach their interfaces into ports. The logical port also defines +the MAC address and the IP address(es) to be assigned to the interfaces +plugged into them. When IP addresses are associated to a port, this also +implies the port is associated with a subnet, as the IP address was taken +from the allocation pool for a specific subnet. + +Example to List Ports + + listOpts := ports.ListOpts{ + DeviceID: "b0b89efe-82f8-461d-958b-adbf80f50c7d", + } + + allPages, err := ports.List(networkClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allPorts, err := ports.ExtractPorts(allPages) + if err != nil { + panic(err) + } + + for _, port := range allPorts { + fmt.Printf("%+v\n", port) + } + +Example to Create a Port + + createOtps := ports.CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }, + SecurityGroups: &[]string{"foo"}, + AllowedAddressPairs: []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }, + } + + port, err := ports.Create(networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Port + + portID := "c34bae2b-7641-49b6-bf6d-d8e473620ed8" + + updateOpts := ports.UpdateOpts{ + Name: "new_name", + SecurityGroups: &[]string{}, + } + + port, err := ports.Update(networkClient, portID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Port + + portID := "c34bae2b-7641-49b6-bf6d-d8e473620ed8" + err := ports.Delete(networkClient, portID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package ports diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go new file mode 100644 index 00000000000..805f0e5b994 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/requests.go @@ -0,0 +1,209 @@ +package ports + +import ( + "fmt" + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPortListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the port attributes you want to see returned. SortKey allows you to sort +// by a particular port attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + DeviceOwner string `q:"device_owner"` + MACAddress string `q:"mac_address"` + ID string `q:"id"` + DeviceID string `q:"device_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + SecurityGroups []string `q:"security_groups"` + FixedIPs []FixedIPOpts +} + +type FixedIPOpts struct { + IPAddress string + IPAddressSubstr string + SubnetID string +} + +func (f FixedIPOpts) String() string { + var res []string + if f.IPAddress != "" { + res = append(res, fmt.Sprintf("ip_address=%s", f.IPAddress)) + } + if f.IPAddressSubstr != "" { + res = append(res, fmt.Sprintf("ip_address_substr=%s", f.IPAddressSubstr)) + } + if f.SubnetID != "" { + res = append(res, fmt.Sprintf("subnet_id=%s", f.SubnetID)) + } + return strings.Join(res, ",") +} + +// ToPortListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPortListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + params := q.Query() + for _, fixedIP := range opts.FixedIPs { + params.Add("fixed_ips", fixedIP.String()) + } + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// ports. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those ports that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToPortListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PortPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific port based on its unique ID. +func Get(c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(getURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToPortCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents the attributes used when creating a new port. +type CreateOpts struct { + NetworkID string `json:"network_id" required:"true"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + MACAddress string `json:"mac_address,omitempty"` + FixedIPs interface{} `json:"fixed_ips,omitempty"` + DeviceID string `json:"device_id,omitempty"` + DeviceOwner string `json:"device_owner,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + SecurityGroups *[]string `json:"security_groups,omitempty"` + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"` + PropagateUplinkStatus *bool `json:"propagate_uplink_status,omitempty"` + ValueSpecs *map[string]string `json:"value_specs,omitempty"` +} + +// ToPortCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToPortCreateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "port") +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. You must remember to provide a NetworkID value. +func Create(c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPortCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(createURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents the attributes used when updating an existing port. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + FixedIPs interface{} `json:"fixed_ips,omitempty"` + DeviceID *string `json:"device_id,omitempty"` + DeviceOwner *string `json:"device_owner,omitempty"` + SecurityGroups *[]string `json:"security_groups,omitempty"` + AllowedAddressPairs *[]AddressPair `json:"allowed_address_pairs,omitempty"` + PropagateUplinkStatus *bool `json:"propagate_uplink_status,omitempty"` + ValueSpecs *map[string]string `json:"value_specs,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` +} + +// ToPortUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]interface{}, error) { + return gophercloud.BuildRequestBody(opts, "port") +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + resp, err := c.Put(updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete accepts a unique ID and deletes the port associated with it. +func Delete(c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(deleteURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go new file mode 100644 index 00000000000..a39133fc067 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/results.go @@ -0,0 +1,209 @@ +package ports + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a port resource. +func (r commonResult) Extract() (*Port, error) { + var s Port + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v interface{}) error { + return r.Result.ExtractIntoStructPtr(v, "port") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Port. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Port. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Port. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// IP is a sub-struct that represents an individual IP. +type IP struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address,omitempty"` +} + +// AddressPair contains the IP Address and the MAC address. +type AddressPair struct { + IPAddress string `json:"ip_address,omitempty"` + MACAddress string `json:"mac_address,omitempty"` +} + +// Port represents a Neutron port. See package documentation for a top-level +// description of what this is. +type Port struct { + // UUID for the port. + ID string `json:"id"` + + // Network that this port is associated with. + NetworkID string `json:"network_id"` + + // Human-readable name for the port. Might not be unique. + Name string `json:"name"` + + // Describes the port. + Description string `json:"description"` + + // Administrative state of port. If false (down), port does not forward + // packets. + AdminStateUp bool `json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional + // values. + Status string `json:"status"` + + // Mac address to use on this port. + MACAddress string `json:"mac_address"` + + // Specifies IP addresses for the port thus associating the port itself with + // the subnets where the IP addresses are picked from + FixedIPs []IP `json:"fixed_ips"` + + // TenantID is the project owner of the port. + TenantID string `json:"tenant_id"` + + // ProjectID is the project owner of the port. + ProjectID string `json:"project_id"` + + // Identifies the entity (e.g.: dhcp agent) using this port. + DeviceOwner string `json:"device_owner"` + + // Specifies the IDs of any security groups associated with a port. + SecurityGroups []string `json:"security_groups"` + + // Identifies the device (e.g., virtual server) using this port. + DeviceID string `json:"device_id"` + + // Identifies the list of IP addresses the port will recognize/accept + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` + + // PropagateUplinkStatus enables/disables propagate uplink status on the port. + PropagateUplinkStatus bool `json:"propagate_uplink_status"` + + // Extra parameters to include in the request. + ValueSpecs map[string]string `json:"value_specs"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` + + // Timestamp when the port was created + CreatedAt time.Time `json:"created_at"` + + // Timestamp when the port was last updated + UpdatedAt time.Time `json:"updated_at"` +} + +func (r *Port) UnmarshalJSON(b []byte) error { + type tmp Port + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Port(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Port(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// PortPage is the page returned by a pager when traversing over a collection +// of network ports. +type PortPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of ports has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r PortPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"ports_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PortPage struct is empty. +func (r PortPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractPorts(r) + return len(is) == 0, err +} + +// ExtractPorts accepts a Page struct, specifically a PortPage struct, +// and extracts the elements into a slice of Port structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPorts(r pagination.Page) ([]Port, error) { + var s []Port + err := ExtractPortsInto(r, &s) + return s, err +} + +func ExtractPortsInto(r pagination.Page, v interface{}) error { + return r.(PortPage).Result.ExtractIntoSlicePtr(v, "ports") +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go new file mode 100644 index 00000000000..600d6f2fd95 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/networking/v2/ports/urls.go @@ -0,0 +1,31 @@ +package ports + +import "github.com/gophercloud/gophercloud" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("ports", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ports") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/utils/base_endpoint.go b/vendor/github.com/gophercloud/gophercloud/openstack/utils/base_endpoint.go new file mode 100644 index 00000000000..40080f7af20 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/utils/base_endpoint.go @@ -0,0 +1,28 @@ +package utils + +import ( + "net/url" + "regexp" + "strings" +) + +// BaseEndpoint will return a URL without the /vX.Y +// portion of the URL. +func BaseEndpoint(endpoint string) (string, error) { + u, err := url.Parse(endpoint) + if err != nil { + return "", err + } + + u.RawQuery, u.Fragment = "", "" + + path := u.Path + versionRe := regexp.MustCompile("v[0-9.]+/?") + + if version := versionRe.FindString(path); version != "" { + versionIndex := strings.Index(path, version) + u.Path = path[:versionIndex] + } + + return u.String(), nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go b/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go new file mode 100644 index 00000000000..27da19f91a8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/openstack/utils/choose_version.go @@ -0,0 +1,111 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// Version is a supported API version, corresponding to a vN package within the appropriate service. +type Version struct { + ID string + Suffix string + Priority int +} + +var goodStatus = map[string]bool{ + "current": true, + "supported": true, + "stable": true, +} + +// 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, as well as its corresponding endpoint. +func ChooseVersion(client *gophercloud.ProviderClient, recognized []*Version) (*Version, string, error) { + type linkResp struct { + Href string `json:"href"` + Rel string `json:"rel"` + } + + type valueResp struct { + ID string `json:"id"` + Status string `json:"status"` + Links []linkResp `json:"links"` + } + + type versionsResp struct { + Values []valueResp `json:"values"` + } + + type response struct { + Versions versionsResp `json:"versions"` + } + + normalize := func(endpoint string) string { + if !strings.HasSuffix(endpoint, "/") { + return endpoint + "/" + } + return endpoint + } + identityEndpoint := normalize(client.IdentityEndpoint) + + // If a full endpoint is specified, check version suffixes for a match first. + for _, v := range recognized { + if strings.HasSuffix(identityEndpoint, v.Suffix) { + return v, identityEndpoint, nil + } + } + + var resp response + _, err := client.Request("GET", client.IdentityBase, &gophercloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + + if err != nil { + return nil, "", err + } + + var highest *Version + var endpoint string + + for _, value := range resp.Versions.Values { + href := "" + for _, link := range value.Links { + if link.Rel == "self" { + href = normalize(link.Href) + } + } + + for _, version := range recognized { + if strings.Contains(value.ID, version.ID) { + // Prefer a version that exactly matches the provided endpoint. + if href == identityEndpoint { + if href == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, client.IdentityBase) + } + return version, href, nil + } + + // Otherwise, find the highest-priority version with a whitelisted status. + if goodStatus[strings.ToLower(value.Status)] { + if highest == nil || version.Priority > highest.Priority { + highest = version + endpoint = href + } + } + } + } + } + + if highest == nil { + return nil, "", fmt.Errorf("No supported version available from endpoint %s", client.IdentityBase) + } + if endpoint == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, client.IdentityBase) + } + + return highest, endpoint, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/http.go b/vendor/github.com/gophercloud/gophercloud/pagination/http.go new file mode 100644 index 00000000000..7845cda13b7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/http.go @@ -0,0 +1,62 @@ +package pagination + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/gophercloud/gophercloud" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + gophercloud.Result + url.URL +} + +// PageResultFrom parses an HTTP response as JSON and returns a PageResult containing the +// results, interpreting it as JSON if the content type indicates. +func PageResultFrom(resp *http.Response) (PageResult, error) { + var parsedBody interface{} + + defer resp.Body.Close() + rawBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return PageResult{}, err + } + + if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + err = json.Unmarshal(rawBody, &parsedBody) + if err != nil { + return PageResult{}, err + } + } else { + parsedBody = rawBody + } + + return PageResultFromParsed(resp, parsedBody), err +} + +// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its +// body parsed as JSON (and closed). +func PageResultFromParsed(resp *http.Response, body interface{}) PageResult { + return PageResult{ + Result: gophercloud.Result{ + Body: body, + StatusCode: resp.StatusCode, + Header: resp.Header, + }, + URL: *resp.Request.URL, + } +} + +// Request performs an HTTP request and extracts the http.Response from the result. +func Request(client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) { + return client.Get(url, nil, &gophercloud.RequestOpts{ + MoreHeaders: headers, + OkCodes: []int{200, 204, 300}, + KeepResponseBody: true, + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/linked.go b/vendor/github.com/gophercloud/gophercloud/pagination/linked.go new file mode 100644 index 00000000000..a664e056738 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/linked.go @@ -0,0 +1,92 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result. +type LinkedPageBase struct { + PageResult + + // LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer. + // If any link along the path is missing, an empty URL will be returned. + // If any link results in an unexpected value type, an error will be returned. + // When left as "nil", []string{"links", "next"} will be used as a default. + LinkPath []string +} + +// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present. +// It assumes that the links are available in a "links" element of the top-level response object. +// If this is not the case, override NextPageURL on your result type. +func (current LinkedPageBase) NextPageURL() (string, error) { + var path []string + var key string + + if current.LinkPath == nil { + path = []string{"links", "next"} + } else { + path = current.LinkPath + } + + submap, ok := current.Body.(map[string]interface{}) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return "", err + } + + for { + key, path = path[0], path[1:] + + value, ok := submap[key] + if !ok { + return "", nil + } + + if len(path) > 0 { + submap, ok = value.(map[string]interface{}) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + } else { + if value == nil { + // Actual null element. + return "", nil + } + + url, ok := value.(string) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "string" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + + return url, nil + } + } +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current LinkedPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current LinkedPageBase) GetBody() interface{} { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/marker.go b/vendor/github.com/gophercloud/gophercloud/pagination/marker.go new file mode 100644 index 00000000000..52e53bae850 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/marker.go @@ -0,0 +1,58 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager. +// For convenience, embed the MarkedPageBase struct. +type MarkerPage interface { + Page + + // LastMarker returns the last "marker" value on this page. + LastMarker() (string, error) +} + +// MarkerPageBase is a page in a collection that's paginated by "limit" and "marker" query parameters. +type MarkerPageBase struct { + PageResult + + // Owner is a reference to the embedding struct. + Owner MarkerPage +} + +// NextPageURL generates the URL for the page of results after this one. +func (current MarkerPageBase) NextPageURL() (string, error) { + currentURL := current.URL + + mark, err := current.Owner.LastMarker() + if err != nil { + return "", err + } + + q := currentURL.Query() + q.Set("marker", mark) + currentURL.RawQuery = q.Encode() + + return currentURL.String(), nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current MarkerPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current MarkerPageBase) GetBody() interface{} { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/pager.go b/vendor/github.com/gophercloud/gophercloud/pagination/pager.go new file mode 100644 index 00000000000..1dec2703ebf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/pager.go @@ -0,0 +1,254 @@ +package pagination + +import ( + "errors" + "fmt" + "net/http" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" +) + +var ( + // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist. + ErrPageNotAvailable = errors.New("The requested page does not exist.") +) + +// Page must be satisfied by the result type of any resource collection. +// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated. +// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs, +// instead. +// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type +// will need to implement. +type Page interface { + // NextPageURL generates the URL for the page of data that follows this collection. + // Return "" if no such page exists. + NextPageURL() (string, error) + + // IsEmpty returns true if this Page has no items in it. + IsEmpty() (bool, error) + + // GetBody returns the Page Body. This is used in the `AllPages` method. + GetBody() interface{} +} + +// Pager knows how to advance through a specific resource collection, one page at a time. +type Pager struct { + client *gophercloud.ServiceClient + + initialURL string + + createPage func(r PageResult) Page + + firstPage Page + + Err error + + // Headers supplies additional HTTP headers to populate on each paged request. + Headers map[string]string +} + +// NewPager constructs a manually-configured pager. +// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page. +func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager { + return Pager{ + client: client, + initialURL: initialURL, + createPage: createPage, + } +} + +// WithPageCreator returns a new Pager that substitutes a different page creation function. This is +// useful for overriding List functions in delegation. +func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager { + return Pager{ + client: p.client, + initialURL: p.initialURL, + createPage: createPage, + } +} + +func (p Pager) fetchNextPage(url string) (Page, error) { + resp, err := Request(p.client, p.Headers, url) + if err != nil { + return nil, err + } + + remembered, err := PageResultFrom(resp) + if err != nil { + return nil, err + } + + return p.createPage(remembered), nil +} + +// EachPage iterates over each page returned by a Pager, yielding one at a time to a handler function. +// Return "false" from the handler to prematurely stop iterating. +func (p Pager) EachPage(handler func(Page) (bool, error)) error { + if p.Err != nil { + return p.Err + } + currentURL := p.initialURL + for { + var currentPage Page + + // if first page has already been fetched, no need to fetch it again + if p.firstPage != nil { + currentPage = p.firstPage + p.firstPage = nil + } else { + var err error + currentPage, err = p.fetchNextPage(currentURL) + if err != nil { + return err + } + } + + empty, err := currentPage.IsEmpty() + if err != nil { + return err + } + if empty { + return nil + } + + ok, err := handler(currentPage) + if err != nil { + return err + } + if !ok { + return nil + } + + currentURL, err = currentPage.NextPageURL() + if err != nil { + return err + } + if currentURL == "" { + return nil + } + } +} + +// AllPages returns all the pages from a `List` operation in a single page, +// allowing the user to retrieve all the pages at once. +func (p Pager) AllPages() (Page, error) { + if p.Err != nil { + return nil, p.Err + } + // pagesSlice holds all the pages until they get converted into as Page Body. + var pagesSlice []interface{} + // body will contain the final concatenated Page body. + var body reflect.Value + + // Grab a first page to ascertain the page body type. + firstPage, err := p.fetchNextPage(p.initialURL) + if err != nil { + return nil, err + } + // Store the page type so we can use reflection to create a new mega-page of + // that type. + pageType := reflect.TypeOf(firstPage) + + // if it's a single page, just return the firstPage (first page) + if _, found := pageType.FieldByName("SinglePageBase"); found { + return firstPage, nil + } + + // store the first page to avoid getting it twice + p.firstPage = firstPage + + // Switch on the page body type. Recognized types are `map[string]interface{}`, + // `[]byte`, and `[]interface{}`. + switch pb := firstPage.GetBody().(type) { + case map[string]interface{}: + // key is the map key for the page body if the body type is `map[string]interface{}`. + var key string + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().(map[string]interface{}) + for k, v := range b { + // If it's a linked page, we don't want the `links`, we want the other one. + if !strings.HasSuffix(k, "links") { + // check the field's type. we only want []interface{} (which is really []map[string]interface{}) + switch vt := v.(type) { + case []interface{}: + key = k + pagesSlice = append(pagesSlice, vt...) + } + } + } + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `map[string]interface{}` + body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice))) + body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice)) + case []byte: + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().([]byte) + pagesSlice = append(pagesSlice, b) + // seperate pages with a comma + pagesSlice = append(pagesSlice, []byte{10}) + return true, nil + }) + if err != nil { + return nil, err + } + if len(pagesSlice) > 0 { + // Remove the trailing comma. + pagesSlice = pagesSlice[:len(pagesSlice)-1] + } + var b []byte + // Combine the slice of slices in to a single slice. + for _, slice := range pagesSlice { + b = append(b, slice.([]byte)...) + } + // Set body to value of type `bytes`. + body = reflect.New(reflect.TypeOf(b)).Elem() + body.SetBytes(b) + case []interface{}: + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody().([]interface{}) + pagesSlice = append(pagesSlice, b...) + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `[]interface{}` + body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice)) + for i, s := range pagesSlice { + body.Index(i).Set(reflect.ValueOf(s)) + } + default: + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]interface{}/[]byte/[]interface{}" + err.Actual = fmt.Sprintf("%T", pb) + return nil, err + } + + // Each `Extract*` function is expecting a specific type of page coming back, + // otherwise the type assertion in those functions will fail. pageType is needed + // to create a type in this method that has the same type that the `Extract*` + // function is expecting and set the Body of that object to the concatenated + // pages. + page := reflect.New(pageType) + // Set the page body to be the concatenated pages. + page.Elem().FieldByName("Body").Set(body) + // Set any additional headers that were pass along. The `objectstorage` pacakge, + // for example, passes a Content-Type header. + h := make(http.Header) + for k, v := range p.Headers { + h.Add(k, v) + } + page.Elem().FieldByName("Header").Set(reflect.ValueOf(h)) + // Type assert the page to a Page interface so that the type assertion in the + // `Extract*` methods will work. + return page.Elem().Interface().(Page), err +} diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go b/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go new file mode 100644 index 00000000000..912daea3642 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/pkg.go @@ -0,0 +1,4 @@ +/* +Package pagination contains utilities and convenience structs that implement common pagination idioms within OpenStack APIs. +*/ +package pagination diff --git a/vendor/github.com/gophercloud/gophercloud/pagination/single.go b/vendor/github.com/gophercloud/gophercloud/pagination/single.go new file mode 100644 index 00000000000..4251d6491ef --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/pagination/single.go @@ -0,0 +1,33 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud" +) + +// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once. +type SinglePageBase PageResult + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (current SinglePageBase) NextPageURL() (string, error) { + return "", nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current SinglePageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]interface{}); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]interface{}" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the single page's body. This method is needed to satisfy the +// Page interface. +func (current SinglePageBase) GetBody() interface{} { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/params.go b/vendor/github.com/gophercloud/gophercloud/params.go new file mode 100644 index 00000000000..17b200cd239 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/params.go @@ -0,0 +1,496 @@ +package gophercloud + +import ( + "encoding/json" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +/* +BuildRequestBody builds a map[string]interface from the given `struct`. If +parent is not an empty string, the final map[string]interface returned will +encapsulate the built one. For example: + + disk := 1 + createOpts := flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: &disk, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + + body, err := gophercloud.BuildRequestBody(createOpts, "flavor") + +The above example can be run as-is, however it is recommended to look at how +BuildRequestBody is used within Gophercloud to more fully understand how it +fits within the request process as a whole rather than use it directly as shown +above. +*/ +func BuildRequestBody(opts interface{}, parent string) (map[string]interface{}, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + optsMap := make(map[string]interface{}) + if optsValue.Kind() == reflect.Struct { + //fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind()) + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + + if f.Name != strings.Title(f.Name) { + //fmt.Printf("Skipping field: %s...\n", f.Name) + continue + } + + //fmt.Printf("Starting on field: %s...\n", f.Name) + + zero := isZero(v) + //fmt.Printf("v is zero?: %v\n", zero) + + // if the field has a required tag that's set to "true" + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + //fmt.Printf("Checking required field [%s]:\n\tv: %+v\n\tisZero:%v\n", f.Name, v.Interface(), zero) + // if the field's value is zero, return a missing-argument error + if zero { + // if the field has a 'required' tag, it can't have a zero-value + err := ErrMissingInput{} + err.Argument = f.Name + return nil, err + } + } + + if xorTag := f.Tag.Get("xor"); xorTag != "" { + //fmt.Printf("Checking `xor` tag for field [%s] with value %+v:\n\txorTag: %s\n", f.Name, v, xorTag) + xorField := optsValue.FieldByName(xorTag) + var xorFieldIsZero bool + if reflect.ValueOf(xorField.Interface()) == reflect.Zero(xorField.Type()) { + xorFieldIsZero = true + } else { + if xorField.Kind() == reflect.Ptr { + xorField = xorField.Elem() + } + xorFieldIsZero = isZero(xorField) + } + if !(zero != xorFieldIsZero) { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, xorTag) + err.Info = fmt.Sprintf("Exactly one of %s and %s must be provided", f.Name, xorTag) + return nil, err + } + } + + if orTag := f.Tag.Get("or"); orTag != "" { + //fmt.Printf("Checking `or` tag for field with:\n\tname: %+v\n\torTag:%s\n", f.Name, orTag) + //fmt.Printf("field is zero?: %v\n", zero) + if zero { + orField := optsValue.FieldByName(orTag) + var orFieldIsZero bool + if reflect.ValueOf(orField.Interface()) == reflect.Zero(orField.Type()) { + orFieldIsZero = true + } else { + if orField.Kind() == reflect.Ptr { + orField = orField.Elem() + } + orFieldIsZero = isZero(orField) + } + if orFieldIsZero { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, orTag) + err.Info = fmt.Sprintf("At least one of %s and %s must be provided", f.Name, orTag) + return nil, err + } + } + } + + jsonTag := f.Tag.Get("json") + if jsonTag == "-" { + continue + } + + if v.Kind() == reflect.Slice || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Slice) { + sliceValue := v + if sliceValue.Kind() == reflect.Ptr { + sliceValue = sliceValue.Elem() + } + + for i := 0; i < sliceValue.Len(); i++ { + element := sliceValue.Index(i) + if element.Kind() == reflect.Struct || (element.Kind() == reflect.Ptr && element.Elem().Kind() == reflect.Struct) { + _, err := BuildRequestBody(element.Interface(), "") + if err != nil { + return nil, err + } + } + } + } + if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) { + if zero { + //fmt.Printf("value before change: %+v\n", optsValue.Field(i)) + if jsonTag != "" { + jsonTagPieces := strings.Split(jsonTag, ",") + if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" { + if v.CanSet() { + if !v.IsNil() { + if v.Kind() == reflect.Ptr { + v.Set(reflect.Zero(v.Type())) + } + } + //fmt.Printf("value after change: %+v\n", optsValue.Field(i)) + } + } + } + continue + } + + //fmt.Printf("Calling BuildRequestBody with:\n\tv: %+v\n\tf.Name:%s\n", v.Interface(), f.Name) + _, err := BuildRequestBody(v.Interface(), f.Name) + if err != nil { + return nil, err + } + } + } + + //fmt.Printf("opts: %+v \n", opts) + + b, err := json.Marshal(opts) + if err != nil { + return nil, err + } + + //fmt.Printf("string(b): %s\n", string(b)) + + err = json.Unmarshal(b, &optsMap) + if err != nil { + return nil, err + } + + //fmt.Printf("optsMap: %+v\n", optsMap) + + if parent != "" { + optsMap = map[string]interface{}{parent: optsMap} + } + //fmt.Printf("optsMap after parent added: %+v\n", optsMap) + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("Options type is not a struct.") +} + +// EnabledState is a convenience type, mostly used in Create and Update +// operations. Because the zero value of a bool is FALSE, we need to use a +// pointer instead to indicate zero-ness. +type EnabledState *bool + +// Convenience vars for EnabledState values. +var ( + iTrue = true + iFalse = false + + Enabled EnabledState = &iTrue + Disabled EnabledState = &iFalse +) + +// IPVersion is a type for the possible IP address versions. Valid instances +// are IPv4 and IPv6 +type IPVersion int + +const ( + // IPv4 is used for IP version 4 addresses + IPv4 IPVersion = 4 + // IPv6 is used for IP version 6 addresses + IPv6 IPVersion = 6 +) + +// IntToPointer is a function for converting integers into integer pointers. +// This is useful when passing in options to operations. +func IntToPointer(i int) *int { + return &i +} + +/* +MaybeString is an internal function to be used by request methods in individual +resource packages. + +It takes a string that might be a zero value and returns either a pointer to its +address or nil. This is useful for allowing users to conveniently omit values +from an options struct by leaving them zeroed, but still pass nil to the JSON +serializer so they'll be omitted from the request body. +*/ +func MaybeString(original string) *string { + if original != "" { + return &original + } + return nil +} + +/* +MaybeInt is an internal function to be used by request methods in individual +resource packages. + +Like MaybeString, it accepts an int that may or may not be a zero value, and +returns either a pointer to its address or nil. It's intended to hint that the +JSON serializer should omit its field. +*/ +func MaybeInt(original int) *int { + if original != 0 { + return &original + } + return nil +} + +/* +func isUnderlyingStructZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Ptr: + return isUnderlyingStructZero(v.Elem()) + default: + return isZero(v) + } +} +*/ + +var t time.Time + +func isZero(v reflect.Value) bool { + //fmt.Printf("\n\nchecking isZero for value: %+v\n", v) + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return true + } + return false + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + z := true + for i := 0; i < v.Len(); i++ { + z = z && isZero(v.Index(i)) + } + return z + case reflect.Struct: + if v.Type() == reflect.TypeOf(t) { + if v.Interface().(time.Time).IsZero() { + return true + } + return false + } + z := true + for i := 0; i < v.NumField(); i++ { + z = z && isZero(v.Field(i)) + } + return z + } + // Compare other types directly: + z := reflect.Zero(v.Type()) + //fmt.Printf("zero type for value: %+v\n\n\n", z) + return v.Interface() == z.Interface() +} + +/* +BuildQueryString is an internal function to be used by request methods in +individual resource packages. + +It accepts a tagged structure and expands it into a URL struct. Field names are +converted into query parameters based on a "q" tag. For example: + + type struct Something { + Bar string `q:"x_bar"` + Baz int `q:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into "?x_bar=AAA&lorem_ipsum=BBB". + +The struct's fields may be strings, integers, or boolean values. Fields left at +their type's zero value will be omitted from the query. +*/ +func BuildQueryString(opts interface{}) (*url.URL, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + params := url.Values{} + + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + qTag := f.Tag.Get("q") + + // if the field has a 'q' tag, it goes in the query string + if qTag != "" { + tags := strings.Split(qTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + loop: + switch v.Kind() { + case reflect.Ptr: + v = v.Elem() + goto loop + case reflect.String: + params.Add(tags[0], v.String()) + case reflect.Int: + params.Add(tags[0], strconv.FormatInt(v.Int(), 10)) + case reflect.Bool: + params.Add(tags[0], strconv.FormatBool(v.Bool())) + case reflect.Slice: + switch v.Type().Elem() { + case reflect.TypeOf(0): + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], strconv.FormatInt(v.Index(i).Int(), 10)) + } + default: + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], v.Index(i).String()) + } + } + case reflect.Map: + if v.Type().Key().Kind() == reflect.String && v.Type().Elem().Kind() == reflect.String { + var s []string + for _, k := range v.MapKeys() { + value := v.MapIndex(k).String() + s = append(s, fmt.Sprintf("'%s':'%s'", k.String(), value)) + } + params.Add(tags[0], fmt.Sprintf("{%s}", strings.Join(s, ", "))) + } + } + } else { + // if the field has a 'required' tag, it can't have a zero-value + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + return &url.URL{}, fmt.Errorf("Required query parameter [%s] not set.", f.Name) + } + } + } + } + + return &url.URL{RawQuery: params.Encode()}, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("Options type is not a struct.") +} + +/* +BuildHeaders is an internal function to be used by request methods in +individual resource packages. + +It accepts an arbitrary tagged structure and produces a string map that's +suitable for use as the HTTP headers of an outgoing request. Field names are +mapped to header names based in "h" tags. + + type struct Something { + Bar string `h:"x_bar"` + Baz int `h:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into: + + map[string]string{ + "x_bar": "AAA", + "lorem_ipsum": "BBB", + } + +Untagged fields and fields left at their zero values are skipped. Integers, +booleans and string values are supported. +*/ +func BuildHeaders(opts interface{}) (map[string]string, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + optsMap := make(map[string]string) + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + hTag := f.Tag.Get("h") + + // if the field has a 'h' tag, it goes in the header + if hTag != "" { + tags := strings.Split(hTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + switch v.Kind() { + case reflect.String: + optsMap[tags[0]] = v.String() + case reflect.Int: + optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10) + case reflect.Int64: + optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10) + case reflect.Bool: + optsMap[tags[0]] = strconv.FormatBool(v.Bool()) + } + } else { + // if the field has a 'required' tag, it can't have a zero-value + if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + return optsMap, fmt.Errorf("Required header [%s] not set.", f.Name) + } + } + } + + } + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return optsMap, fmt.Errorf("Options type is not a struct.") +} + +// IDSliceToQueryString takes a slice of elements and converts them into a query +// string. For example, if name=foo and slice=[]int{20, 40, 60}, then the +// result would be `?name=20&name=40&name=60' +func IDSliceToQueryString(name string, ids []int) string { + str := "" + for k, v := range ids { + if k == 0 { + str += "?" + } else { + str += "&" + } + str += fmt.Sprintf("%s=%s", name, strconv.Itoa(v)) + } + return str +} + +// IntWithinRange returns TRUE if an integer falls within a defined range, and +// FALSE if not. +func IntWithinRange(val, min, max int) bool { + return val > min && val < max +} diff --git a/vendor/github.com/gophercloud/gophercloud/provider_client.go b/vendor/github.com/gophercloud/gophercloud/provider_client.go new file mode 100644 index 00000000000..e53b713cd4f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/provider_client.go @@ -0,0 +1,652 @@ +package gophercloud + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "io/ioutil" + "net/http" + "strings" + "sync" +) + +// DefaultUserAgent is the default User-Agent string set in the request header. +const ( + DefaultUserAgent = "gophercloud/v1.7.0" + DefaultMaxBackoffRetries = 60 +) + +// UserAgent represents a User-Agent header. +type UserAgent struct { + // prepend is the slice of User-Agent strings to prepend to DefaultUserAgent. + // All the strings to prepend are accumulated and prepended in the Join method. + prepend []string +} + +type RetryBackoffFunc func(context.Context, *ErrUnexpectedResponseCode, error, uint) error + +// RetryFunc is a catch-all function for retrying failed API requests. +// If it returns nil, the request will be retried. If it returns an error, +// the request method will exit with that error. failCount is the number of +// times the request has failed (starting at 1). +type RetryFunc func(context context.Context, method, url string, options *RequestOpts, err error, failCount uint) error + +// Prepend prepends a user-defined string to the default User-Agent string. Users +// may pass in one or more strings to prepend. +func (ua *UserAgent) Prepend(s ...string) { + ua.prepend = append(s, ua.prepend...) +} + +// Join concatenates all the user-defined User-Agend strings with the default +// Gophercloud User-Agent string. +func (ua *UserAgent) Join() string { + uaSlice := append(ua.prepend, DefaultUserAgent) + return strings.Join(uaSlice, " ") +} + +// ProviderClient stores details that are required to interact with any +// services within a specific provider's API. +// +// Generally, you acquire a ProviderClient by calling the NewClient method in +// the appropriate provider's child package, providing whatever authentication +// credentials are required. +type ProviderClient struct { + // IdentityBase is the base URL used for a particular provider's identity + // service - it will be used when issuing authenticatation requests. It + // should point to the root resource of the identity service, not a specific + // identity version. + IdentityBase string + + // IdentityEndpoint is the identity endpoint. This may be a specific version + // of the identity service. If this is the case, this endpoint is used rather + // than querying versions first. + IdentityEndpoint string + + // TokenID is the ID of the most recently issued valid token. + // NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application. + // To safely read or write this value, call `Token` or `SetToken`, respectively + TokenID string + + // EndpointLocator describes how this provider discovers the endpoints for + // its constituent services. + EndpointLocator EndpointLocator + + // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors. + HTTPClient http.Client + + // UserAgent represents the User-Agent header in the HTTP request. + UserAgent UserAgent + + // ReauthFunc is the function used to re-authenticate the user if the request + // fails with a 401 HTTP response code. This a needed because there may be multiple + // authentication functions for different Identity service versions. + ReauthFunc func() error + + // Throwaway determines whether if this client is a throw-away client. It's a copy of user's provider client + // with the token and reauth func zeroed. Such client can be used to perform reauthorization. + Throwaway bool + + // Context is the context passed to the HTTP request. + Context context.Context + + // Retry backoff func is called when rate limited. + RetryBackoffFunc RetryBackoffFunc + + // MaxBackoffRetries set the maximum number of backoffs. When not set, defaults to DefaultMaxBackoffRetries + MaxBackoffRetries uint + + // A general failed request handler method - this is always called in the end if a request failed. Leave as nil + // to abort when an error is encountered. + RetryFunc RetryFunc + + // mut is a mutex for the client. It protects read and write access to client attributes such as getting + // and setting the TokenID. + mut *sync.RWMutex + + // reauthmut is a mutex for reauthentication it attempts to ensure that only one reauthentication + // attempt happens at one time. + reauthmut *reauthlock + + authResult AuthResult +} + +// reauthlock represents a set of attributes used to help in the reauthentication process. +type reauthlock struct { + sync.RWMutex + ongoing *reauthFuture +} + +// reauthFuture represents future result of the reauthentication process. +// while done channel is not closed, reauthentication is in progress. +// when done channel is closed, err contains the result of reauthentication. +type reauthFuture struct { + done chan struct{} + err error +} + +func newReauthFuture() *reauthFuture { + return &reauthFuture{ + make(chan struct{}), + nil, + } +} + +func (f *reauthFuture) Set(err error) { + f.err = err + close(f.done) +} + +func (f *reauthFuture) Get() error { + <-f.done + return f.err +} + +// AuthenticatedHeaders returns a map of HTTP headers that are common for all +// authenticated service requests. Blocks if Reauthenticate is in progress. +func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) { + if client.IsThrowaway() { + return + } + if client.reauthmut != nil { + // If a Reauthenticate is in progress, wait for it to complete. + client.reauthmut.Lock() + ongoing := client.reauthmut.ongoing + client.reauthmut.Unlock() + if ongoing != nil { + _ = ongoing.Get() + } + } + t := client.Token() + if t == "" { + return + } + return map[string]string{"X-Auth-Token": t} +} + +// UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token. +// If the application's ProviderClient is not used concurrently, this doesn't need to be called. +func (client *ProviderClient) UseTokenLock() { + client.mut = new(sync.RWMutex) + client.reauthmut = new(reauthlock) +} + +// GetAuthResult returns the result from the request that was used to obtain a +// provider client's Keystone token. +// +// The result is nil when authentication has not yet taken place, when the token +// was set manually with SetToken(), or when a ReauthFunc was used that does not +// record the AuthResult. +func (client *ProviderClient) GetAuthResult() AuthResult { + if client.mut != nil { + client.mut.RLock() + defer client.mut.RUnlock() + } + return client.authResult +} + +// Token safely reads the value of the auth token from the ProviderClient. Applications should +// call this method to access the token instead of the TokenID field +func (client *ProviderClient) Token() string { + if client.mut != nil { + client.mut.RLock() + defer client.mut.RUnlock() + } + return client.TokenID +} + +// SetToken safely sets the value of the auth token in the ProviderClient. Applications may +// use this method in a custom ReauthFunc. +// +// WARNING: This function is deprecated. Use SetTokenAndAuthResult() instead. +func (client *ProviderClient) SetToken(t string) { + if client.mut != nil { + client.mut.Lock() + defer client.mut.Unlock() + } + client.TokenID = t + client.authResult = nil +} + +// SetTokenAndAuthResult safely sets the value of the auth token in the +// ProviderClient and also records the AuthResult that was returned from the +// token creation request. Applications may call this in a custom ReauthFunc. +func (client *ProviderClient) SetTokenAndAuthResult(r AuthResult) error { + tokenID := "" + var err error + if r != nil { + tokenID, err = r.ExtractTokenID() + if err != nil { + return err + } + } + + if client.mut != nil { + client.mut.Lock() + defer client.mut.Unlock() + } + client.TokenID = tokenID + client.authResult = r + return nil +} + +// CopyTokenFrom safely copies the token from another ProviderClient into the +// this one. +func (client *ProviderClient) CopyTokenFrom(other *ProviderClient) { + if client.mut != nil { + client.mut.Lock() + defer client.mut.Unlock() + } + if other.mut != nil && other.mut != client.mut { + other.mut.RLock() + defer other.mut.RUnlock() + } + client.TokenID = other.TokenID + client.authResult = other.authResult +} + +// IsThrowaway safely reads the value of the client Throwaway field. +func (client *ProviderClient) IsThrowaway() bool { + if client.reauthmut != nil { + client.reauthmut.RLock() + defer client.reauthmut.RUnlock() + } + return client.Throwaway +} + +// SetThrowaway safely sets the value of the client Throwaway field. +func (client *ProviderClient) SetThrowaway(v bool) { + if client.reauthmut != nil { + client.reauthmut.Lock() + defer client.reauthmut.Unlock() + } + client.Throwaway = v +} + +// Reauthenticate calls client.ReauthFunc in a thread-safe way. If this is +// called because of a 401 response, the caller may pass the previous token. In +// this case, the reauthentication can be skipped if another thread has already +// reauthenticated in the meantime. If no previous token is known, an empty +// string should be passed instead to force unconditional reauthentication. +func (client *ProviderClient) Reauthenticate(previousToken string) error { + if client.ReauthFunc == nil { + return nil + } + + if client.reauthmut == nil { + return client.ReauthFunc() + } + + future := newReauthFuture() + + // Check if a Reauthenticate is in progress, or start one if not. + client.reauthmut.Lock() + ongoing := client.reauthmut.ongoing + if ongoing == nil { + client.reauthmut.ongoing = future + } + client.reauthmut.Unlock() + + // If Reauthenticate is running elsewhere, wait for its result. + if ongoing != nil { + return ongoing.Get() + } + + // Perform the actual reauthentication. + var err error + if previousToken == "" || client.TokenID == previousToken { + err = client.ReauthFunc() + } else { + err = nil + } + + // Mark Reauthenticate as finished. + client.reauthmut.Lock() + client.reauthmut.ongoing.Set(err) + client.reauthmut.ongoing = nil + client.reauthmut.Unlock() + + return err +} + +// RequestOpts customizes the behavior of the provider.Request() method. +type RequestOpts struct { + // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The + // content type of the request will default to "application/json" unless overridden by MoreHeaders. + // It's an error to specify both a JSONBody and a RawBody. + JSONBody interface{} + // RawBody contains an io.Reader that will be consumed by the request directly. No content-type + // will be set unless one is provided explicitly by MoreHeaders. + RawBody io.Reader + // JSONResponse, if provided, will be populated with the contents of the response body parsed as + // JSON. + JSONResponse interface{} + // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If + // the response has a different code, an error will be returned. + OkCodes []int + // MoreHeaders specifies additional HTTP headers to be provided on the request. + // MoreHeaders will be overridden by OmitHeaders + MoreHeaders map[string]string + // OmitHeaders specifies the HTTP headers which should be omitted. + // OmitHeaders will override MoreHeaders + OmitHeaders []string + // ErrorContext specifies the resource error type to return if an error is encountered. + // This lets resources override default error messages based on the response status code. + ErrorContext error + // KeepResponseBody specifies whether to keep the HTTP response body. Usually used, when the HTTP + // response body is considered for further use. Valid when JSONResponse is nil. + KeepResponseBody bool +} + +// requestState contains temporary state for a single ProviderClient.Request() call. +type requestState struct { + // This flag indicates if we have reauthenticated during this request because of a 401 response. + // It ensures that we don't reauthenticate multiple times for a single request. If we + // reauthenticate, but keep getting 401 responses with the fresh token, reauthenticating some more + // will just get us into an infinite loop. + hasReauthenticated bool + // Retry-After backoff counter, increments during each backoff call + retries uint +} + +var applicationJSON = "application/json" + +// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication +// header will automatically be provided. +func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { + return client.doRequest(method, url, options, &requestState{ + hasReauthenticated: false, + }) +} + +func (client *ProviderClient) doRequest(method, url string, options *RequestOpts, state *requestState) (*http.Response, error) { + var body io.Reader + var contentType *string + + // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided + // io.ReadSeeker as-is. Default the content-type to application/json. + if options.JSONBody != nil { + if options.RawBody != nil { + return nil, errors.New("please provide only one of JSONBody or RawBody to gophercloud.Request()") + } + + rendered, err := json.Marshal(options.JSONBody) + if err != nil { + return nil, err + } + + body = bytes.NewReader(rendered) + contentType = &applicationJSON + } + + // Return an error, when "KeepResponseBody" is true and "JSONResponse" is not nil + if options.KeepResponseBody && options.JSONResponse != nil { + return nil, errors.New("cannot use KeepResponseBody when JSONResponse is not nil") + } + + if options.RawBody != nil { + body = options.RawBody + } + + // Construct the http.Request. + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + if client.Context != nil { + req = req.WithContext(client.Context) + } + + // Populate the request headers. + // Apply options.MoreHeaders and options.OmitHeaders, to give the caller the chance to + // modify or omit any header. + if contentType != nil { + req.Header.Set("Content-Type", *contentType) + } + req.Header.Set("Accept", applicationJSON) + + // Set the User-Agent header + req.Header.Set("User-Agent", client.UserAgent.Join()) + + if options.MoreHeaders != nil { + for k, v := range options.MoreHeaders { + req.Header.Set(k, v) + } + } + + for _, v := range options.OmitHeaders { + req.Header.Del(v) + } + + // get latest token from client + for k, v := range client.AuthenticatedHeaders() { + req.Header.Set(k, v) + } + + prereqtok := req.Header.Get("X-Auth-Token") + + // Issue the request. + resp, err := client.HTTPClient.Do(req) + if err != nil { + if client.RetryFunc != nil { + var e error + state.retries = state.retries + 1 + e = client.RetryFunc(client.Context, method, url, options, err, state.retries) + if e != nil { + return nil, e + } + + return client.doRequest(method, url, options, state) + } + return nil, err + } + + // Allow default OkCodes if none explicitly set + okc := options.OkCodes + if okc == nil { + okc = defaultOkCodes(method) + } + + // Validate the HTTP response status. + var ok bool + for _, code := range okc { + if resp.StatusCode == code { + ok = true + break + } + } + + if !ok { + body, _ := ioutil.ReadAll(resp.Body) + resp.Body.Close() + respErr := ErrUnexpectedResponseCode{ + URL: url, + Method: method, + Expected: okc, + Actual: resp.StatusCode, + Body: body, + ResponseHeader: resp.Header, + } + + errType := options.ErrorContext + switch resp.StatusCode { + case http.StatusBadRequest: + err = ErrDefault400{respErr} + if error400er, ok := errType.(Err400er); ok { + err = error400er.Error400(respErr) + } + case http.StatusUnauthorized: + if client.ReauthFunc != nil && !state.hasReauthenticated { + err = client.Reauthenticate(prereqtok) + if err != nil { + e := &ErrUnableToReauthenticate{} + e.ErrOriginal = respErr + e.ErrReauth = err + return nil, e + } + if options.RawBody != nil { + if seeker, ok := options.RawBody.(io.Seeker); ok { + seeker.Seek(0, 0) + } + } + state.hasReauthenticated = true + resp, err = client.doRequest(method, url, options, state) + if err != nil { + switch err.(type) { + case *ErrUnexpectedResponseCode: + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err.(*ErrUnexpectedResponseCode) + return nil, e + default: + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err + return nil, e + } + } + return resp, nil + } + err = ErrDefault401{respErr} + if error401er, ok := errType.(Err401er); ok { + err = error401er.Error401(respErr) + } + case http.StatusForbidden: + err = ErrDefault403{respErr} + if error403er, ok := errType.(Err403er); ok { + err = error403er.Error403(respErr) + } + case http.StatusNotFound: + err = ErrDefault404{respErr} + if error404er, ok := errType.(Err404er); ok { + err = error404er.Error404(respErr) + } + case http.StatusMethodNotAllowed: + err = ErrDefault405{respErr} + if error405er, ok := errType.(Err405er); ok { + err = error405er.Error405(respErr) + } + case http.StatusRequestTimeout: + err = ErrDefault408{respErr} + if error408er, ok := errType.(Err408er); ok { + err = error408er.Error408(respErr) + } + case http.StatusConflict: + err = ErrDefault409{respErr} + if error409er, ok := errType.(Err409er); ok { + err = error409er.Error409(respErr) + } + case http.StatusTooManyRequests, 498: + err = ErrDefault429{respErr} + if error429er, ok := errType.(Err429er); ok { + err = error429er.Error429(respErr) + } + + maxTries := client.MaxBackoffRetries + if maxTries == 0 { + maxTries = DefaultMaxBackoffRetries + } + + if f := client.RetryBackoffFunc; f != nil && state.retries < maxTries { + var e error + + state.retries = state.retries + 1 + e = f(client.Context, &respErr, err, state.retries) + + if e != nil { + return resp, e + } + + return client.doRequest(method, url, options, state) + } + case http.StatusInternalServerError: + err = ErrDefault500{respErr} + if error500er, ok := errType.(Err500er); ok { + err = error500er.Error500(respErr) + } + case http.StatusBadGateway: + err = ErrDefault502{respErr} + if error502er, ok := errType.(Err502er); ok { + err = error502er.Error502(respErr) + } + case http.StatusServiceUnavailable: + err = ErrDefault503{respErr} + if error503er, ok := errType.(Err503er); ok { + err = error503er.Error503(respErr) + } + case http.StatusGatewayTimeout: + err = ErrDefault504{respErr} + if error504er, ok := errType.(Err504er); ok { + err = error504er.Error504(respErr) + } + } + + if err == nil { + err = respErr + } + + if err != nil && client.RetryFunc != nil { + var e error + state.retries = state.retries + 1 + e = client.RetryFunc(client.Context, method, url, options, err, state.retries) + if e != nil { + return resp, e + } + + return client.doRequest(method, url, options, state) + } + + return resp, err + } + + // Parse the response body as JSON, if requested to do so. + if options.JSONResponse != nil { + defer resp.Body.Close() + // Don't decode JSON when there is no content + if resp.StatusCode == http.StatusNoContent { + // read till EOF, otherwise the connection will be closed and cannot be reused + _, err = io.Copy(ioutil.Discard, resp.Body) + return resp, err + } + if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil { + if client.RetryFunc != nil { + var e error + state.retries = state.retries + 1 + e = client.RetryFunc(client.Context, method, url, options, err, state.retries) + if e != nil { + return resp, e + } + + return client.doRequest(method, url, options, state) + } + return nil, err + } + } + + // Close unused body to allow the HTTP connection to be reused + if !options.KeepResponseBody && options.JSONResponse == nil { + defer resp.Body.Close() + // read till EOF, otherwise the connection will be closed and cannot be reused + if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil { + return nil, err + } + } + + return resp, nil +} + +func defaultOkCodes(method string) []int { + switch method { + case "GET", "HEAD": + return []int{200} + case "POST": + return []int{201, 202} + case "PUT": + return []int{201, 202} + case "PATCH": + return []int{200, 202, 204} + case "DELETE": + return []int{202, 204} + } + + return []int{} +} diff --git a/vendor/github.com/gophercloud/gophercloud/results.go b/vendor/github.com/gophercloud/gophercloud/results.go new file mode 100644 index 00000000000..b3ee9d5682d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/results.go @@ -0,0 +1,465 @@ +package gophercloud + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "strconv" + "time" +) + +/* +Result is an internal type to be used by individual resource packages, but its +methods will be available on a wide variety of user-facing embedding types. + +It acts as a base struct that other Result types, returned from request +functions, can embed for convenience. All Results capture basic information +from the HTTP transaction that was performed, including the response body, +HTTP headers, and any errors that happened. + +Generally, each Result type will have an Extract method that can be used to +further interpret the result's payload in a specific context. Extensions or +providers can then provide additional extraction functions to pull out +provider- or extension-specific information as well. +*/ +type Result struct { + // Body is the payload of the HTTP response from the server. In most cases, + // this will be the deserialized JSON structure. + Body interface{} + + // StatusCode is the HTTP status code of the original response. Will be + // one of the OkCodes defined on the gophercloud.RequestOpts that was + // used in the request. + StatusCode int + + // Header contains the HTTP header structure from the original response. + Header http.Header + + // Err is an error that occurred during the operation. It's deferred until + // extraction to make it easier to chain the Extract call. + Err error +} + +// ExtractInto allows users to provide an object into which `Extract` will extract +// the `Result.Body`. This would be useful for OpenStack providers that have +// different fields in the response object than OpenStack proper. +func (r Result) ExtractInto(to interface{}) error { + if r.Err != nil { + return r.Err + } + + if reader, ok := r.Body.(io.Reader); ok { + if readCloser, ok := reader.(io.Closer); ok { + defer readCloser.Close() + } + return json.NewDecoder(reader).Decode(to) + } + + b, err := json.Marshal(r.Body) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + return err +} + +func (r Result) extractIntoPtr(to interface{}, label string) error { + if label == "" { + return r.ExtractInto(&to) + } + + var m map[string]interface{} + err := r.ExtractInto(&m) + if err != nil { + return err + } + + b, err := json.Marshal(m[label]) + if err != nil { + return err + } + + toValue := reflect.ValueOf(to) + if toValue.Kind() == reflect.Ptr { + toValue = toValue.Elem() + } + + switch toValue.Kind() { + case reflect.Slice: + typeOfV := toValue.Type().Elem() + if typeOfV.Kind() == reflect.Struct { + if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { + newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0) + + if mSlice, ok := m[label].([]interface{}); ok { + for _, v := range mSlice { + // For each iteration of the slice, we create a new struct. + // This is to work around a bug where elements of a slice + // are reused and not overwritten when the same copy of the + // struct is used: + // + // https://github.com/golang/go/issues/21092 + // https://github.com/golang/go/issues/24155 + // https://play.golang.org/p/NHo3ywlPZli + newType := reflect.New(typeOfV).Elem() + + b, err := json.Marshal(v) + if err != nil { + return err + } + + // This is needed for structs with an UnmarshalJSON method. + // Technically this is just unmarshalling the response into + // a struct that is never used, but it's good enough to + // trigger the UnmarshalJSON method. + for i := 0; i < newType.NumField(); i++ { + s := newType.Field(i).Addr().Interface() + + // Unmarshal is used rather than NewDecoder to also work + // around the above-mentioned bug. + err = json.Unmarshal(b, s) + if err != nil { + return err + } + } + + newSlice = reflect.Append(newSlice, newType) + } + } + + // "to" should now be properly modeled to receive the + // JSON response body and unmarshal into all the correct + // fields of the struct or composed extension struct + // at the end of this method. + toValue.Set(newSlice) + + // jtopjian: This was put into place to resolve the issue + // described at + // https://github.com/gophercloud/gophercloud/issues/1963 + // + // This probably isn't the best fix, but it appears to + // be resolving the issue, so I'm going to implement it + // for now. + // + // For future readers, this entire case statement could + // use a review. + return nil + } + } + case reflect.Struct: + typeOfV := toValue.Type() + if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { + for i := 0; i < toValue.NumField(); i++ { + toField := toValue.Field(i) + if toField.Kind() == reflect.Struct { + s := toField.Addr().Interface() + err = json.NewDecoder(bytes.NewReader(b)).Decode(s) + if err != nil { + return err + } + } + } + } + } + + err = json.Unmarshal(b, &to) + return err +} + +// ExtractIntoStructPtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying struct type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +func (r Result) ExtractIntoStructPtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("Expected pointer, got %v", k) + } + switch t.Elem().Kind() { + case reflect.Struct: + return r.extractIntoPtr(to, label) + default: + return fmt.Errorf("Expected pointer to struct, got: %v", t) + } +} + +// ExtractIntoSlicePtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying slice type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +func (r Result) ExtractIntoSlicePtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("Expected pointer, got %v", k) + } + switch t.Elem().Kind() { + case reflect.Slice: + return r.extractIntoPtr(to, label) + default: + return fmt.Errorf("Expected pointer to slice, got: %v", t) + } +} + +// PrettyPrintJSON creates a string containing the full response body as +// pretty-printed JSON. It's useful for capturing test fixtures and for +// debugging extraction bugs. If you include its output in an issue related to +// a buggy extraction function, we will all love you forever. +func (r Result) PrettyPrintJSON() string { + pretty, err := json.MarshalIndent(r.Body, "", " ") + if err != nil { + panic(err.Error()) + } + return string(pretty) +} + +// ErrResult is an internal type to be used by individual resource packages, but +// its methods will be available on a wide variety of user-facing embedding +// types. +// +// It represents results that only contain a potential error and +// nothing else. Usually, if the operation executed successfully, the Err field +// will be nil; otherwise it will be stocked with a relevant error. Use the +// ExtractErr method +// to cleanly pull it out. +type ErrResult struct { + Result +} + +// ExtractErr is a function that extracts error information, or nil, from a result. +func (r ErrResult) ExtractErr() error { + return r.Err +} + +/* +HeaderResult is an internal type to be used by individual resource packages, but +its methods will be available on a wide variety of user-facing embedding types. + +It represents a result that only contains an error (possibly nil) and an +http.Header. This is used, for example, by the objectstorage packages in +openstack, because most of the operations don't return response bodies, but do +have relevant information in headers. +*/ +type HeaderResult struct { + Result +} + +// ExtractInto allows users to provide an object into which `Extract` will +// extract the http.Header headers of the result. +func (r HeaderResult) ExtractInto(to interface{}) error { + if r.Err != nil { + return r.Err + } + + tmpHeaderMap := map[string]string{} + for k, v := range r.Header { + if len(v) > 0 { + tmpHeaderMap[k] = v[0] + } + } + + b, err := json.Marshal(tmpHeaderMap) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + return err +} + +// RFC3339Milli describes a common time format used by some API responses. +const RFC3339Milli = "2006-01-02T15:04:05.999999Z" + +type JSONRFC3339Milli time.Time + +func (jt *JSONRFC3339Milli) UnmarshalJSON(data []byte) error { + b := bytes.NewBuffer(data) + dec := json.NewDecoder(b) + var s string + if err := dec.Decode(&s); err != nil { + return err + } + t, err := time.Parse(RFC3339Milli, s) + if err != nil { + return err + } + *jt = JSONRFC3339Milli(t) + return nil +} + +const RFC3339MilliNoZ = "2006-01-02T15:04:05.999999" + +type JSONRFC3339MilliNoZ time.Time + +func (jt *JSONRFC3339MilliNoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339MilliNoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339MilliNoZ(t) + return nil +} + +type JSONRFC1123 time.Time + +func (jt *JSONRFC1123) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(time.RFC1123, s) + if err != nil { + return err + } + *jt = JSONRFC1123(t) + return nil +} + +type JSONUnix time.Time + +func (jt *JSONUnix) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + unix, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + t = time.Unix(unix, 0) + *jt = JSONUnix(t) + return nil +} + +// RFC3339NoZ is the time format used in Heat (Orchestration). +const RFC3339NoZ = "2006-01-02T15:04:05" + +type JSONRFC3339NoZ time.Time + +func (jt *JSONRFC3339NoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339NoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339NoZ(t) + return nil +} + +// RFC3339ZNoT is the time format used in Zun (Containers Service). +const RFC3339ZNoT = "2006-01-02 15:04:05-07:00" + +type JSONRFC3339ZNoT time.Time + +func (jt *JSONRFC3339ZNoT) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339ZNoT, s) + if err != nil { + return err + } + *jt = JSONRFC3339ZNoT(t) + return nil +} + +// RFC3339ZNoTNoZ is another time format used in Zun (Containers Service). +const RFC3339ZNoTNoZ = "2006-01-02 15:04:05" + +type JSONRFC3339ZNoTNoZ time.Time + +func (jt *JSONRFC3339ZNoTNoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339ZNoTNoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339ZNoTNoZ(t) + return nil +} + +/* +Link is an internal type to be used in packages of collection resources that are +paginated in a certain way. + +It's a response substructure common to many paginated collection results that is +used to point to related pages. Usually, the one we care about is the one with +Rel field set to "next". +*/ +type Link struct { + Href string `json:"href"` + Rel string `json:"rel"` +} + +/* +ExtractNextURL is an internal function useful for packages of collection +resources that are paginated in a certain way. + +It attempts to extract the "next" URL from slice of Link structs, or +"" if no such URL is present. +*/ +func ExtractNextURL(links []Link) (string, error) { + var url string + + for _, l := range links { + if l.Rel == "next" { + url = l.Href + } + } + + if url == "" { + return "", nil + } + + return url, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/service_client.go b/vendor/github.com/gophercloud/gophercloud/service_client.go new file mode 100644 index 00000000000..dd54abe30ef --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/service_client.go @@ -0,0 +1,162 @@ +package gophercloud + +import ( + "io" + "net/http" + "strings" +) + +// ServiceClient stores details required to interact with a specific service API implemented by a provider. +// Generally, you'll acquire these by calling the appropriate `New` method on a ProviderClient. +type ServiceClient struct { + // ProviderClient is a reference to the provider that implements this service. + *ProviderClient + + // Endpoint is the base URL of the service's API, acquired from a service catalog. + // It MUST end with a /. + Endpoint string + + // ResourceBase is the base URL shared by the resources within a service's API. It should include + // the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used + // as-is, instead. + ResourceBase string + + // This is the service client type (e.g. compute, sharev2). + // NOTE: FOR INTERNAL USE ONLY. DO NOT SET. GOPHERCLOUD WILL SET THIS. + // It is only exported because it gets set in a different package. + Type string + + // The microversion of the service to use. Set this to use a particular microversion. + Microversion string + + // MoreHeaders allows users (or Gophercloud) to set service-wide headers on requests. Put another way, + // values set in this field will be set on all the HTTP requests the service client sends. + MoreHeaders map[string]string +} + +// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /. +func (client *ServiceClient) ResourceBaseURL() string { + if client.ResourceBase != "" { + return client.ResourceBase + } + return client.Endpoint +} + +// ServiceURL constructs a URL for a resource belonging to this provider. +func (client *ServiceClient) ServiceURL(parts ...string) string { + return client.ResourceBaseURL() + strings.Join(parts, "/") +} + +func (client *ServiceClient) initReqOpts(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) { + if v, ok := (JSONBody).(io.Reader); ok { + opts.RawBody = v + } else if JSONBody != nil { + opts.JSONBody = JSONBody + } + + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + + if opts.MoreHeaders == nil { + opts.MoreHeaders = make(map[string]string) + } + + if client.Microversion != "" { + client.setMicroversionHeader(opts) + } +} + +// Get calls `Request` with the "GET" HTTP verb. +func (client *ServiceClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, JSONResponse, opts) + return client.Request("GET", url, opts) +} + +// Post calls `Request` with the "POST" HTTP verb. +func (client *ServiceClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("POST", url, opts) +} + +// Put calls `Request` with the "PUT" HTTP verb. +func (client *ServiceClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("PUT", url, opts) +} + +// Patch calls `Request` with the "PATCH" HTTP verb. +func (client *ServiceClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("PATCH", url, opts) +} + +// Delete calls `Request` with the "DELETE" HTTP verb. +func (client *ServiceClient) Delete(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, nil, opts) + return client.Request("DELETE", url, opts) +} + +// Head calls `Request` with the "HEAD" HTTP verb. +func (client *ServiceClient) Head(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, nil, opts) + return client.Request("HEAD", url, opts) +} + +func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { + switch client.Type { + case "compute": + opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion + case "sharev2": + opts.MoreHeaders["X-OpenStack-Manila-API-Version"] = client.Microversion + case "volume": + opts.MoreHeaders["X-OpenStack-Volume-API-Version"] = client.Microversion + case "baremetal": + opts.MoreHeaders["X-OpenStack-Ironic-API-Version"] = client.Microversion + case "baremetal-introspection": + opts.MoreHeaders["X-OpenStack-Ironic-Inspector-API-Version"] = client.Microversion + } + + if client.Type != "" { + opts.MoreHeaders["OpenStack-API-Version"] = client.Type + " " + client.Microversion + } +} + +// Request carries out the HTTP operation for the service client +func (client *ServiceClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { + if len(client.MoreHeaders) > 0 { + if options == nil { + options = new(RequestOpts) + } + for k, v := range client.MoreHeaders { + options.MoreHeaders[k] = v + } + } + return client.ProviderClient.Request(method, url, options) +} + +// ParseResponse is a helper function to parse http.Response to constituents. +func ParseResponse(resp *http.Response, err error) (io.ReadCloser, http.Header, error) { + if resp != nil { + return resp.Body, resp.Header, err + } + return nil, nil, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/util.go b/vendor/github.com/gophercloud/gophercloud/util.go new file mode 100644 index 00000000000..2740c301e51 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/util.go @@ -0,0 +1,130 @@ +package gophercloud + +import ( + "fmt" + "net/url" + "path/filepath" + "reflect" + "strings" + "time" +) + +// NormalizePathURL is used to convert rawPath to a fqdn, using basePath as +// a reference in the filesystem, if necessary. basePath is assumed to contain +// either '.' when first used, or the file:// type fqdn of the parent resource. +// e.g. myFavScript.yaml => file://opt/lib/myFavScript.yaml +func NormalizePathURL(basePath, rawPath string) (string, error) { + u, err := url.Parse(rawPath) + if err != nil { + return "", err + } + // if a scheme is defined, it must be a fqdn already + if u.Scheme != "" { + return u.String(), nil + } + // if basePath is a url, then child resources are assumed to be relative to it + bu, err := url.Parse(basePath) + if err != nil { + return "", err + } + var basePathSys, absPathSys string + if bu.Scheme != "" { + basePathSys = filepath.FromSlash(bu.Path) + absPathSys = filepath.Join(basePathSys, rawPath) + bu.Path = filepath.ToSlash(absPathSys) + return bu.String(), nil + } + + absPathSys = filepath.Join(basePath, rawPath) + u.Path = filepath.ToSlash(absPathSys) + if err != nil { + return "", err + } + u.Scheme = "file" + return u.String(), nil +} + +// NormalizeURL is an internal function to be used by provider clients. +// +// It ensures that each endpoint URL has a closing `/`, as expected by +// ServiceClient's methods. +func NormalizeURL(url string) string { + if !strings.HasSuffix(url, "/") { + return url + "/" + } + return url +} + +// RemainingKeys will inspect a struct and compare it to a map. Any struct +// field that does not have a JSON tag that matches a key in the map or +// a matching lower-case field in the map will be returned as an extra. +// +// This is useful for determining the extra fields returned in response bodies +// for resources that can contain an arbitrary or dynamic number of fields. +func RemainingKeys(s interface{}, m map[string]interface{}) (extras map[string]interface{}) { + extras = make(map[string]interface{}) + for k, v := range m { + extras[k] = v + } + + valueOf := reflect.ValueOf(s) + typeOf := reflect.TypeOf(s) + for i := 0; i < valueOf.NumField(); i++ { + field := typeOf.Field(i) + + lowerField := strings.ToLower(field.Name) + delete(extras, lowerField) + + if tagValue := field.Tag.Get("json"); tagValue != "" && tagValue != "-" { + delete(extras, tagValue) + } + } + + return +} + +// WaitFor polls a predicate function, once per second, up to a timeout limit. +// This is useful to wait for a resource to transition to a certain state. +// To handle situations when the predicate might hang indefinitely, the +// predicate will be prematurely cancelled after the timeout. +// Resource packages will wrap this in a more convenient function that's +// specific to a certain resource, but it can also be useful on its own. +func WaitFor(timeout int, predicate func() (bool, error)) error { + type WaitForResult struct { + Success bool + Error error + } + + start := time.Now().Unix() + + for { + // If a timeout is set, and that's been exceeded, shut it down. + if timeout >= 0 && time.Now().Unix()-start >= int64(timeout) { + return fmt.Errorf("A timeout occurred") + } + + time.Sleep(1 * time.Second) + + var result WaitForResult + ch := make(chan bool, 1) + go func() { + defer close(ch) + satisfied, err := predicate() + result.Success = satisfied + result.Error = err + }() + + select { + case <-ch: + if result.Error != nil { + return result.Error + } + if result.Success { + return nil + } + // If the predicate has not finished by the timeout, cancel it. + case <-time.After(time.Duration(timeout) * time.Second): + return fmt.Errorf("A timeout occurred") + } + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index d00ba7857c0..d733f7414f3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1238,6 +1238,25 @@ github.com/googleapis/gax-go/v2/internal github.com/googleapis/gax-go/v2/internallog github.com/googleapis/gax-go/v2/internallog/internal github.com/googleapis/gax-go/v2/iterator +# github.com/gophercloud/gophercloud v1.7.0 +## explicit; go 1.14 +github.com/gophercloud/gophercloud +github.com/gophercloud/gophercloud/openstack +github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets +github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups +github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage +github.com/gophercloud/gophercloud/openstack/compute/v2/flavors +github.com/gophercloud/gophercloud/openstack/compute/v2/servers +github.com/gophercloud/gophercloud/openstack/identity/v2/tenants +github.com/gophercloud/gophercloud/openstack/identity/v2/tokens +github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens +github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1 +github.com/gophercloud/gophercloud/openstack/identity/v3/tokens +github.com/gophercloud/gophercloud/openstack/imageservice/v2/images +github.com/gophercloud/gophercloud/openstack/networking/v2/networks +github.com/gophercloud/gophercloud/openstack/networking/v2/ports +github.com/gophercloud/gophercloud/openstack/utils +github.com/gophercloud/gophercloud/pagination # github.com/gophercloud/gophercloud/v2 v2.5.0 ## explicit; go 1.22 github.com/gophercloud/gophercloud/v2 From 541b6dd9e9a7853edee8a9cb7e1bba02dbb99015 Mon Sep 17 00:00:00 2001 From: daturece Date: Fri, 12 Sep 2025 18:59:33 +0200 Subject: [PATCH 5/8] Remove quota validation, structs, client creation from upstream, snapshot naming, pausing other fixes to logging, structure, comments --- docs/hibernating-clusters.md | 8 +- .../hibernation/openstack_actuator.go | 564 +++++++------- .../hibernation/openstack_actuator_test.go | 706 ++++++++++-------- pkg/openstackclient/client.go | 288 +++---- pkg/openstackclient/mock/client_generated.go | 105 +-- 5 files changed, 864 insertions(+), 807 deletions(-) diff --git a/docs/hibernating-clusters.md b/docs/hibernating-clusters.md index 40b7434d11b..fb53a5d6170 100644 --- a/docs/hibernating-clusters.md +++ b/docs/hibernating-clusters.md @@ -353,8 +353,10 @@ OpenStack hibernation deletes cluster instances while preserving the ability to While OpenStack provides shelving functionality, replicating true hibernation behavior similar to other cloud providers is difficult and often not possible depending on the OpenStack deployment configuration. Hive's snapshot-based approach provides a universally compatible solution that works with any OpenStack setup. -OpenStack hibernation creates snapshots of all instances, saves their complete configuration into a secret (CD namespace), and deletes the instances, completely freeing all project resources consumed by the cluster. +OpenStack hibernation pauses the instances, creates snapshots of all instances, saves their complete configuration into a secret (CD namespace), and deletes the instances, completely freeing all project resources consumed by the cluster. -Restoration recreates instances with identical attributes to their original state. +Restoration recreates instances with identical attributes to their original state, including tags and metadata. -Due to the specific nature of the snapshot based implementation, the hibernation and restoration process inherently requires more time to finish compared to other cloud providers. \ No newline at end of file +Due to the specific nature of the snapshot based implementation, the hibernation and restoration process inherently requires more time to finish compared to other cloud providers, approximately 10 minutes for each, creating the snapshots during hibernation and restoring the instances from the snapshots taking majority of the time. + +Instance snapshots (Images) do not take from the project's quota and are being cleaned up after each Hibernation cycle (worker snaphots can only be deleted once the worker instance dissappears) \ No newline at end of file diff --git a/pkg/controller/hibernation/openstack_actuator.go b/pkg/controller/hibernation/openstack_actuator.go index 50fbeeb7d78..7bdc4a7b889 100644 --- a/pkg/controller/hibernation/openstack_actuator.go +++ b/pkg/controller/hibernation/openstack_actuator.go @@ -7,11 +7,12 @@ import ( "strings" "time" - "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" hivev1 "github.com/openshift/hive/apis/hive/v1" "github.com/openshift/hive/pkg/openstackclient" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/sets" log "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" @@ -30,6 +31,8 @@ type openstackActuator struct { openstackClientFn func(*hivev1.ClusterDeployment, client.Client, log.FieldLogger) (openstackclient.Client, error) } +var _ HibernationActuator = &openstackActuator{} + // Create API client func getOpenStackClient(cd *hivev1.ClusterDeployment, c client.Client, logger log.FieldLogger) (openstackclient.Client, error) { ctx := context.Background() @@ -58,7 +61,6 @@ func (a *openstackActuator) CanHandle(cd *hivev1.ClusterDeployment) bool { return cd.Spec.Platform.OpenStack != nil } -// Create snapshots and saves configuration, then stops machines func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { logger = logger.WithField("cloud", "openstack") logger.Info("stopping machines and creating snapshots") @@ -70,8 +72,8 @@ func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClien infraID := cd.Spec.ClusterMetadata.InfraID - // 1. - matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + // 1. Find instances + matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) if err != nil { return fmt.Errorf("error finding instances: %v", err) } @@ -81,44 +83,58 @@ func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClien return nil } - logger.Infof("found %d instances to hibernate", len(matchingServers)) + logger.WithField("count", len(matchingServers)).Info("found instances to hibernate") + // 2. Validate instance states if err := a.validateInstanceStates(openstackClient, matchingServers, logger); err != nil { return err } - // 3. - snapshotIDs, err := a.createSnapshots(openstackClient, matchingServers, logger) + // 3. Pause all instances for data consistency + if err := a.pauseInstances(openstackClient, matchingServers, logger); err != nil { + return fmt.Errorf("failed to pause instances: %v", err) + } + + // 4. Create snapshots + snapshotIDs, snapshotNames, err := a.createSnapshots(openstackClient, matchingServers, logger) if err != nil { + // If snapshot creation fails, unpause + logger.Warn("snapshot creation failed - unpausing instances") + a.unpauseInstances(openstackClient, matchingServers, logger) return err } - // 4. - if err := a.waitForSnapshots(openstackClient, snapshotIDs, matchingServers, logger); err != nil { + // 5. Wait for snapshots to complete + if err := a.waitForSnapshots(openstackClient, snapshotIDs, snapshotNames, matchingServers, logger); err != nil { + // If snapshot wait fails, unpause + logger.Warn("snapshot wait failed - unpausing instances") + a.unpauseInstances(openstackClient, matchingServers, logger) return err } - // 5. - if err := a.saveInstanceConfigurationToSecret(cd, hiveClient, openstackClient, matchingServers, snapshotIDs, logger); err != nil { + // 6. Save instance configuration + if err := a.saveInstanceConfigurationToSecret(cd, hiveClient, openstackClient, matchingServers, snapshotIDs, snapshotNames, logger); err != nil { + // If config save fails, unpause instances before returning + logger.Warn("config save failed - unpausing instances") + a.unpauseInstances(openstackClient, matchingServers, logger) return fmt.Errorf("error saving configuration: %v", err) } - // 6. + // 7. Delete instances if err := a.deleteInstances(openstackClient, matchingServers, logger); err != nil { return err } - // 7. + // 8. Wait for instance cleanup if err := a.waitForInstanceCleanup(openstackClient, infraID, logger); err != nil { return err } - // 8. + // 9. Cleanup old snapshots logger.Info("cleaning up old hibernation snapshots after instance deletion") - err = a.cleanupOldSnapshotsAfterDeletion(openstackClient, infraID, snapshotIDs, logger) + err = a.cleanupOldSnapshotsAfterDeletion(cd, hiveClient, openstackClient, infraID, snapshotIDs, logger) if err != nil { - // Don't fail hibernation due to cleanup issues - logger.Warnf("some old snapshots couldn't be cleaned up: %v", err) + logger.WithField("error", err).Warn("some old snapshots couldn't be cleaned up") } logger.Info("hibernation completed successfully") @@ -126,7 +142,7 @@ func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClien } // Find which instances are missing -func (a *openstackActuator) findMissingInstances(expectedInstances []OpenStackInstanceConfig, existingServers []ServerInfo, logger log.FieldLogger) []OpenStackInstanceConfig { +func (a *openstackActuator) findMissingInstances(expectedInstances []OpenStackInstanceConfig, existingServers []*servers.Server, logger log.FieldLogger) []OpenStackInstanceConfig { var missingInstances []OpenStackInstanceConfig // Create a map of existing instance names for quick lookup @@ -137,11 +153,12 @@ func (a *openstackActuator) findMissingInstances(expectedInstances []OpenStackIn // Check which expected instances are missing for _, expected := range expectedInstances { + instanceLogger := logger.WithField("instance", expected.Name) if !existingNames[expected.Name] { - logger.Infof("instance %s is missing - needs to be created", expected.Name) + instanceLogger.Info("instance is missing - needs to be created") missingInstances = append(missingInstances, expected) } else { - logger.Infof("instance %s already exists", expected.Name) + instanceLogger.Info("instance already exists") } } @@ -149,58 +166,78 @@ func (a *openstackActuator) findMissingInstances(expectedInstances []OpenStackIn } // Check if instances are in valid states for hibernation -func (a *openstackActuator) validateInstanceStates(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) error { - ctx := context.Background() - +func (a *openstackActuator) validateInstanceStates(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) error { for _, server := range servers { - serverDetails, err := openstackClient.GetServer(ctx, server.ID) - if err != nil { - return fmt.Errorf("failed to get server %s details: %v", server.Name, err) - } + serverLogger := logger.WithFields(log.Fields{ + "instance": server.Name, + "status": server.Status, + }) - logger.Infof("instance %s status: %s", server.Name, serverDetails.Status) + serverLogger.Info("instance status") // Check for deleting states that would cause conflicts - if strings.Contains(strings.ToLower(serverDetails.Status), "delet") { + if strings.Contains(strings.ToLower(server.Status), "delet") { return fmt.Errorf("cannot hibernate: instance %s is being deleted by another process", server.Name) } - if serverDetails.Status != "ACTIVE" { - logger.Warnf("instance %s status is %s (not ACTIVE) - snapshot may fail", server.Name, serverDetails.Status) + if server.Status != "ACTIVE" { + serverLogger.Warn("instance status is not ACTIVE - snapshot may fail") } } return nil } +// Helper function to generate hibernation snapshot names +func (a *openstackActuator) generateHibernationSnapshotName(serverName string, timestamp string) string { + return fmt.Sprintf("%s-hibernation-%s", serverName, timestamp) +} + // Create snapshots for all instances -func (a *openstackActuator) createSnapshots(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) ([]string, error) { +func (a *openstackActuator) createSnapshots(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) ([]string, []string, error) { ctx := context.Background() snapshotIDs := make([]string, 0, len(servers)) + snapshotNames := make([]string, 0, len(servers)) + timestamp := time.Now().UTC().Format("20060102-150405") for i, server := range servers { - logger.Infof("creating snapshot %d/%d for instance %s", i+1, len(servers), server.Name) + progressLogger := logger.WithFields(log.Fields{ + "current": i + 1, + "total": len(servers), + "instance": server.Name, + }) + + progressLogger.Info("creating snapshot") - snapshotName := fmt.Sprintf("%s-hibernation-%s", server.Name, time.Now().UTC().Format("20060102-150405")) + snapshotName := a.generateHibernationSnapshotName(server.Name, timestamp) snapshotID, err := openstackClient.CreateServerSnapshot(ctx, server.ID, snapshotName) if err != nil { if strings.Contains(err.Error(), "task_state deleting") || strings.Contains(err.Error(), "409") { - return nil, fmt.Errorf("hibernation conflict: instance %s is being modified by another process", server.Name) + return nil, nil, fmt.Errorf("hibernation conflict: instance %s is being modified by another process", server.Name) } - return nil, fmt.Errorf("failed to create snapshot for %s: %v", server.Name, err) + return nil, nil, fmt.Errorf("failed to create snapshot for %s: %v", server.Name, err) } snapshotIDs = append(snapshotIDs, snapshotID) - logger.Infof("snapshot created for %s (ID: %s)", server.Name, snapshotID) + snapshotNames = append(snapshotNames, snapshotName) + progressLogger.WithFields(log.Fields{ + "snapshot_id": snapshotID, + "snapshot_name": snapshotName, + }).Info("snapshot created") } - return snapshotIDs, nil + return snapshotIDs, snapshotNames, nil } // Wait for all snapshots to complete -func (a *openstackActuator) waitForSnapshots(openstackClient openstackclient.Client, snapshotIDs []string, servers []ServerInfo, logger log.FieldLogger) error { +func (a *openstackActuator) waitForSnapshots(openstackClient openstackclient.Client, snapshotIDs []string, snapshotNames []string, servers []*servers.Server, logger log.FieldLogger) error { for i, snapshotID := range snapshotIDs { serverName := servers[i].Name - logger.Infof("waiting for snapshot %s to complete for %s", snapshotID, serverName) + snapshotName := snapshotNames[i] + logger.WithFields(log.Fields{ + "snapshot_id": snapshotID, + "snapshot_name": snapshotName, + "server": serverName, + }).Info("waiting for snapshot to complete") err := a.waitForSnapshotCompletion(openstackClient, snapshotID, serverName, logger) if err != nil { @@ -211,11 +248,15 @@ func (a *openstackActuator) waitForSnapshots(openstackClient openstackclient.Cli } // Delete all instances -func (a *openstackActuator) deleteInstances(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) error { +func (a *openstackActuator) deleteInstances(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) error { ctx := context.Background() for i, server := range servers { - logger.Infof("deleting instance %d/%d: %s", i+1, len(servers), server.Name) + logger.WithFields(log.Fields{ + "current": i + 1, + "total": len(servers), + "instance": server.Name, + }).Info("deleting instance") err := openstackClient.DeleteServer(ctx, server.ID) if err != nil { @@ -267,7 +308,7 @@ func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClie // Only proceed if PowerState is Running if cd.Spec.PowerState != hivev1.ClusterPowerStateRunning { - logger.Infof("PowerState is %s, not Running - refusing to start machines", cd.Spec.PowerState) + logger.WithField("power_state", cd.Spec.PowerState).Info("PowerState is not Running - refusing to start machines") return nil } @@ -283,7 +324,7 @@ func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClie // Check if we have existing instances but no hibernation config infraID := cd.Spec.ClusterMetadata.InfraID - existingServers, checkErr := a.findInstancesByPrefix(openstackClient, infraID) + existingServers, checkErr := a.findInstancesByInfraID(openstackClient, infraID) if checkErr != nil { logger.Warnf("could not check existing instances: %v", checkErr) } else if len(existingServers) > 0 { @@ -297,7 +338,7 @@ func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClie // Check for existing instances infraID := cd.Spec.ClusterMetadata.InfraID - existingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + existingServers, err := a.findInstancesByInfraID(openstackClient, infraID) if err != nil { logger.Warnf("could not check existing instances: %v", err) } @@ -314,150 +355,44 @@ func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClie return a.restoreFromHibernationConfig(cd, hiveClient, openstackClient, instances, logger) } -// Validate project resource quotas prior to the restoration -func (a *openstackActuator) validateRestoreResources(openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { - ctx := context.Background() - - logger.Info("checking OpenStack quotas before restoration...") - - // Get current quotas and usage - quotas, err := openstackClient.GetComputeQuotas(ctx) - if err != nil { - return fmt.Errorf("failed to get compute quotas: %v", err) - } - - usage, err := openstackClient.GetComputeUsage(ctx) - if err != nil { - return fmt.Errorf("failed to get compute usage: %v", err) - } - - // Calculate requirements for restoration - requirements, err := a.calculateResourceRequirements(openstackClient, instances, logger) - if err != nil { - return fmt.Errorf("failed to calculate resource requirements: %v", err) - } - - logger.Infof("restoration requires: %d instances, %d vCPUs, %d MB RAM", - requirements.Instances, requirements.VCPUs, requirements.RAM) - - // Handle usage data (can be nil) - var instancesUsed, coresUsed, ramUsed int - if usage != nil { - instancesUsed = len(usage.ServerUsages) - coresUsed = int(usage.TotalVCPUsUsage) - ramUsed = int(usage.TotalMemoryMBUsage) - } - - // Check available resources - availableInstances := quotas.Instances - instancesUsed - availableVCPUs := quotas.Cores - coresUsed - availableRAM := quotas.RAM - ramUsed - - logger.Infof("available resources: %d instances, %d vCPUs, %d MB RAM", - availableInstances, availableVCPUs, availableRAM) - - // Validate sufficient resources - var errors []string - - if requirements.Instances > availableInstances { - errors = append(errors, fmt.Sprintf("insufficient instances: need %d, have %d available", - requirements.Instances, availableInstances)) - } - - if requirements.VCPUs > availableVCPUs { - errors = append(errors, fmt.Sprintf("insufficient vCPUs: need %d, have %d available", - requirements.VCPUs, availableVCPUs)) - } - - if requirements.RAM > availableRAM { - errors = append(errors, fmt.Sprintf("insufficient RAM: need %d MB, have %d MB available", - requirements.RAM, availableRAM)) - } - - if len(errors) > 0 { - return fmt.Errorf("insufficient OpenStack resources for restoration: %s", - strings.Join(errors, "; ")) - } - - logger.Info("sufficient resources available for restoration") - return nil -} - -// Calculate required project resources -func (a *openstackActuator) calculateResourceRequirements(openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) (*openstackclient.ResourceRequirements, error) { - ctx := context.Background() - requirements := &openstackclient.ResourceRequirements{} - - // Track unique flavors to avoid duplicate API calls - flavorCache := make(map[string]*flavors.Flavor) // Changed type - - for _, instance := range instances { - requirements.Instances++ - - // Get flavor details - var flavor *flavors.Flavor // Changed type - if cached, exists := flavorCache[instance.Flavor]; exists { - flavor = cached - } else { - var err error - flavor, err = openstackClient.GetFlavorDetails(ctx, instance.Flavor) - if err != nil { - return nil, fmt.Errorf("failed to get flavor %s details: %v", instance.Flavor, err) - } - flavorCache[instance.Flavor] = flavor - } - - requirements.VCPUs += flavor.VCPUs - requirements.RAM += flavor.RAM - - logger.Infof("instance %s (flavor %s): %d vCPUs, %d MB RAM", - instance.Name, flavor.Name, flavor.VCPUs, flavor.RAM) - } - - return requirements, nil -} - // Recreate instances from hibernation configuration func (a *openstackActuator) restoreFromHibernationConfig(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { infraID := cd.Spec.ClusterMetadata.InfraID - logger.Infof("restoring %d instances from hibernation", len(instances)) - - // Validate sufficient resources before starting restoration - err := a.validateRestoreResources(openstackClient, instances, logger) - if err != nil { - return fmt.Errorf("resource validation failed: %v", err) - } + logger.WithField("count", len(instances)).Info("restoring instances from hibernation") // Check what instances already exist - existingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + existingServers, err := a.findInstancesByInfraID(openstackClient, infraID) if err != nil { logger.Warnf("could not check existing instances: %v", err) - existingServers = []ServerInfo{} // Assume none exist + existingServers = []*servers.Server{} // Assume none exist } // Figure out what we need to create instancesToCreate := a.findMissingInstances(instances, existingServers, logger) if len(instancesToCreate) == 0 { - logger.Info("all instances already exist - restoration complete") - return a.deleteHibernationConfigSecret(cd, hiveClient, logger) - } + logger.Info("all instances already exist - continuing to verify they're active") + } else { + logger.WithField("count", len(instancesToCreate)).Info("need to create missing instances") - logger.Infof("need to create %d missing instances", len(instancesToCreate)) - - // Create missing instances - err = a.createMissingInstances(openstackClient, instancesToCreate, logger) - if err != nil { - logger.Errorf("some instance creation failed: %v", err) + // Create missing instances + err = a.createMissingInstances(openstackClient, instancesToCreate, logger) + if err != nil { + logger.Errorf("some instance creation failed: %v", err) + } } - // Wait for instances to be active + // ALWAYS wait for instances to be active (regardless of whether we created new ones) err = a.waitForAllInstancesToBeActive(openstackClient, infraID, len(instances), logger) if err != nil { return fmt.Errorf("not all instances are active yet: %v", err) } + // Tags have to be added separately AFTER the instances are ACTIVE + a.restoreInstanceTags(openstackClient, instances, logger) + + // ALWAYS clean up hibernation snapshots after successful restoration logger.Info("cleaning up hibernation snapshots after successful restoration") err = a.cleanupRestorationSnapshots(openstackClient, instances, logger) if err != nil { @@ -465,7 +400,7 @@ func (a *openstackActuator) restoreFromHibernationConfig(cd *hivev1.ClusterDeplo logger.Warnf("failed to cleanup some snapshots: %v", err) } - // Only clear hibernation config when we have the instances running + // ALWAYS clear hibernation config when we have confirmed instances are running logger.Info("all instances confirmed active - clearing hibernation configuration") return a.deleteHibernationConfigSecret(cd, hiveClient, logger) } @@ -473,17 +408,23 @@ func (a *openstackActuator) restoreFromHibernationConfig(cd *hivev1.ClusterDeplo // Create missing instances during restoration func (a *openstackActuator) createMissingInstances(openstackClient openstackclient.Client, instancesToCreate []OpenStackInstanceConfig, logger log.FieldLogger) error { ctx := context.Background() - var errors []string + var errs []error for i, instance := range instancesToCreate { - logger.Infof("creating missing instance %d/%d: %s", i+1, len(instancesToCreate), instance.Name) + instanceLogger := logger.WithFields(log.Fields{ + "current": i + 1, + "total": len(instancesToCreate), + "instance": instance.Name, + }) + + instanceLogger.Info("creating missing instance") // Validate snapshot still exists _, err := openstackClient.GetImage(ctx, instance.SnapshotID) if err != nil { - errorMsg := fmt.Sprintf("snapshot %s not found for %s: %v", instance.SnapshotID, instance.Name, err) - errors = append(errors, errorMsg) - logger.Error(errorMsg) + err := fmt.Errorf("snapshot %s not found for %s: %w", instance.SnapshotID, instance.Name, err) + errs = append(errs, err) + logger.WithField("error", err).Error("snapshot validation failed") continue } @@ -504,20 +445,17 @@ func (a *openstackActuator) createMissingInstances(openstackClient openstackclie newServer, err := openstackClient.CreateServerFromOpts(ctx, createOpts) if err != nil { - errorMsg := fmt.Sprintf("failed to create instance %s: %v", instance.Name, err) - errors = append(errors, errorMsg) - logger.Error(errorMsg) + err := fmt.Errorf("failed to create instance %s: %w", instance.Name, err) + errs = append(errs, err) + logger.WithField("error", err).Error("instance creation failed") continue } - logger.Infof("created instance %s (ID: %s)", instance.Name, newServer.ID) - } + instanceLogger.WithField("server_id", newServer.ID).Info("created instance") - if len(errors) > 0 { - return fmt.Errorf("some instance creation failed: %s", strings.Join(errors, "; ")) } - return nil + return utilerrors.NewAggregate(errs) } // Wait for ALL instances to be active @@ -534,7 +472,7 @@ func (a *openstackActuator) waitForAllInstancesToBeActive(openstackClient openst return fmt.Errorf("timeout waiting for all instances to become active after %v", timeout) case <-ticker.C: // Get current instances - currentServers, err := a.findInstancesByPrefix(openstackClient, infraID) + currentServers, err := a.findInstancesByInfraID(openstackClient, infraID) if err != nil { logger.Warnf("error checking instance status: %v", err) continue @@ -550,16 +488,10 @@ func (a *openstackActuator) waitForAllInstancesToBeActive(openstackClient openst var nonActiveInstances []string for _, server := range currentServers { - serverDetails, err := openstackClient.GetServer(context.Background(), server.ID) - if err != nil { - logger.Warnf("could not get server %s details: %v", server.Name, err) - continue - } - - if serverDetails.Status == "ACTIVE" { + if server.Status == "ACTIVE" { activeCount++ } else { - nonActiveInstances = append(nonActiveInstances, fmt.Sprintf("%s(%s)", server.Name, serverDetails.Status)) + nonActiveInstances = append(nonActiveInstances, fmt.Sprintf("%s(%s)", server.Name, server.Status)) } } @@ -584,7 +516,7 @@ func (a *openstackActuator) MachinesRunning(cd *hivev1.ClusterDeployment, hiveCl } infraID := cd.Spec.ClusterMetadata.InfraID - matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) if err != nil { return false, nil, fmt.Errorf("error finding instances: %v", err) } @@ -609,25 +541,17 @@ func (a *openstackActuator) MachinesRunning(cd *hivev1.ClusterDeployment, hiveCl } // Check the actual state of instances in OpenStack -func (a *openstackActuator) categorizeInstanceStates(openstackClient openstackclient.Client, servers []ServerInfo, logger log.FieldLogger) (int, []string) { - ctx := context.Background() +func (a *openstackActuator) categorizeInstanceStates(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) (int, []string) { runningCount := 0 var deletingInstances []string for _, server := range servers { - serverDetails, err := openstackClient.GetServer(ctx, server.ID) - if err != nil { - logger.Warnf("could not get server %s details: %v", server.Name, err) - runningCount++ // Assume running if we can't check - continue - } - - status := strings.ToLower(serverDetails.Status) + status := strings.ToLower(server.Status) if strings.Contains(status, "delet") || status == "shutoff" || status == "error" { - logger.Infof("instance %s is being deleted/stopped (status: %s)", server.Name, serverDetails.Status) + logger.Infof("instance %s is being deleted/stopped (status: %s)", server.Name, server.Status) deletingInstances = append(deletingInstances, server.Name) } else { - logger.Infof("instance %s is running (status: %s)", server.Name, serverDetails.Status) + logger.Infof("instance %s is running (status: %s)", server.Name, server.Status) runningCount++ } } @@ -646,7 +570,7 @@ func (a *openstackActuator) MachinesStopped(cd *hivev1.ClusterDeployment, hiveCl } infraID := cd.Spec.ClusterMetadata.InfraID - matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) if err != nil { return false, nil, fmt.Errorf("error finding instances: %v", err) } @@ -665,40 +589,33 @@ func (a *openstackActuator) MachinesStopped(cd *hivev1.ClusterDeployment, hiveCl return false, notStopped, nil } -// basic server information -type ServerInfo struct { - ID string - Name string -} - type OpenStackInstanceConfig struct { Name string `json:"name"` Flavor string `json:"flavor"` PortID string `json:"portID"` SnapshotID string `json:"snapshotID"` + SnapshotName string `json:"snapshotName"` SecurityGroups []string `json:"securityGroups"` ClusterID string `json:"clusterID"` NetworkID string `json:"networkID"` OpenshiftClusterID string `json:"openshiftClusterID"` Metadata map[string]string `json:"metadata"` + Tags []string `json:"tags"` } // Return servers that match the infraID prefix -func (a *openstackActuator) findInstancesByPrefix(openstackClient openstackclient.Client, prefix string) ([]ServerInfo, error) { +func (a *openstackActuator) findInstancesByInfraID(openstackClient openstackclient.Client, prefix string) ([]*servers.Server, error) { ctx := context.Background() - servers, err := openstackClient.ListServers(ctx, nil) + allServers, err := openstackClient.ListServers(ctx, nil) if err != nil { return nil, fmt.Errorf("error listing servers: %v", err) } - var matchingServers []ServerInfo - for _, server := range servers { + var matchingServers []*servers.Server + for _, server := range allServers { if strings.HasPrefix(server.Name, prefix) { - matchingServers = append(matchingServers, ServerInfo{ - ID: server.ID, - Name: server.Name, - }) + matchingServers = append(matchingServers, &server) } } @@ -706,15 +623,19 @@ func (a *openstackActuator) findInstancesByPrefix(openstackClient openstackclien } // Configuration persistence methods -func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, matchingServers []ServerInfo, snapshotIDs []string, logger log.FieldLogger) error { +func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, servers []*servers.Server, snapshotIDs []string, snapshotNames []string, logger log.FieldLogger) error { ctx := context.Background() - if len(matchingServers) == 0 { + if len(servers) == 0 { return nil } - if len(snapshotIDs) != len(matchingServers) { - return fmt.Errorf("mismatch between servers (%d) and snapshot IDs (%d)", len(matchingServers), len(snapshotIDs)) + if len(snapshotIDs) != len(servers) { + return fmt.Errorf("mismatch between servers (%d) and snapshot IDs (%d)", len(servers), len(snapshotIDs)) + } + + if len(snapshotNames) != len(servers) { + return fmt.Errorf("mismatch between servers (%d) and snapshot names (%d)", len(servers), len(snapshotNames)) } // Get InfraID @@ -727,11 +648,7 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster } // Get openshiftClusterID from first instance - server, err := openstackClient.GetServer(ctx, matchingServers[0].ID) - if err != nil { - return fmt.Errorf("error getting server details: %v", err) - } - + server := servers[0] var openshiftClusterID string if server.Metadata != nil { if id, exists := server.Metadata["openshiftClusterID"]; exists { @@ -747,57 +664,67 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster // Build configuration for each instance var instanceConfigs []OpenStackInstanceConfig - for i, serverInfo := range matchingServers { - serverDetails, err := openstackClient.GetServer(ctx, serverInfo.ID) - if err != nil { - return fmt.Errorf("error getting server details for %s: %v", serverInfo.Name, err) - } - + for i, server := range servers { // Get flavor ID var flavorID string - if serverDetails.Flavor != nil { - if id, ok := serverDetails.Flavor["id"].(string); ok { + if server.Flavor != nil { + if id, ok := server.Flavor["id"].(string); ok { flavorID = id } else { - return fmt.Errorf("could not extract flavor ID for %s", serverInfo.Name) + return fmt.Errorf("could not extract flavor ID for %s", server.Name) } } else { - return fmt.Errorf("no flavor information found for %s", serverInfo.Name) + return fmt.Errorf("no flavor information found for %s", server.Name) } - // Find matching port + // Find maching port var portID string for _, port := range allPorts { - if port.Name == serverInfo.Name || port.Name == serverInfo.Name+"-0" { + if port.Name == server.Name || port.Name == server.Name+"-0" { portID = port.ID break } } if portID == "" { - return fmt.Errorf("no port found for instance %s", serverInfo.Name) + return fmt.Errorf("no port found for instance %s", server.Name) } // Get security groups - secGroups, err := openstackClient.GetServerSecurityGroupNames(ctx, serverInfo.ID) + secGroups, err := openstackClient.GetServerSecurityGroupNames(ctx, server.ID) if err != nil { - return fmt.Errorf("error getting security groups for %s: %v", serverInfo.Name, err) + return fmt.Errorf("error getting security groups for %s: %v", server.Name, err) } + // Get tags + serverTags, err := openstackClient.GetServerTags(ctx, server.ID) + if err != nil { + logger.Warnf("failed to get tags for %s: %v", server.Name, err) + serverTags = []string{} + } + + // Use the snapshot name directly + snapshotName := snapshotNames[i] + instanceConfigs = append(instanceConfigs, OpenStackInstanceConfig{ - Name: serverInfo.Name, + Name: server.Name, Flavor: flavorID, PortID: portID, SnapshotID: snapshotIDs[i], + SnapshotName: snapshotName, SecurityGroups: secGroups, ClusterID: infraID, NetworkID: networkID, OpenshiftClusterID: openshiftClusterID, - Metadata: serverDetails.Metadata, + Metadata: server.Metadata, + Tags: serverTags, }) - logger.Infof("captured metadata for %s: %d properties", - serverInfo.Name, len(serverDetails.Metadata)) + logger.WithFields(log.Fields{ + "instance": server.Name, + "snapshot_id": snapshotIDs[i], + "snapshot_name": snapshotName, + }).Info("saved snapshot info") } return a.saveHibernationConfigToSecret(cd, hiveClient, instanceConfigs, logger) @@ -873,7 +800,7 @@ func (a *openstackActuator) waitForInstanceCleanup(openstackClient openstackclie case <-timeout: return fmt.Errorf("timeout waiting for instance cleanup after %v", timeout) case <-ticker.C: - matchingServers, err := a.findInstancesByPrefix(openstackClient, infraID) + matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) if err != nil { logger.Warnf("error checking for remaining instances: %v", err) continue // Continue polling despite errors @@ -894,6 +821,39 @@ func (a *openstackActuator) waitForInstanceCleanup(openstackClient openstackclie } } +func (a *openstackActuator) restoreInstanceTags(openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) { + ctx := context.Background() + + for _, instance := range instances { + if len(instance.Tags) > 0 { + // Find the active instance by name + servers, err := a.findInstancesByInfraID(openstackClient, instance.ClusterID) + if err != nil { + continue + } + + for _, server := range servers { + if server.Name == instance.Name { + err = openstackClient.SetServerTags(ctx, server.ID, instance.Tags) + if err != nil { + logger.WithFields(log.Fields{ + "instance": instance.Name, + "error": err, + "tags": instance.Tags, + }).Warn("failed to restore server tags") + } else { + logger.WithFields(log.Fields{ + "instance": instance.Name, + "tag_count": len(instance.Tags), + }).Info("restored server tags") + } + break + } + } + } + } +} + // Get stored hibernation information from secret func (a *openstackActuator) loadHibernationConfigFromSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) ([]OpenStackInstanceConfig, error) { ctx := context.Background() @@ -987,11 +947,9 @@ func (a *openstackActuator) cleanupRestorationSnapshots(openstackClient openstac return nil // Always succeed } -func (a *openstackActuator) cleanupOldSnapshotsAfterDeletion(openstackClient openstackclient.Client, infraID string, currentSnapshotIDs []string, logger log.FieldLogger) error { - ctx := context.Background() - +func (a *openstackActuator) cleanupOldSnapshotsAfterDeletion(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, infraID string, currentSnapshotIDs []string, logger log.FieldLogger) error { // Get all hibernation snapshots for this cluster - allSnapshots, err := a.findAllHibernationSnapshots(openstackClient, infraID, logger) + allSnapshots, err := a.findHibernationSnapshotsByExactNames(cd, hiveClient, openstackClient, infraID, logger) if err != nil { return err } @@ -1002,15 +960,12 @@ func (a *openstackActuator) cleanupOldSnapshotsAfterDeletion(openstackClient ope } // Create map of current snapshot IDs for exclusion - currentIDs := make(map[string]bool) - for _, id := range currentSnapshotIDs { - currentIDs[id] = true - } + currentIDs := sets.NewString(currentSnapshotIDs...) // Filter out current snapshots - only delete OLD ones var oldSnapshots []images.Image for _, snapshot := range allSnapshots { - if !currentIDs[snapshot.ID] { + if !currentIDs.Has(snapshot.ID) { oldSnapshots = append(oldSnapshots, snapshot) logger.Infof("found OLD snapshot to delete: %s (ID: %s)", snapshot.Name, snapshot.ID) } else { @@ -1028,7 +983,7 @@ func (a *openstackActuator) cleanupOldSnapshotsAfterDeletion(openstackClient ope successCount := 0 for _, snapshot := range oldSnapshots { logger.Infof("deleting old hibernation snapshot: %s", snapshot.Name) - err := openstackClient.DeleteImage(ctx, snapshot.ID) + err := openstackClient.DeleteImage(context.Background(), snapshot.ID) if err != nil { logger.Warnf("failed to delete old snapshot %s: %v", snapshot.Name, err) } else { @@ -1041,25 +996,96 @@ func (a *openstackActuator) cleanupOldSnapshotsAfterDeletion(openstackClient ope return nil } -// Helper function to find all hibernation snapshots -func (a *openstackActuator) findAllHibernationSnapshots(openstackClient openstackclient.Client, infraID string, logger log.FieldLogger) ([]images.Image, error) { +func (a *openstackActuator) getExpectedSnapshotNames(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) ([]string, error) { + instances, err := a.loadHibernationConfigFromSecret(cd, hiveClient, logger) + if err != nil { + return nil, fmt.Errorf("failed to load hibernation config: %v", err) + } + + var snapshotNames []string + for _, instance := range instances { + if instance.SnapshotName != "" { + snapshotNames = append(snapshotNames, instance.SnapshotName) + } + } + + return snapshotNames, nil +} + +func (a *openstackActuator) findHibernationSnapshotsByExactNames(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, infraID string, logger log.FieldLogger) ([]images.Image, error) { ctx := context.Background() - // List all images - existingSnapshots, err := openstackClient.ListImages(ctx, nil) + // Get the exact snapshot names from hibernation config + expectedSnapshotNames, err := a.getExpectedSnapshotNames(cd, hiveClient, logger) + if err != nil { - return nil, fmt.Errorf("failed to list snapshots: %v", err) + return nil, fmt.Errorf("failed to get expected snapshot names: %v", err) } - var hibernationSnapshots []images.Image + if len(expectedSnapshotNames) == 0 { + return []images.Image{}, nil + } - // Filter snapshots - for _, snapshot := range existingSnapshots { - if strings.Contains(snapshot.Name, infraID) && strings.Contains(snapshot.Name, "hibernation") { - hibernationSnapshots = append(hibernationSnapshots, snapshot) - } + nameFilter := fmt.Sprintf("in:%s", strings.Join(expectedSnapshotNames, ",")) + listOpts := &images.ListOpts{ + Name: nameFilter, } - logger.Infof("found %d total hibernation snapshots for cluster %s", len(hibernationSnapshots), infraID) + hibernationSnapshots, err := openstackClient.ListImages(ctx, listOpts) + if err != nil { + return nil, fmt.Errorf("failed to query snapshots by exact names: %v", err) + } + + logger.WithFields(log.Fields{ + "expected_count": len(expectedSnapshotNames), + "found_count": len(hibernationSnapshots), + }).Info("found snapshots by exact names") + return hibernationSnapshots, nil } + +func (a *openstackActuator) pauseInstances(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) error { + ctx := context.Background() + + for i, server := range servers { + serverLogger := logger.WithFields(log.Fields{ + "current": i + 1, + "total": len(servers), + "instance": server.Name, + }) + + serverLogger.Info("pausing instance for snapshot consistency") + + err := openstackClient.PauseServer(ctx, server.ID) + if err != nil { + // If pause fails, try to unpause any previously paused instances + if i > 0 { + logger.Warn("pause failed - attempting to unpause previously paused instances") + a.unpauseInstances(openstackClient, servers[:i], logger) + } + return fmt.Errorf("failed to pause instance %s: %w", server.Name, err) + } + + serverLogger.Info("instance paused successfully") + } + + logger.WithField("count", len(servers)).Info("all instances paused successfully") + return nil +} + +// Unpause instances +func (a *openstackActuator) unpauseInstances(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) { + ctx := context.Background() + + for _, server := range servers { + serverLogger := logger.WithField("instance", server.Name) + serverLogger.Info("unpausing instance") + + err := openstackClient.UnpauseServer(ctx, server.ID) + if err != nil { + serverLogger.WithField("error", err).Error("failed to unpause instance") + } else { + serverLogger.Info("instance unpaused successfully") + } + } +} diff --git a/pkg/controller/hibernation/openstack_actuator_test.go b/pkg/controller/hibernation/openstack_actuator_test.go index 69cf93358e3..36e3ecd1f7c 100644 --- a/pkg/controller/hibernation/openstack_actuator_test.go +++ b/pkg/controller/hibernation/openstack_actuator_test.go @@ -8,9 +8,6 @@ import ( "testing" "github.com/golang/mock/gomock" - "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" - "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage" - "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" @@ -53,119 +50,125 @@ func TestOpenStackStopMachines(t *testing.T) { name: "stop no running instances", instances: map[string]int{}, setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // No instances found + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) }, }, { - name: "stop running instances", + name: "stop running instances with pause/unpause flow", instances: map[string]int{"ACTIVE": 2}, setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { - // Expect cleanup of existing snapshots - c.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return([]images.Image{}, nil).Times(1) + // Initial server discovery + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "server-1", Name: "testinfra-master-0", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, + {ID: "server-2", Name: "testinfra-worker-0", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, + }, nil).Times(1) - // Expect snapshot creation - c.EXPECT().CreateServerSnapshot(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return("snapshot-1", nil) + // Pause all instances + c.EXPECT().PauseServer(gomock.Any(), "server-1").Return(nil).Times(1) + c.EXPECT().PauseServer(gomock.Any(), "server-2").Return(nil).Times(1) - // Expect image status checks for waiting - activeImg := &images.Image{} - activeImg.Status = "active" - c.EXPECT().GetImage(gomock.Any(), gomock.Any()).Return(activeImg, nil).AnyTimes() + // Create snapshots (timestamp will be consistent) + c.EXPECT().CreateServerSnapshot(gomock.Any(), "server-1", gomock.Any()).Return("snapshot-1", nil).Times(1) + c.EXPECT().CreateServerSnapshot(gomock.Any(), "server-2", gomock.Any()).Return("snapshot-2", nil).Times(1) - // Expect instance deletion - c.EXPECT().DeleteServer(gomock.Any(), gomock.Any()).Times(2).Return(nil) + // Wait for snapshots to complete + activeImg := &images.Image{Status: "active"} + c.EXPECT().GetImage(gomock.Any(), "snapshot-1").Return(activeImg, nil).AnyTimes() + c.EXPECT().GetImage(gomock.Any(), "snapshot-2").Return(activeImg, nil).AnyTimes() // Network operations for saving configuration setupOpenStackNetworkOps(c) - // Server details for metadata extraction - c.EXPECT().GetServer(gomock.Any(), gomock.Any()).Return(&servers.Server{ - ID: "testinfra-ACTIVE-0", - Name: "testinfra-ACTIVE-0", - Status: "ACTIVE", - Flavor: map[string]interface{}{"id": "flavor-1"}, - Metadata: map[string]string{"openshiftClusterID": "testinfra"}, - }, nil).AnyTimes() + // Security groups for configuration + c.EXPECT().GetServerSecurityGroupNames(gomock.Any(), gomock.Any()).Return([]string{"default"}, nil).AnyTimes() + + // Tags for configuration + c.EXPECT().GetServerTags(gomock.Any(), gomock.Any()).Return([]string{"tag1", "tag2"}, nil).AnyTimes() + + // Delete instances + c.EXPECT().DeleteServer(gomock.Any(), "server-1").Return(nil).Times(1) + c.EXPECT().DeleteServer(gomock.Any(), "server-2").Return(nil).Times(1) - // ListServers expectations for cleanup verification - gomock.InOrder( - // First calls: find instances to hibernate - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ - {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0", Status: "ACTIVE"}, - {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1", Status: "ACTIVE"}, - }, nil).Times(1), // Called by findInstancesByPrefix - - // After deletion: verify cleanup (waitForInstanceCleanup) - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ - {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0", Status: "DELETING"}, - {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1", Status: "DELETING"}, - }, nil).Times(1), // First cleanup check - still deleting - - // Final check: all gone - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), - ) + // Wait for cleanup - first check shows deleting, second shows empty + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "server-1", Name: "testinfra-master-0", Status: "DELETING"}, + }, nil).Times(1) + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) + + // Cleanup old snapshots + c.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return([]images.Image{}, nil).Times(1) }, }, { - name: "stop running instances with existing snapshots to cleanup", + name: "pause failure should unpause previous instances", instances: map[string]int{"ACTIVE": 2}, setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { - // ===== FIX: Return snapshots that match our filter criteria ===== - oldSnapshots := []images.Image{ - { - ID: "old-snap-1", - Name: "testinfra-master-0-hibernation-20240101", - Metadata: map[string]string{ - "cluster_infra_id": "testinfra", - "hive_hibernation": "true", - }, - }, - { - ID: "old-snap-2", - Name: "testinfra-worker-0-hibernation-20240101", - Metadata: map[string]string{ - "cluster_infra_id": "testinfra", - "hive_hibernation": "true", - }, - }, - } - c.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return(oldSnapshots, nil).Times(1) + // Initial server discovery + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "server-1", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "server-2", Name: "testinfra-worker-0", Status: "ACTIVE"}, + }, nil).Times(1) - c.EXPECT().DeleteImage(gomock.Any(), "old-snap-1").Return(nil).Times(1) - c.EXPECT().DeleteImage(gomock.Any(), "old-snap-2").Return(nil).Times(1) + // First pause succeeds + c.EXPECT().PauseServer(gomock.Any(), "server-1").Return(nil).Times(1) + // Second pause fails + c.EXPECT().PauseServer(gomock.Any(), "server-2").Return(errors.New("pause failed")).Times(1) + + // Should unpause the first server due to failure + c.EXPECT().UnpauseServer(gomock.Any(), "server-1").Return(nil).Times(1) + }, + expectErr: true, + }, + { + name: "snapshot creation with old snapshot cleanup", + instances: map[string]int{"ACTIVE": 1}, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // Initial server discovery + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "server-1", Name: "testinfra-master-0", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, + }, nil).Times(1) - // Continue with normal hibernation flow - c.EXPECT().CreateServerSnapshot(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return("snapshot-1", nil) + // Pause + c.EXPECT().PauseServer(gomock.Any(), "server-1").Return(nil).Times(1) - // Add these missing expectations: - activeImg := &images.Image{} - activeImg.Status = "active" - c.EXPECT().GetImage(gomock.Any(), gomock.Any()).Return(activeImg, nil).AnyTimes() + // Snapshot creation + c.EXPECT().CreateServerSnapshot(gomock.Any(), "server-1", gomock.Any()).Return("snapshot-new", nil).Times(1) - c.EXPECT().DeleteServer(gomock.Any(), gomock.Any()).Times(2).Return(nil) + // Wait for snapshot + activeImg := &images.Image{Status: "active"} + c.EXPECT().GetImage(gomock.Any(), "snapshot-new").Return(activeImg, nil).AnyTimes() + // Network operations setupOpenStackNetworkOps(c) - c.EXPECT().GetServer(gomock.Any(), gomock.Any()).Return(&servers.Server{ - ID: "testinfra-ACTIVE-0", - Name: "testinfra-ACTIVE-0", - Status: "ACTIVE", - Flavor: map[string]interface{}{"id": "flavor-1"}, - Metadata: map[string]string{"openshiftClusterID": "testinfra"}, - }, nil).AnyTimes() + // Security groups for configuration + c.EXPECT().GetServerSecurityGroupNames(gomock.Any(), gomock.Any()).Return([]string{"default"}, nil).AnyTimes() - gomock.InOrder( - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ - {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0", Status: "ACTIVE"}, - {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1", Status: "ACTIVE"}, - }, nil).Times(1), - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), - ) + // Tags for configuration + c.EXPECT().GetServerTags(gomock.Any(), gomock.Any()).Return([]string{"tag1", "tag2"}, nil).AnyTimes() + + // Delete instance + c.EXPECT().DeleteServer(gomock.Any(), "server-1").Return(nil).Times(1) + + // Cleanup wait + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) + + // Old snapshot cleanup - return old snapshots and delete them + oldSnapshots := []images.Image{ + {ID: "old-snap-1", Name: "testinfra-master-0-hibernation-20240101"}, + {ID: "old-snap-2", Name: "testinfra-worker-0-hibernation-20240102"}, + } + c.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return(oldSnapshots, nil).Times(1) + c.EXPECT().DeleteImage(gomock.Any(), "old-snap-1").Return(nil).Times(1) + c.EXPECT().DeleteImage(gomock.Any(), "old-snap-2").Return(nil).Times(1) }, }, { name: "unable to list servers", - instances: map[string]int{"ACTIVE": 2}, + instances: map[string]int{"ACTIVE": 1}, setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return(nil, errors.New("cannot list servers")) + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return(nil, errors.New("cannot list servers")).Times(1) }, expectErr: true, }, @@ -177,9 +180,6 @@ func TestOpenStackStopMachines(t *testing.T) { defer ctrl.Finish() openstackClient := mockopenstackclient.NewMockClient(ctrl) - if !test.expectErr { - setupOpenStackClientInstances(openstackClient, test.instances) - } if test.setupClient != nil { test.setupClient(t, openstackClient) } @@ -203,46 +203,37 @@ func TestOpenStackStartMachines(t *testing.T) { instances map[string]int setupClient func(*testing.T, *mockopenstackclient.MockClient) withSecret bool + expectErr bool }{ { - name: "start with existing instances", + name: "start with existing instances - clear hibernation config", instances: map[string]int{"ACTIVE": 2}, setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { - // Setup ListServers for existing instances check + // Check existing instances - return enough instances c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ - {ID: "testinfra-ACTIVE-0", Name: "testinfra-ACTIVE-0", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, - {ID: "testinfra-ACTIVE-1", Name: "testinfra-ACTIVE-1", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, - }, nil).AnyTimes() + {ID: "server-1", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "server-2", Name: "testinfra-worker-0", Status: "ACTIVE"}, + }, nil).Times(1) }, + withSecret: true, // Has hibernation config but instances already exist }, { - name: "start from hibernation config with quota validation", + name: "start from hibernation config - successful restoration", instances: map[string]int{}, // No existing instances withSecret: true, setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { - // Setup quota checking mocks (sufficient resources) - setupOpenStackQuotaMocks(c) - - // Expect image validation - activeImg := &images.Image{} - activeImg.Status = "active" - c.EXPECT().GetImage(gomock.Any(), gomock.Any()).Return(activeImg, nil).AnyTimes() - - gomock.InOrder( - // First call: no existing instances (for existing instance check) - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), + // First check: no existing instances + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) - // Second call: still no instances (for missing instances check) - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), + // Second check: still no instances (for missing instances check) + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) - // After creation: return the newly created instances - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ - {ID: "new-testinfra-master-0", Name: "testinfra-master-0", Status: "ACTIVE"}, - {ID: "new-testinfra-worker-0", Name: "testinfra-worker-0", Status: "ACTIVE"}, - }, nil).AnyTimes(), - ) + // Snapshot validation + activeImg := &images.Image{Status: "active"} + c.EXPECT().GetImage(gomock.Any(), "snapshot-1").Return(activeImg, nil).Times(1) + c.EXPECT().GetImage(gomock.Any(), "snapshot-2").Return(activeImg, nil).Times(1) - // Expect server creation + // Server creation c.EXPECT().CreateServerFromOpts(gomock.Any(), gomock.Any()).Times(2).DoAndReturn( func(ctx context.Context, opts *servers.CreateOpts) (*servers.Server, error) { return &servers.Server{ @@ -252,64 +243,108 @@ func TestOpenStackStartMachines(t *testing.T) { }, nil }) - // Expect status checks for waiting - activeServer := &servers.Server{Status: "ACTIVE"} - c.EXPECT().GetServer(gomock.Any(), gomock.Any()).Return(activeServer, nil).AnyTimes() + // Wait for instances to be active - return active instances + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "new-testinfra-master-0", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "new-testinfra-worker-0", Name: "testinfra-worker-0", Status: "ACTIVE"}, + }, nil).AnyTimes() + + // FIXED: Add tag restoration expectations + c.EXPECT().SetServerTags(gomock.Any(), "new-testinfra-master-0", []string{"tag1", "tag2"}).Return(nil).Times(1) + c.EXPECT().SetServerTags(gomock.Any(), "new-testinfra-worker-0", []string{"tag1", "tag2"}).Return(nil).Times(1) - // The hibernation config has 2 instances with snapshots + // Cleanup snapshots after successful restoration c.EXPECT().DeleteImage(gomock.Any(), "snapshot-1").Return(nil).Times(1) c.EXPECT().DeleteImage(gomock.Any(), "snapshot-2").Return(nil).Times(1) }, }, { - name: "start from hibernation config - insufficient quota", - instances: map[string]int{}, // No existing instances + name: "start from hibernation - some instances already exist", + instances: map[string]int{}, withSecret: true, setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { - // Setup quota checking mocks (insufficient resources) - setupOpenStackQuotaMocksInsufficient(c) + // First check: one instance already exists + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "existing-1", Name: "testinfra-master-0", Status: "ACTIVE"}, + }, nil).Times(1) - // First call: no existing instances - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) - }, - }, - { - name: "start from hibernation - cleanup continues even if snapshot deletion fails", - instances: map[string]int{}, // No existing instances - withSecret: true, - setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { - // Setup for successful restoration - setupOpenStackQuotaMocks(c) - - activeImg := &images.Image{} - activeImg.Status = "active" - c.EXPECT().GetImage(gomock.Any(), gomock.Any()).Return(activeImg, nil).AnyTimes() - - gomock.InOrder( - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1), - c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ - {ID: "new-testinfra-master-0", Name: "testinfra-master-0", Status: "ACTIVE"}, - {ID: "new-testinfra-worker-0", Name: "testinfra-worker-0", Status: "ACTIVE"}, - }, nil).AnyTimes(), - ) + // Second check for missing instances + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "existing-1", Name: "testinfra-master-0", Status: "ACTIVE"}, + }, nil).Times(1) - c.EXPECT().CreateServerFromOpts(gomock.Any(), gomock.Any()).Times(2).DoAndReturn( + // Only create the missing worker instance + activeImg := &images.Image{Status: "active"} + c.EXPECT().GetImage(gomock.Any(), "snapshot-2").Return(activeImg, nil).Times(1) + + c.EXPECT().CreateServerFromOpts(gomock.Any(), gomock.Any()).Times(1).DoAndReturn( func(ctx context.Context, opts *servers.CreateOpts) (*servers.Server, error) { + // Should only create the worker instance + assert.Equal(t, "testinfra-worker-0", opts.Name) return &servers.Server{ - ID: fmt.Sprintf("new-%s", opts.Name), + ID: "new-testinfra-worker-0", Name: opts.Name, Status: "BUILD", }, nil }) - activeServer := &servers.Server{Status: "ACTIVE"} - c.EXPECT().GetServer(gomock.Any(), gomock.Any()).Return(activeServer, nil).AnyTimes() + // Wait for all instances active + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "existing-1", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "new-testinfra-worker-0", Name: "testinfra-worker-0", Status: "ACTIVE"}, + }, nil).AnyTimes() + + // FIXED: Add specific tag restoration expectations for both instances + c.EXPECT().SetServerTags(gomock.Any(), "existing-1", []string{"tag1", "tag2"}).Return(nil).Times(1) + c.EXPECT().SetServerTags(gomock.Any(), "new-testinfra-worker-0", []string{"tag1", "tag2"}).Return(nil).Times(1) + + // Cleanup snapshots + c.EXPECT().DeleteImage(gomock.Any(), "snapshot-1").Return(nil).Times(1) + c.EXPECT().DeleteImage(gomock.Any(), "snapshot-2").Return(nil).Times(1) + }, + }, + { + name: "start from hibernation - snapshot missing error", + instances: map[string]int{}, + withSecret: true, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // No existing instances + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) + + // First snapshot exists, second is missing + activeImg := &images.Image{Status: "active"} + c.EXPECT().GetImage(gomock.Any(), "snapshot-1").Return(activeImg, nil).Times(1) + c.EXPECT().GetImage(gomock.Any(), "snapshot-2").Return(nil, errors.New("snapshot not found")).Times(1) + + // Code still tries to create the first instance (snapshot-1 was valid) + c.EXPECT().CreateServerFromOpts(gomock.Any(), gomock.Any()).Times(1).Return(&servers.Server{ + ID: "new-master", Name: "testinfra-master-0", Status: "BUILD", + }, nil) - // First snapshot deletes successfully + // It will try to wait for instances to be active + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "new-master", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "fake-worker", Name: "testinfra-worker-0", Status: "ACTIVE"}, + }, nil).AnyTimes() + + // FIXED: Add tag restoration expectations for both instances + c.EXPECT().SetServerTags(gomock.Any(), "new-master", []string{"tag1", "tag2"}).Return(nil).Times(1) + c.EXPECT().SetServerTags(gomock.Any(), "fake-worker", []string{"tag1", "tag2"}).Return(nil).Times(1) + + // FIXED: Add both snapshot cleanup calls c.EXPECT().DeleteImage(gomock.Any(), "snapshot-1").Return(nil).Times(1) - // Second snapshot fails to delete (should not cause overall failure) - c.EXPECT().DeleteImage(gomock.Any(), "snapshot-2").Return(errors.New("snapshot in use")).Times(1) + c.EXPECT().DeleteImage(gomock.Any(), "snapshot-2").Return(nil).Times(1) + }, + expectErr: false, // Code continues even with partial failure + }, + { + name: "no hibernation config and no instances", + instances: map[string]int{}, + withSecret: false, + setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { + // Check for existing instances - none found + c.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(1) }, }, } @@ -321,11 +356,6 @@ func TestOpenStackStartMachines(t *testing.T) { openstackClient := mockopenstackclient.NewMockClient(ctrl) - // Only call setupOpenStackClientInstances for simple tests, not hibernation config tests - if !test.withSecret { - setupOpenStackClientInstances(openstackClient, test.instances) - } - if test.setupClient != nil { test.setupClient(t, openstackClient) } @@ -340,38 +370,54 @@ func TestOpenStackStartMachines(t *testing.T) { actuator := testOpenStackActuator(openstackClient) err := actuator.StartMachines(testOpenStackClusterDeployment(), c, log.New()) - // Check expectations based on test case - if test.name == "start from hibernation config - insufficient quota" { + if test.expectErr { assert.NotNil(t, err) - assert.Contains(t, err.Error(), "resource validation failed") } else { - // Even if snapshot deletion fails, StartMachines should succeed assert.Nil(t, err) } }) } } -// Test quota checking functionality separately -func TestOpenStackQuotaValidation(t *testing.T) { +func TestOpenStackMachinesRunning(t *testing.T) { tests := []struct { - name string - setupClient func(*testing.T, *mockopenstackclient.MockClient) - expectErr bool + name string + servers []servers.Server + expectedRunning bool + expectedNames []string }{ { - name: "sufficient quotas", - setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { - setupOpenStackQuotaMocks(c) + name: "no instances found", + servers: []servers.Server{}, + expectedRunning: false, + expectedNames: []string{"no instances found"}, + }, + { + name: "all instances running", + servers: []servers.Server{ + {ID: "1", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "2", Name: "testinfra-worker-0", Status: "ACTIVE"}, }, - expectErr: false, + expectedRunning: true, + expectedNames: []string{}, }, { - name: "insufficient quotas", - setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { - setupOpenStackQuotaMocksInsufficient(c) + name: "instances being deleted", + servers: []servers.Server{ + {ID: "1", Name: "testinfra-master-0", Status: "DELETING"}, + {ID: "2", Name: "testinfra-worker-0", Status: "DELETING"}, }, - expectErr: true, + expectedRunning: false, + expectedNames: []string{"instances-being-deleted"}, + }, + { + name: "mixed states", + servers: []servers.Server{ + {ID: "1", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "2", Name: "testinfra-worker-0", Status: "SHUTOFF"}, + }, + expectedRunning: true, // At least one is running + expectedNames: []string{}, }, } @@ -381,59 +427,71 @@ func TestOpenStackQuotaValidation(t *testing.T) { defer ctrl.Finish() openstackClient := mockopenstackclient.NewMockClient(ctrl) - test.setupClient(t, openstackClient) + openstackClient.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return(test.servers, nil).Times(1) actuator := testOpenStackActuator(openstackClient) + c := testfake.NewFakeClientBuilder().Build() - // Create test instances for validation - instances := []OpenStackInstanceConfig{ - {Name: "test-master-0", Flavor: "flavor-1"}, - {Name: "test-worker-0", Flavor: "flavor-2"}, - } - - err := actuator.validateRestoreResources(openstackClient, instances, log.New()) + running, names, err := actuator.MachinesRunning(testOpenStackClusterDeployment(), c, log.New()) - if test.expectErr { - assert.NotNil(t, err) - assert.Contains(t, err.Error(), "insufficient") - } else { - assert.Nil(t, err) - } + assert.Nil(t, err) + assert.Equal(t, test.expectedRunning, running) + assert.Equal(t, test.expectedNames, names) }) } } -func testOpenStackActuator(openstackClient openstackclient.Client) *openstackActuator { - return &openstackActuator{ - openstackClientFn: func(*hivev1.ClusterDeployment, client.Client, log.FieldLogger) (openstackclient.Client, error) { - return openstackClient, nil +func TestOpenStackMachinesStopped(t *testing.T) { + tests := []struct { + name string + servers []servers.Server + expectedStopped bool + expectedNames []string + }{ + { + name: "no instances found - stopped", + servers: []servers.Server{}, + expectedStopped: true, + expectedNames: nil, + }, + { + name: "instances still running", + servers: []servers.Server{ + {ID: "1", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "2", Name: "testinfra-worker-0", Status: "ACTIVE"}, + }, + expectedStopped: false, + expectedNames: []string{"testinfra-master-0", "testinfra-worker-0"}, }, } -} -func setupOpenStackClientInstances(openstackClient *mockopenstackclient.MockClient, statuses map[string]int) { - var allServers []servers.Server - for status, count := range statuses { - for i := 0; i < count; i++ { - serverName := fmt.Sprintf("%s-%d", status, i) - server := servers.Server{ - ID: fmt.Sprintf("testinfra-%s", serverName), - Name: fmt.Sprintf("testinfra-%s", serverName), - Status: status, - Flavor: map[string]interface{}{"id": "flavor-1"}, - } - allServers = append(allServers, server) - } - } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + openstackClient := mockopenstackclient.NewMockClient(ctrl) + openstackClient.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return(test.servers, nil).Times(1) + + actuator := testOpenStackActuator(openstackClient) + c := testfake.NewFakeClientBuilder().Build() - // Only setup ListServers for simple tests (empty instances) - if len(allServers) == 0 { - openstackClient.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return(allServers, nil).AnyTimes() + stopped, names, err := actuator.MachinesStopped(testOpenStackClusterDeployment(), c, log.New()) + + assert.Nil(t, err) + assert.Equal(t, test.expectedStopped, stopped) + assert.Equal(t, test.expectedNames, names) + }) } +} - // Setup GetServer calls - for _, server := range allServers { - openstackClient.EXPECT().GetServer(gomock.Any(), server.ID).Return(&server, nil).AnyTimes() +// Helper functions + +func testOpenStackActuator(openstackClient openstackclient.Client) *openstackActuator { + return &openstackActuator{ + openstackClientFn: func(*hivev1.ClusterDeployment, client.Client, log.FieldLogger) (openstackclient.Client, error) { + return openstackClient, nil + }, } } @@ -448,94 +506,10 @@ func setupOpenStackNetworkOps(openstackClient *mockopenstackclient.MockClient) { // Port listing ports := []ports.Port{ - {ID: "port-1", Name: "testinfra-ACTIVE-0"}, - {ID: "port-2", Name: "testinfra-ACTIVE-1"}, + {ID: "port-1", Name: "testinfra-master-0"}, + {ID: "port-2", Name: "testinfra-worker-0"}, } openstackClient.EXPECT().ListPorts(gomock.Any()).Return(ports, nil).AnyTimes() - - // Security groups - openstackClient.EXPECT().GetServerSecurityGroupNames(gomock.Any(), gomock.Any()).Return([]string{"default"}, nil).AnyTimes() -} - -func setupOpenStackClientWithSnapshotCleanup(openstackClient *mockopenstackclient.MockClient) { - // For ListImages (cleanup check) - openstackClient.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return([]images.Image{}, nil).AnyTimes() - - // For DeleteImage (cleanup action) - openstackClient.EXPECT().DeleteImage(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() -} - -// Setup quota mocks with sufficient resources -func setupOpenStackQuotaMocks(openstackClient *mockopenstackclient.MockClient) { - // Mock quotas - plenty of resources available - quotas := "asets.QuotaSet{ - Instances: 20, - Cores: 40, - RAM: 81920, - } - openstackClient.EXPECT().GetComputeQuotas(gomock.Any()).Return(quotas, nil).AnyTimes() - - // Mock usage - minimal usage - var usage *usage.TenantUsage = nil - openstackClient.EXPECT().GetComputeUsage(gomock.Any()).Return(usage, nil).AnyTimes() - - // Mock flavor details for our test flavors - flavorDetails := &flavors.Flavor{ - ID: "flavor-1", - Name: "m1.small", - VCPUs: 2, - RAM: 2048, - Disk: 10, - } - openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-1").Return(flavorDetails, nil).AnyTimes() - - // Also mock flavor-2 for worker - flavorDetails2 := &flavors.Flavor{ - ID: "flavor-2", - Name: "m1.medium", - VCPUs: 4, - RAM: 4096, - Disk: 20, - } - openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-2").Return(flavorDetails2, nil).AnyTimes() -} - -// Setup quota mocks with insufficient resources -func setupOpenStackQuotaMocksInsufficient(openstackClient *mockopenstackclient.MockClient) { - // Mock quotas - very limited resources - quotas := "asets.QuotaSet{ - Instances: 2, - Cores: 4, - RAM: 4096, - } - openstackClient.EXPECT().GetComputeQuotas(gomock.Any()).Return(quotas, nil).AnyTimes() - - // Mock usage - most resources already used - usage := &usage.TenantUsage{ - ServerUsages: make([]usage.ServerUsage, 1), // 1 instance used - TotalVCPUsUsage: 3, - TotalMemoryMBUsage: 3072, - } - openstackClient.EXPECT().GetComputeUsage(gomock.Any()).Return(usage, nil).AnyTimes() - - // Mock flavor details - will show insufficient resources - flavorDetails1 := &flavors.Flavor{ - ID: "flavor-1", - Name: "m1.small", - VCPUs: 2, - RAM: 2048, - Disk: 10, - } - openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-1").Return(flavorDetails1, nil).AnyTimes() - - flavorDetails2 := &flavors.Flavor{ - ID: "flavor-2", - Name: "m1.medium", - VCPUs: 4, - RAM: 4096, - Disk: 20, - } - openstackClient.EXPECT().GetFlavorDetails(gomock.Any(), "flavor-2").Return(flavorDetails2, nil).AnyTimes() } func testOpenStackClusterDeployment() *hivev1.ClusterDeployment { @@ -556,6 +530,7 @@ func testHibernationSecretWithMetadata() *corev1.Secret { Flavor: "flavor-1", PortID: "port-1", SnapshotID: "snapshot-1", + SnapshotName: "testinfra-master-0-hibernation-20240101-120000", SecurityGroups: []string{"default"}, ClusterID: "testinfra", NetworkID: "network-1", @@ -564,12 +539,14 @@ func testHibernationSecretWithMetadata() *corev1.Secret { "Name": "testinfra-master", "openshiftClusterID": "testinfra", }, + Tags: []string{"tag1", "tag2"}, }, { Name: "testinfra-worker-0", Flavor: "flavor-2", PortID: "port-2", SnapshotID: "snapshot-2", + SnapshotName: "testinfra-worker-0-hibernation-20240101-120000", SecurityGroups: []string{"default"}, ClusterID: "testinfra", NetworkID: "network-1", @@ -578,6 +555,7 @@ func testHibernationSecretWithMetadata() *corev1.Secret { "Name": "testinfra-worker", "openshiftClusterID": "testinfra", }, + Tags: []string{"tag1", "tag2"}, }, } @@ -593,3 +571,131 @@ func testHibernationSecretWithMetadata() *corev1.Secret { }, } } + +func TestExactNameFiltering(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + openstackClient := mockopenstackclient.NewMockClient(ctrl) + + // Mock the exact name filtering call + expectedSnapshots := []images.Image{ + {ID: "snapshot-1", Name: "testinfra-master-0-hibernation-20240101-120000"}, + {ID: "snapshot-2", Name: "testinfra-worker-0-hibernation-20240101-120000"}, + } + + openstackClient.EXPECT().ListImages(gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, opts *images.ListOpts) ([]images.Image, error) { + // Verify the "in:" filter is being used + expectedFilter := "in:testinfra-master-0-hibernation-20240101-120000,testinfra-worker-0-hibernation-20240101-120000" + assert.Equal(t, expectedFilter, opts.Name) + return expectedSnapshots, nil + }).Times(1) + + actuator := testOpenStackActuator(openstackClient) + c := testfake.NewFakeClientBuilder().WithRuntimeObjects(testHibernationSecretWithMetadata()).Build() + + cd := testOpenStackClusterDeployment() + snapshots, err := actuator.findHibernationSnapshotsByExactNames(cd, c, openstackClient, "testinfra", log.New()) + + assert.Nil(t, err) + assert.Equal(t, 2, len(snapshots)) + assert.Equal(t, "snapshot-1", snapshots[0].ID) +} + +func TestSnapshotCleanupWithExactFiltering(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + openstackClient := mockopenstackclient.NewMockClient(ctrl) + + // Mock exact name filtering returning both old and new snapshots + allSnapshots := []images.Image{ + {ID: "snapshot-new", Name: "testinfra-master-0-hibernation-20240201-120000"}, // Current + {ID: "snapshot-old", Name: "testinfra-master-0-hibernation-20240101-120000"}, // Old + } + + openstackClient.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return(allSnapshots, nil).Times(1) + + // Should only delete the old snapshot + openstackClient.EXPECT().DeleteImage(gomock.Any(), "snapshot-old").Return(nil).Times(1) + + actuator := testOpenStackActuator(openstackClient) + c := testfake.NewFakeClientBuilder().WithRuntimeObjects(testHibernationSecretWithMetadata()).Build() + + cd := testOpenStackClusterDeployment() + err := actuator.cleanupOldSnapshotsAfterDeletion(cd, c, openstackClient, "testinfra", []string{"snapshot-new"}, log.New()) + + assert.Nil(t, err) +} + +func TestStartMachinesWithTagRestoration(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + openstackClient := mockopenstackclient.NewMockClient(ctrl) + + // Initial checks - no existing instances + openstackClient.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{}, nil).Times(2) + + // Snapshot validation + activeImg := &images.Image{Status: "active"} + openstackClient.EXPECT().GetImage(gomock.Any(), "snapshot-1").Return(activeImg, nil).Times(1) + openstackClient.EXPECT().GetImage(gomock.Any(), "snapshot-2").Return(activeImg, nil).Times(1) + + // Server creation + openstackClient.EXPECT().CreateServerFromOpts(gomock.Any(), gomock.Any()).Times(2).DoAndReturn( + func(ctx context.Context, opts *servers.CreateOpts) (*servers.Server, error) { + return &servers.Server{ + ID: fmt.Sprintf("new-%s", opts.Name), + Name: opts.Name, + Status: "BUILD", + }, nil + }) + + // Wait for instances to be active + openstackClient.EXPECT().ListServers(gomock.Any(), gomock.Any()).Return([]servers.Server{ + {ID: "new-testinfra-master-0", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "new-testinfra-worker-0", Name: "testinfra-worker-0", Status: "ACTIVE"}, + }, nil).AnyTimes() + + // Tag restoration calls + openstackClient.EXPECT().SetServerTags(gomock.Any(), "new-testinfra-master-0", []string{"tag1", "tag2"}).Return(nil).Times(1) + openstackClient.EXPECT().SetServerTags(gomock.Any(), "new-testinfra-worker-0", []string{"tag1", "tag2"}).Return(nil).Times(1) + + // Cleanup snapshots + openstackClient.EXPECT().DeleteImage(gomock.Any(), "snapshot-1").Return(nil).Times(1) + openstackClient.EXPECT().DeleteImage(gomock.Any(), "snapshot-2").Return(nil).Times(1) + + actuator := testOpenStackActuator(openstackClient) + c := testfake.NewFakeClientBuilder().WithRuntimeObjects(testHibernationSecretWithMetadata()).Build() + + err := actuator.StartMachines(testOpenStackClusterDeployment(), c, log.New()) + assert.Nil(t, err) +} + +func TestPauseUnpauseFlow(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + openstackClient := mockopenstackclient.NewMockClient(ctrl) + + servers := []*servers.Server{ + {ID: "server-1", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "server-2", Name: "testinfra-worker-0", Status: "ACTIVE"}, + } + + // Test successful pause + openstackClient.EXPECT().PauseServer(gomock.Any(), "server-1").Return(nil).Times(1) + openstackClient.EXPECT().PauseServer(gomock.Any(), "server-2").Return(nil).Times(1) + + actuator := testOpenStackActuator(openstackClient) + err := actuator.pauseInstances(openstackClient, servers, log.New()) + assert.Nil(t, err) + + // Test unpause + openstackClient.EXPECT().UnpauseServer(gomock.Any(), "server-1").Return(nil).Times(1) + openstackClient.EXPECT().UnpauseServer(gomock.Any(), "server-2").Return(nil).Times(1) + + actuator.unpauseInstances(openstackClient, servers, log.New()) +} diff --git a/pkg/openstackclient/client.go b/pkg/openstackclient/client.go index 41b3da313a4..4c984976ba1 100644 --- a/pkg/openstackclient/client.go +++ b/pkg/openstackclient/client.go @@ -4,18 +4,16 @@ import ( "context" "fmt" - "gopkg.in/yaml.v2" - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack" - "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups" - "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage" - "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" "github.com/gophercloud/gophercloud/openstack/networking/v2/ports" + "github.com/gophercloud/utils/openstack/clientconfig" + + "gopkg.in/yaml.v2" + "github.com/openshift/hive/pkg/constants" corev1 "k8s.io/api/core/v1" @@ -37,68 +35,20 @@ type Client interface { ListImages(ctx context.Context, opts *images.ListOpts) ([]images.Image, error) DeleteImage(ctx context.Context, imageID string) error + // Instance state management + PauseServer(ctx context.Context, serverID string) error + UnpauseServer(ctx context.Context, serverID string) error + // Networks GetNetworkByName(ctx context.Context, networkName string) (*networks.Network, error) ListPorts(ctx context.Context) ([]ports.Port, error) + // Tags + GetServerTags(ctx context.Context, serverID string) ([]string, error) + SetServerTags(ctx context.Context, serverID string, tags []string) error + // Security Group Names GetServerSecurityGroupNames(ctx context.Context, serverID string) ([]string, error) - - // Project resources - GetComputeQuotas(ctx context.Context) (*quotasets.QuotaSet, error) - GetComputeUsage(ctx context.Context) (*usage.TenantUsage, error) - GetFlavorDetails(ctx context.Context, flavorID string) (*flavors.Flavor, error) -} - -type ResourceRequirements struct { - Instances int - VCPUs int - RAM int -} - -type Credentials struct { - AuthURL string `json:"auth_url"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - UserID string `json:"user_id,omitempty"` - ProjectID string `json:"project_id,omitempty"` - ProjectName string `json:"project_name,omitempty"` - UserDomainName string `json:"user_domain_name,omitempty"` - UserDomainID string `json:"user_domain_id,omitempty"` - ProjectDomainName string `json:"project_domain_name,omitempty"` - ProjectDomainID string `json:"project_domain_id,omitempty"` - RegionName string `json:"region_name,omitempty"` - Interface string `json:"interface,omitempty"` - IdentityAPIVersion string `json:"identity_api_version,omitempty"` - - TenantID string `json:"tenant_id,omitempty"` - TenantName string `json:"tenant_name,omitempty"` - DomainID string `json:"domain_id,omitempty"` - DomainName string `json:"domain_name,omitempty"` - Region string `json:"region,omitempty"` -} - -type CloudsYAML struct { - Clouds map[string]CloudConfig `yaml:"clouds"` -} - -type CloudConfig struct { - Auth CloudAuth `yaml:"auth"` - Region string `yaml:"region_name"` - Interface string `yaml:"interface"` - Version string `yaml:"identity_api_version"` -} - -type CloudAuth struct { - AuthURL string `yaml:"auth_url"` - Username string `yaml:"username"` - Password string `yaml:"password"` - ProjectID string `yaml:"project_id"` - ProjectName string `yaml:"project_name"` - UserDomainName string `yaml:"user_domain_name"` - ProjectDomainName string `yaml:"project_domain_name"` - UserDomainID string `yaml:"user_domain_id"` - ProjectDomainID string `yaml:"project_domain_id"` } type openstackClient struct { @@ -106,9 +56,10 @@ type openstackClient struct { computeClient *gophercloud.ServiceClient imageClient *gophercloud.ServiceClient networkClient *gophercloud.ServiceClient - credentials *Credentials } +var _ Client = &openstackClient{} + // Implementation of server methods func (c *openstackClient) ListServers(ctx context.Context, opts *servers.ListOpts) ([]servers.Server, error) { if opts == nil { @@ -129,6 +80,63 @@ func (c *openstackClient) DeleteServer(ctx context.Context, serverID string) err return servers.Delete(c.computeClient, serverID).ExtractErr() } +// PauseServer pauses a server using the raw OpenStack API +func (c *openstackClient) PauseServer(ctx context.Context, serverID string) error { + url := c.computeClient.ServiceURL("servers", serverID, "action") + + reqBody := map[string]interface{}{ + "pause": nil, + } + + _, err := c.computeClient.Post(url, reqBody, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return err +} + +// UnpauseServer unpauses a server using the raw OpenStack API +func (c *openstackClient) UnpauseServer(ctx context.Context, serverID string) error { + url := c.computeClient.ServiceURL("servers", serverID, "action") + + reqBody := map[string]interface{}{ + "unpause": nil, + } + + _, err := c.computeClient.Post(url, reqBody, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + return err +} + +func (c *openstackClient) GetServerTags(ctx context.Context, serverID string) ([]string, error) { + url := c.computeClient.ServiceURL("servers", serverID, "tags") + + var result struct { + Tags []string `json:"tags"` + } + + _, err := c.computeClient.Get(url, &result, nil) + if err != nil { + // Just return empty tags if the API call fails for any reason + return []string{}, nil + } + + return result.Tags, nil +} + +func (c *openstackClient) SetServerTags(ctx context.Context, serverID string, tags []string) error { + url := c.computeClient.ServiceURL("servers", serverID, "tags") + + reqBody := map[string][]string{ + "tags": tags, + } + + _, err := c.computeClient.Put(url, reqBody, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + return err +} + // Create a snapshot of the specified server func (c *openstackClient) CreateServerSnapshot(ctx context.Context, serverID, snapshotName string) (string, error) { createImageOpts := servers.CreateImageOpts{ @@ -227,158 +235,65 @@ func (c *openstackClient) GetServerSecurityGroupNames(ctx context.Context, serve return secGroupNames, nil } -// Gets the compute quotas for the current project -func (c *openstackClient) GetComputeQuotas(ctx context.Context) (*quotasets.QuotaSet, error) { - projectID := c.credentials.ProjectID - if projectID == "" { - projectID = c.credentials.TenantID - } - if projectID == "" { - return nil, fmt.Errorf("no project ID found in credentials") - } - - quotaSet, err := quotasets.Get(c.computeClient, projectID).Extract() - if err != nil { - return nil, fmt.Errorf("failed to get quota set: %w", err) - } - - return quotaSet, nil -} - -// Gets the current compute usage for the project -func (c *openstackClient) GetComputeUsage(ctx context.Context) (*usage.TenantUsage, error) { - projectID := c.credentials.ProjectID - if projectID == "" { - projectID = c.credentials.TenantID - } - if projectID == "" { - return nil, fmt.Errorf("no project ID found in credentials") - } - - // Get usage for current project - allPages, err := usage.SingleTenant(c.computeClient, projectID, usage.SingleTenantOpts{}).AllPages() - if err != nil { - return nil, fmt.Errorf("failed to get compute usage pages: %w", err) - } - - // Extract usage from pages - tenantUsage, err := usage.ExtractSingleTenant(allPages) - if err != nil { - return nil, fmt.Errorf("failed to extract compute usage: %w", err) - } - - // Note: tenantUsage can be nil - caller needs to handle this - return tenantUsage, nil -} - -// Gets detailed information about a specific flavor -func (c *openstackClient) GetFlavorDetails(ctx context.Context, flavorID string) (*flavors.Flavor, error) { - return flavors.Get(c.computeClient, flavorID).Extract() -} - // Create a client wrapper object for interacting with OpenStack. func NewClientFromSecret(secret *corev1.Secret) (Client, error) { cloudsYaml, ok := secret.Data[constants.OpenStackCredentialsName] if !ok { return nil, fmt.Errorf("secret does not contain %q data", constants.OpenStackCredentialsName) } - return newClientFromCloudsYAML(cloudsYaml) -} -// Creates a client from clouds.yaml data -func newClientFromCloudsYAML(cloudsYamlData []byte) (Client, error) { - var clouds CloudsYAML - if err := yaml.Unmarshal(cloudsYamlData, &clouds); err != nil { + // Use upstream structs instead of custom ones + var clouds clientconfig.Clouds + if err := yaml.Unmarshal(cloudsYaml, &clouds); err != nil { return nil, fmt.Errorf("failed to parse clouds.yaml: %w", err) } - // Get the "openstack" cloud config - openstackCloud, ok := clouds.Clouds["openstack"] - if !ok { - return nil, fmt.Errorf("no 'openstack' cloud found in clouds.yaml") + // Get the first cloud (or specific cloud name) + var cloud *clientconfig.Cloud + for _, c := range clouds.Clouds { + cloud = &c + break } - // Convert to Credentials struct - creds := &Credentials{ - AuthURL: openstackCloud.Auth.AuthURL, - Username: openstackCloud.Auth.Username, - Password: openstackCloud.Auth.Password, - ProjectID: openstackCloud.Auth.ProjectID, - ProjectName: openstackCloud.Auth.ProjectName, - UserDomainName: openstackCloud.Auth.UserDomainName, - ProjectDomainName: openstackCloud.Auth.ProjectDomainName, - UserDomainID: openstackCloud.Auth.UserDomainID, - ProjectDomainID: openstackCloud.Auth.ProjectDomainID, - RegionName: openstackCloud.Region, - Interface: openstackCloud.Interface, - IdentityAPIVersion: openstackCloud.Version, + if cloud == nil { + return nil, fmt.Errorf("no cloud configuration found in clouds.yaml") } - return newClientFromStruct(creds) -} - -// getRegion helper function -func getRegion(creds *Credentials) string { - if creds.RegionName != "" { - return creds.RegionName - } - if creds.Region != "" { - return creds.Region - } - return "RegionOne" -} - -func newClientFromStruct(creds *Credentials) (*openstackClient, error) { - // Validate required credentials - if creds.AuthURL == "" { - return nil, fmt.Errorf("missing auth_url in credentials") + authInfo := &clientconfig.AuthInfo{ + AuthURL: cloud.AuthInfo.AuthURL, + Username: cloud.AuthInfo.Username, + Password: cloud.AuthInfo.Password, + ProjectID: cloud.AuthInfo.ProjectID, + ProjectName: cloud.AuthInfo.ProjectName, + UserDomainName: cloud.AuthInfo.UserDomainName, + ProjectDomainName: cloud.AuthInfo.ProjectDomainName, } - // Authentication options - authOpts := gophercloud.AuthOptions{ - IdentityEndpoint: creds.AuthURL, - Username: creds.Username, - UserID: creds.UserID, - Password: creds.Password, - TenantID: creds.ProjectID, - TenantName: creds.ProjectName, - DomainName: creds.UserDomainName, - DomainID: creds.UserDomainID, + opts := &clientconfig.ClientOpts{ + AuthInfo: authInfo, + RegionName: cloud.RegionName, + EndpointType: cloud.Interface, } - // Authenticate and get provider client - provider, err := openstack.AuthenticatedClient(authOpts) + provider, err := clientconfig.AuthenticatedClient(opts) if err != nil { - return nil, fmt.Errorf("failed to authenticate with OpenStack: %w", err) + return nil, fmt.Errorf("failed to create authenticated client: %w", err) } - region := getRegion(creds) - - interfaceType := gophercloud.AvailabilityPublic - switch creds.Interface { - case "internal": - interfaceType = gophercloud.AvailabilityInternal - case "admin": - interfaceType = gophercloud.AvailabilityAdmin - } - - endpointOpts := gophercloud.EndpointOpts{ - Region: region, - Availability: interfaceType, - } - - // Create service clients - computeClient, err := openstack.NewComputeV2(provider, endpointOpts) + computeClient, err := clientconfig.NewServiceClient("compute", opts) if err != nil { return nil, fmt.Errorf("failed to create compute client: %w", err) } - imageClient, err := openstack.NewImageServiceV2(provider, endpointOpts) + // Specify microversion in order to allow instance tagging + computeClient.Microversion = "2.26" + + imageClient, err := clientconfig.NewServiceClient("image", opts) if err != nil { return nil, fmt.Errorf("failed to create image client: %w", err) } - networkClient, err := openstack.NewNetworkV2(provider, endpointOpts) + networkClient, err := clientconfig.NewServiceClient("network", opts) if err != nil { return nil, fmt.Errorf("failed to create network client: %w", err) } @@ -388,6 +303,5 @@ func newClientFromStruct(creds *Credentials) (*openstackClient, error) { computeClient: computeClient, imageClient: imageClient, networkClient: networkClient, - credentials: creds, }, nil } diff --git a/pkg/openstackclient/mock/client_generated.go b/pkg/openstackclient/mock/client_generated.go index 23dc54fb60b..2d919d372b7 100644 --- a/pkg/openstackclient/mock/client_generated.go +++ b/pkg/openstackclient/mock/client_generated.go @@ -9,9 +9,6 @@ import ( reflect "reflect" gomock "github.com/golang/mock/gomock" - quotasets "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets" - usage "github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage" - flavors "github.com/gophercloud/gophercloud/openstack/compute/v2/flavors" servers "github.com/gophercloud/gophercloud/openstack/compute/v2/servers" images "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" networks "github.com/gophercloud/gophercloud/openstack/networking/v2/networks" @@ -99,51 +96,6 @@ func (mr *MockClientMockRecorder) DeleteServer(ctx, serverID interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteServer", reflect.TypeOf((*MockClient)(nil).DeleteServer), ctx, serverID) } -// GetComputeQuotas mocks base method. -func (m *MockClient) GetComputeQuotas(ctx context.Context) (*quotasets.QuotaSet, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetComputeQuotas", ctx) - ret0, _ := ret[0].(*quotasets.QuotaSet) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetComputeQuotas indicates an expected call of GetComputeQuotas. -func (mr *MockClientMockRecorder) GetComputeQuotas(ctx interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetComputeQuotas", reflect.TypeOf((*MockClient)(nil).GetComputeQuotas), ctx) -} - -// GetComputeUsage mocks base method. -func (m *MockClient) GetComputeUsage(ctx context.Context) (*usage.TenantUsage, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetComputeUsage", ctx) - ret0, _ := ret[0].(*usage.TenantUsage) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetComputeUsage indicates an expected call of GetComputeUsage. -func (mr *MockClientMockRecorder) GetComputeUsage(ctx interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetComputeUsage", reflect.TypeOf((*MockClient)(nil).GetComputeUsage), ctx) -} - -// GetFlavorDetails mocks base method. -func (m *MockClient) GetFlavorDetails(ctx context.Context, flavorID string) (*flavors.Flavor, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetFlavorDetails", ctx, flavorID) - ret0, _ := ret[0].(*flavors.Flavor) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetFlavorDetails indicates an expected call of GetFlavorDetails. -func (mr *MockClientMockRecorder) GetFlavorDetails(ctx, flavorID interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFlavorDetails", reflect.TypeOf((*MockClient)(nil).GetFlavorDetails), ctx, flavorID) -} - // GetImage mocks base method. func (m *MockClient) GetImage(ctx context.Context, imageID string) (*images.Image, error) { m.ctrl.T.Helper() @@ -204,6 +156,21 @@ func (mr *MockClientMockRecorder) GetServerSecurityGroupNames(ctx, serverID inte return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerSecurityGroupNames", reflect.TypeOf((*MockClient)(nil).GetServerSecurityGroupNames), ctx, serverID) } +// GetServerTags mocks base method. +func (m *MockClient) GetServerTags(ctx context.Context, serverID string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServerTags", ctx, serverID) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServerTags indicates an expected call of GetServerTags. +func (mr *MockClientMockRecorder) GetServerTags(ctx, serverID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerTags", reflect.TypeOf((*MockClient)(nil).GetServerTags), ctx, serverID) +} + // ListImages mocks base method. func (m *MockClient) ListImages(ctx context.Context, opts *images.ListOpts) ([]images.Image, error) { m.ctrl.T.Helper() @@ -248,3 +215,45 @@ func (mr *MockClientMockRecorder) ListServers(ctx, opts interface{}) *gomock.Cal mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListServers", reflect.TypeOf((*MockClient)(nil).ListServers), ctx, opts) } + +// PauseServer mocks base method. +func (m *MockClient) PauseServer(ctx context.Context, serverID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PauseServer", ctx, serverID) + ret0, _ := ret[0].(error) + return ret0 +} + +// PauseServer indicates an expected call of PauseServer. +func (mr *MockClientMockRecorder) PauseServer(ctx, serverID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PauseServer", reflect.TypeOf((*MockClient)(nil).PauseServer), ctx, serverID) +} + +// SetServerTags mocks base method. +func (m *MockClient) SetServerTags(ctx context.Context, serverID string, tags []string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetServerTags", ctx, serverID, tags) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetServerTags indicates an expected call of SetServerTags. +func (mr *MockClientMockRecorder) SetServerTags(ctx, serverID, tags interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetServerTags", reflect.TypeOf((*MockClient)(nil).SetServerTags), ctx, serverID, tags) +} + +// UnpauseServer mocks base method. +func (m *MockClient) UnpauseServer(ctx context.Context, serverID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnpauseServer", ctx, serverID) + ret0, _ := ret[0].(error) + return ret0 +} + +// UnpauseServer indicates an expected call of UnpauseServer. +func (mr *MockClientMockRecorder) UnpauseServer(ctx, serverID interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnpauseServer", reflect.TypeOf((*MockClient)(nil).UnpauseServer), ctx, serverID) +} From 33efb34d6c768ad74c6f53f0bd3062b93e84a771 Mon Sep 17 00:00:00 2001 From: daturece Date: Thu, 2 Oct 2025 16:00:31 +0200 Subject: [PATCH 6/8] fix missing func for testing, try to fix dependencies --- go.mod | 7 +- go.sum | 12 +- pkg/openstackclient/mock/client_generated.go | 2 +- .../clusterdeployment/clusterdeployment.go | 9 +- .../gophercloud/gophercloud/CHANGELOG.md | 73 ++ .../compute/v2/extensions/quotasets/doc.go | 36 - .../v2/extensions/quotasets/requests.go | 103 -- .../v2/extensions/quotasets/results.go | 198 ---- .../compute/v2/extensions/quotasets/urls.go | 25 - .../compute/v2/extensions/usage/doc.go | 58 - .../compute/v2/extensions/usage/requests.go | 134 --- .../compute/v2/extensions/usage/results.go | 191 --- .../compute/v2/extensions/usage/urls.go | 13 - .../openstack/compute/v2/flavors/doc.go | 150 --- .../openstack/compute/v2/flavors/requests.go | 364 ------ .../openstack/compute/v2/flavors/results.go | 271 ----- .../openstack/compute/v2/flavors/urls.go | 53 - .../gophercloud/gophercloud/params.go | 21 +- .../gophercloud/provider_client.go | 2 +- .../gophercloud/gophercloud/service_client.go | 31 +- .../gophercloud/gophercloud/v2/CHANGELOG.md | 64 ++ .../gophercloud/gophercloud/v2/Makefile | 55 +- .../gophercloud/v2/endpoint_search.go | 66 +- .../gophercloud/v2/openstack/auth_env.go | 4 +- .../blockstorage/v3/snapshots/requests.go | 15 + .../blockstorage/v3/snapshots/results.go | 15 + .../blockstorage/v3/snapshots/urls.go | 4 + .../openstack/blockstorage/v3/volumes/doc.go | 7 + .../blockstorage/v3/volumes/requests.go | 27 +- .../blockstorage/v3/volumes/results.go | 5 + .../blockstorage/v3/volumetypes/requests.go | 31 +- .../gophercloud/v2/openstack/client.go | 56 +- .../openstack/compute/v2/servers/requests.go | 9 + .../gophercloud/v2/openstack/endpoint.go | 190 +++ .../v2/openstack/endpoint_location.go | 98 +- .../openstack/identity/v2/tenants/requests.go | 18 +- .../openstack/identity/v3/oauth1/requests.go | 8 +- .../loadbalancer/v2/l7policies/requests.go | 8 +- .../loadbalancer/v2/listeners/requests.go | 2 +- .../loadbalancer/v2/loadbalancers/requests.go | 2 +- .../loadbalancer/v2/monitors/requests.go | 41 +- .../loadbalancer/v2/monitors/results.go | 25 +- .../layer3/floatingips/constants.go | 7 + .../extensions/layer3/floatingips/requests.go | 20 +- .../extensions/layer3/floatingips/results.go | 3 + .../v2/extensions/layer3/routers/requests.go | 82 +- .../v2/extensions/layer3/routers/results.go | 62 +- .../v2/extensions/security/groups/requests.go | 48 +- .../v2/extensions/security/groups/results.go | 3 + .../v2/extensions/security/rules/requests.go | 74 +- .../v2/extensions/security/rules/results.go | 54 + .../v2/extensions/trunks/constants.go | 9 + .../v2/extensions/trunks/requests.go | 48 +- .../v2/extensions/trunks/results.go | 1 + .../networking/v2/networks/constants.go | 8 + .../networking/v2/networks/requests.go | 33 +- .../networking/v2/ports/constants.go | 8 + .../openstack/networking/v2/ports/requests.go | 1 + .../openstack/networking/v2/ports/results.go | 2 +- .../networking/v2/subnets/requests.go | 15 +- .../networking/v2/subnets/results.go | 51 + .../v2/openstack/utils/base_endpoint.go | 19 +- .../v2/openstack/utils/choose_version.go | 122 +- .../v2/openstack/utils/discovery.go | 372 ++++++ .../gophercloud/v2/provider_client.go | 2 +- .../gophercloud/v2/service_client.go | 13 +- vendor/github.com/gophercloud/utils/LICENSE | 201 ++++ .../github.com/gophercloud/utils/env/env.go | 12 + .../gophercloud/utils/env/env_windows.go | 106 ++ .../gophercloud/utils/gnocchi/client.go | 25 + .../gophercloud/utils/gnocchi/results.go | 37 + .../gophercloud/utils/internal/pkg.go | 1 + .../gophercloud/utils/internal/util.go | 111 ++ .../utils/openstack/clientconfig/doc.go | 46 + .../utils/openstack/clientconfig/requests.go | 1023 +++++++++++++++++ .../utils/openstack/clientconfig/results.go | 187 +++ .../utils/openstack/clientconfig/utils.go | 190 +++ .../github.com/hashicorp/go-uuid/.travis.yml | 12 + vendor/github.com/hashicorp/go-uuid/LICENSE | 365 ++++++ vendor/github.com/hashicorp/go-uuid/README.md | 8 + vendor/github.com/hashicorp/go-uuid/uuid.go | 83 ++ vendor/modules.txt | 16 +- 82 files changed, 3999 insertions(+), 1984 deletions(-) delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/doc.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/requests.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/results.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/urls.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go delete mode 100644 vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go create mode 100644 vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint.go create mode 100644 vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/constants.go create mode 100644 vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/constants.go create mode 100644 vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/constants.go create mode 100644 vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/constants.go create mode 100644 vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/discovery.go create mode 100644 vendor/github.com/gophercloud/utils/LICENSE create mode 100644 vendor/github.com/gophercloud/utils/env/env.go create mode 100644 vendor/github.com/gophercloud/utils/env/env_windows.go create mode 100644 vendor/github.com/gophercloud/utils/gnocchi/client.go create mode 100644 vendor/github.com/gophercloud/utils/gnocchi/results.go create mode 100644 vendor/github.com/gophercloud/utils/internal/pkg.go create mode 100644 vendor/github.com/gophercloud/utils/internal/util.go create mode 100644 vendor/github.com/gophercloud/utils/openstack/clientconfig/doc.go create mode 100644 vendor/github.com/gophercloud/utils/openstack/clientconfig/requests.go create mode 100644 vendor/github.com/gophercloud/utils/openstack/clientconfig/results.go create mode 100644 vendor/github.com/gophercloud/utils/openstack/clientconfig/utils.go create mode 100644 vendor/github.com/hashicorp/go-uuid/.travis.yml create mode 100644 vendor/github.com/hashicorp/go-uuid/LICENSE create mode 100644 vendor/github.com/hashicorp/go-uuid/README.md create mode 100644 vendor/github.com/hashicorp/go-uuid/uuid.go diff --git a/go.mod b/go.mod index 7b877bca8ff..b53dfc298f8 100644 --- a/go.mod +++ b/go.mod @@ -127,7 +127,6 @@ require ( ) require ( - github.com/gophercloud/gophercloud v1.7.0 github.com/aws/aws-sdk-go-v2 v1.36.5 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect github.com/aws/aws-sdk-go-v2/config v1.29.17 @@ -152,10 +151,13 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 github.com/aws/smithy-go v1.22.4 + github.com/gophercloud/gophercloud v1.14.1 github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb gopkg.in/evanphx/json-patch.v4 v4.12.0 ) +require github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 + require ( 4d63.com/gochecknoglobals v0.2.2 // indirect cel.dev/expr v0.19.1 // indirect @@ -260,7 +262,7 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect - github.com/gophercloud/gophercloud/v2 v2.5.0 // indirect + github.com/gophercloud/gophercloud/v2 v2.8.0 // indirect github.com/gordonklaus/ineffassign v0.1.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.4.2 // indirect @@ -271,6 +273,7 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 4215c26d380..65c449bc1a8 100644 --- a/go.sum +++ b/go.sum @@ -512,10 +512,13 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gT github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= -github.com/gophercloud/gophercloud v1.7.0 h1:fyJGKh0LBvIZKLvBWvQdIgkaV5yTM3Jh9EYUh+UNCAs= -github.com/gophercloud/gophercloud v1.7.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= -github.com/gophercloud/gophercloud/v2 v2.5.0 h1:DubPfC43gsZiGZ9LT1IJflVMm+0rck0ejoPsH8D5rqk= -github.com/gophercloud/gophercloud/v2 v2.5.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= +github.com/gophercloud/gophercloud v1.3.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/gophercloud/gophercloud v1.14.1 h1:DTCNaTVGl8/cFu58O1JwWgis9gtISAFONqpMKNg/Vpw= +github.com/gophercloud/gophercloud v1.14.1/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/gophercloud/gophercloud/v2 v2.8.0 h1:of2+8tT6+FbEYHfYC8GBu8TXJNsXYSNm9KuvpX7Neqo= +github.com/gophercloud/gophercloud/v2 v2.8.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= +github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 h1:sH7xkTfYzxIEgzq1tDHIMKRh1vThOEOGNsettdEeLbE= +github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56/go.mod h1:VSalo4adEk+3sNkmVJLnhHoOyOYYS8sTWLG4mv5BKto= github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb h1:TQTXVYXL3d0zRAybRUKKboO0z/XAsXEfU6Oax8n00kc= github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb/go.mod h1:tIUw/gFHOB6lFV9LhzNZg5jfCLYMxI2lC1dZUa7NlHM= github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= @@ -1177,6 +1180,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= diff --git a/pkg/openstackclient/mock/client_generated.go b/pkg/openstackclient/mock/client_generated.go index 2d919d372b7..54988bce2c6 100644 --- a/pkg/openstackclient/mock/client_generated.go +++ b/pkg/openstackclient/mock/client_generated.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: ./pkg/openstackclient/client.go +// Source: ./client.go // Package mock is a generated GoMock package. package mock diff --git a/pkg/test/clusterdeployment/clusterdeployment.go b/pkg/test/clusterdeployment/clusterdeployment.go index 2f6c0954c0f..6dd2493508d 100644 --- a/pkg/test/clusterdeployment/clusterdeployment.go +++ b/pkg/test/clusterdeployment/clusterdeployment.go @@ -255,7 +255,14 @@ func WithAWSPlatformStatus(platformStatus *hivev1aws.PlatformStatus) Option { } } -// WithAWSPlatformStatus sets the specified aws platform on the supplied object. +// WithOpenStackPlatform sets the specified OpenStack platform on the cd. +func WithOpenStackPlatform(platform *hivev1openstack.Platform) Option { + return func(clusterDeployment *hivev1.ClusterDeployment) { + clusterDeployment.Spec.Platform.OpenStack = platform + } +} + +// WithGCPPlatformStatus sets the specified aws platform on the supplied object. func WithGCPPlatformStatus(platformStatus *hivev1gcp.PlatformStatus) Option { return func(clusterDeployment *hivev1.ClusterDeployment) { if clusterDeployment.Status.Platform == nil { diff --git a/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md b/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md index b470df398bc..b19b5e77532 100644 --- a/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md +++ b/vendor/github.com/gophercloud/gophercloud/CHANGELOG.md @@ -1,3 +1,76 @@ +## v1.14.1 (2024-09-18) + +* [GH-3162](https://github.com/gophercloud/gophercloud/pull/3162) Fix security group rule "any protocol" + +## v1.14.0 (2024-07-24) + +* [GH-3095](https://github.com/gophercloud/gophercloud/pull/3095) [neutron]: introduce Description argument for the portforwarding +* [GH-3098](https://github.com/gophercloud/gophercloud/pull/3098) [neutron]: introduce Stateful argument for the security groups +* [GH-3099](https://github.com/gophercloud/gophercloud/pull/3099) [networking]: subnet add field dns_publish_fixed_ip + +## v1.13.0 (2024-07-08) + +* [GH-3044](https://github.com/gophercloud/gophercloud/pull/3044) [v1] Add ci jobs for openstack caracal +* [GH-3073](https://github.com/gophercloud/gophercloud/pull/3073) [v1] Adding missing QoS field for router +* [GH-3080](https://github.com/gophercloud/gophercloud/pull/3080) [networking]: add BGP VPNs support (backport to 1.x) + +## v1.12.0 (2024-05-27) + +* [GH-2979](https://github.com/gophercloud/gophercloud/pull/2979) [v1] CI backports +* [GH-2985](https://github.com/gophercloud/gophercloud/pull/2985) [v1] baremetal: fix handling of the "fields" query argument +* [GH-2989](https://github.com/gophercloud/gophercloud/pull/2989) [v1] [CI] Fix portbiding tests +* [GH-2992](https://github.com/gophercloud/gophercloud/pull/2992) [v1] [CI] Fix portbiding tests +* [GH-2993](https://github.com/gophercloud/gophercloud/pull/2993) [v1] build(deps): bump EmilienM/devstack-action from 0.14 to 0.15 +* [GH-2998](https://github.com/gophercloud/gophercloud/pull/2998) [v1] testhelper: mark all helpers with t.Helper +* [GH-3043](https://github.com/gophercloud/gophercloud/pull/3043) [v1] CI: remove Zed from testing coverage + +## v1.11.0 (2024-03-07) + +This version reverts the inclusion of Context in the v1 branch. This inclusion +didn't add much value because no packages were using it; on the other hand, it +introduced a bug when using the Context property of the Provider client. + +## v1.10.0 (2024-02-27) **RETRACTED**: see https://github.com/gophercloud/gophercloud/issues/2969 + +* [GH-2893](https://github.com/gophercloud/gophercloud/pull/2893) [v1] authentication: Add WithContext functions +* [GH-2894](https://github.com/gophercloud/gophercloud/pull/2894) [v1] pager: Add WithContext functions +* [GH-2899](https://github.com/gophercloud/gophercloud/pull/2899) [v1] Authenticate with a clouds.yaml +* [GH-2917](https://github.com/gophercloud/gophercloud/pull/2917) [v1] Add ParseOption type to made clouds.Parse() more usable for optional With* funcs +* [GH-2924](https://github.com/gophercloud/gophercloud/pull/2924) [v1] build(deps): bump EmilienM/devstack-action from 0.11 to 0.14 +* [GH-2933](https://github.com/gophercloud/gophercloud/pull/2933) [v1] Fix AllowReauth reauthentication +* [GH-2950](https://github.com/gophercloud/gophercloud/pull/2950) [v1] compute: Use volumeID, not attachmentID for volume attachments + +## v1.9.0 (2024-02-02) **RETRACTED**: see https://github.com/gophercloud/gophercloud/issues/2969 + +New features and improvements: + +* [GH-2884](https://github.com/gophercloud/gophercloud/pull/2884) [v1] Context-aware methods to ProviderClient and ServiceClient +* [GH-2887](https://github.com/gophercloud/gophercloud/pull/2887) [v1] Add support of Flavors and FlavorProfiles for Octavia +* [GH-2875](https://github.com/gophercloud/gophercloud/pull/2875) [v1] [db/v1/instance]: adding support for availability_zone for a db instance + +CI changes: + +* [GH-2856](https://github.com/gophercloud/gophercloud/pull/2856) [v1] Fix devstack install on EOL magnum branches +* [GH-2857](https://github.com/gophercloud/gophercloud/pull/2857) [v1] Fix networking acceptance tests +* [GH-2858](https://github.com/gophercloud/gophercloud/pull/2858) [v1] build(deps): bump actions/upload-artifact from 3 to 4 +* [GH-2859](https://github.com/gophercloud/gophercloud/pull/2859) [v1] build(deps): bump github/codeql-action from 2 to 3 + +## v1.8.0 (2023-11-30) + +New features and improvements: + +* [GH-2800](https://github.com/gophercloud/gophercloud/pull/2800) [v1] Fix options initialization in ServiceClient.Request (fixes #2798) +* [GH-2823](https://github.com/gophercloud/gophercloud/pull/2823) [v1] Add more godoc to GuestFormat +* [GH-2826](https://github.com/gophercloud/gophercloud/pull/2826) Allow objects.CreateTempURL with names containing /v1/ + +CI changes: + +* [GH-2802](https://github.com/gophercloud/gophercloud/pull/2802) [v1] Add job for bobcat stable/2023.2 +* [GH-2819](https://github.com/gophercloud/gophercloud/pull/2819) [v1] Test files alongside code +* [GH-2814](https://github.com/gophercloud/gophercloud/pull/2814) Make fixtures part of tests +* [GH-2796](https://github.com/gophercloud/gophercloud/pull/2796) [v1] ci/unit: switch to coverallsapp/github-action +* [GH-2840](https://github.com/gophercloud/gophercloud/pull/2840) unit tests: Fix the installation of tools + ## v1.7.0 (2023-09-22) New features and improvements: diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go deleted file mode 100644 index 04d9887a143..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/doc.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -Package quotasets enables retrieving and managing Compute quotas. - -Example to Get a Quota Set - - quotaset, err := quotasets.Get(computeClient, "tenant-id").Extract() - if err != nil { - panic(err) - } - - fmt.Printf("%+v\n", quotaset) - -Example to Get a Detailed Quota Set - - quotaset, err := quotasets.GetDetail(computeClient, "tenant-id").Extract() - if err != nil { - panic(err) - } - - fmt.Printf("%+v\n", quotaset) - -Example to Update a Quota Set - - updateOpts := quotasets.UpdateOpts{ - FixedIPs: gophercloud.IntToPointer(100), - Cores: gophercloud.IntToPointer(64), - } - - quotaset, err := quotasets.Update(computeClient, "tenant-id", updateOpts).Extract() - if err != nil { - panic(err) - } - - fmt.Printf("%+v\n", quotaset) -*/ -package quotasets diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go deleted file mode 100644 index bb99a1085de..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/requests.go +++ /dev/null @@ -1,103 +0,0 @@ -package quotasets - -import ( - "github.com/gophercloud/gophercloud" -) - -// Get returns public data about a previously created QuotaSet. -func Get(client *gophercloud.ServiceClient, tenantID string) (r GetResult) { - resp, err := client.Get(getURL(client, tenantID), &r.Body, nil) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// GetDetail returns detailed public data about a previously created QuotaSet. -func GetDetail(client *gophercloud.ServiceClient, tenantID string) (r GetDetailResult) { - resp, err := client.Get(getDetailURL(client, tenantID), &r.Body, nil) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// Updates the quotas for the given tenantID and returns the new QuotaSet. -func Update(client *gophercloud.ServiceClient, tenantID string, opts UpdateOptsBuilder) (r UpdateResult) { - reqBody, err := opts.ToComputeQuotaUpdateMap() - if err != nil { - r.Err = err - return - } - - resp, err := client.Put(updateURL(client, tenantID), reqBody, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{200}}) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// Resets the quotas for the given tenant to their default values. -func Delete(client *gophercloud.ServiceClient, tenantID string) (r DeleteResult) { - resp, err := client.Delete(deleteURL(client, tenantID), nil) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// Options for Updating the quotas of a Tenant. -// All int-values are pointers so they can be nil if they are not needed. -// You can use gopercloud.IntToPointer() for convenience -type UpdateOpts struct { - // FixedIPs is number of fixed ips allotted this quota_set. - FixedIPs *int `json:"fixed_ips,omitempty"` - - // FloatingIPs is number of floating ips allotted this quota_set. - FloatingIPs *int `json:"floating_ips,omitempty"` - - // InjectedFileContentBytes is content bytes allowed for each injected file. - InjectedFileContentBytes *int `json:"injected_file_content_bytes,omitempty"` - - // InjectedFilePathBytes is allowed bytes for each injected file path. - InjectedFilePathBytes *int `json:"injected_file_path_bytes,omitempty"` - - // InjectedFiles is injected files allowed for each project. - InjectedFiles *int `json:"injected_files,omitempty"` - - // KeyPairs is number of ssh keypairs. - KeyPairs *int `json:"key_pairs,omitempty"` - - // MetadataItems is number of metadata items allowed for each instance. - MetadataItems *int `json:"metadata_items,omitempty"` - - // RAM is megabytes allowed for each instance. - RAM *int `json:"ram,omitempty"` - - // SecurityGroupRules is rules allowed for each security group. - SecurityGroupRules *int `json:"security_group_rules,omitempty"` - - // SecurityGroups security groups allowed for each project. - SecurityGroups *int `json:"security_groups,omitempty"` - - // Cores is number of instance cores allowed for each project. - Cores *int `json:"cores,omitempty"` - - // Instances is number of instances allowed for each project. - Instances *int `json:"instances,omitempty"` - - // Number of ServerGroups allowed for the project. - ServerGroups *int `json:"server_groups,omitempty"` - - // Max number of Members for each ServerGroup. - ServerGroupMembers *int `json:"server_group_members,omitempty"` - - // Force will update the quotaset even if the quota has already been used - // and the reserved quota exceeds the new quota. - Force bool `json:"force,omitempty"` -} - -// UpdateOptsBuilder enables extensins to add parameters to the update request. -type UpdateOptsBuilder interface { - // Extra specific name to prevent collisions with interfaces for other quotas - // (e.g. neutron) - ToComputeQuotaUpdateMap() (map[string]interface{}, error) -} - -// ToComputeQuotaUpdateMap builds the update options into a serializable -// format. -func (opts UpdateOpts) ToComputeQuotaUpdateMap() (map[string]interface{}, error) { - return gophercloud.BuildRequestBody(opts, "quota_set") -} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go deleted file mode 100644 index 07fb49c1274..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/results.go +++ /dev/null @@ -1,198 +0,0 @@ -package quotasets - -import ( - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/pagination" -) - -// QuotaSet is a set of operational limits that allow for control of compute -// usage. -type QuotaSet struct { - // ID is tenant associated with this QuotaSet. - ID string `json:"id"` - - // FixedIPs is number of fixed ips allotted this QuotaSet. - FixedIPs int `json:"fixed_ips"` - - // FloatingIPs is number of floating ips allotted this QuotaSet. - FloatingIPs int `json:"floating_ips"` - - // InjectedFileContentBytes is the allowed bytes for each injected file. - InjectedFileContentBytes int `json:"injected_file_content_bytes"` - - // InjectedFilePathBytes is allowed bytes for each injected file path. - InjectedFilePathBytes int `json:"injected_file_path_bytes"` - - // InjectedFiles is the number of injected files allowed for each project. - InjectedFiles int `json:"injected_files"` - - // KeyPairs is number of ssh keypairs. - KeyPairs int `json:"key_pairs"` - - // MetadataItems is number of metadata items allowed for each instance. - MetadataItems int `json:"metadata_items"` - - // RAM is megabytes allowed for each instance. - RAM int `json:"ram"` - - // SecurityGroupRules is number of security group rules allowed for each - // security group. - SecurityGroupRules int `json:"security_group_rules"` - - // SecurityGroups is the number of security groups allowed for each project. - SecurityGroups int `json:"security_groups"` - - // Cores is number of instance cores allowed for each project. - Cores int `json:"cores"` - - // Instances is number of instances allowed for each project. - Instances int `json:"instances"` - - // ServerGroups is the number of ServerGroups allowed for the project. - ServerGroups int `json:"server_groups"` - - // ServerGroupMembers is the number of members for each ServerGroup. - ServerGroupMembers int `json:"server_group_members"` -} - -// QuotaDetailSet represents details of both operational limits of compute -// resources and the current usage of those resources. -type QuotaDetailSet struct { - // ID is the tenant ID associated with this QuotaDetailSet. - ID string `json:"id"` - - // FixedIPs is number of fixed ips allotted this QuotaDetailSet. - FixedIPs QuotaDetail `json:"fixed_ips"` - - // FloatingIPs is number of floating ips allotted this QuotaDetailSet. - FloatingIPs QuotaDetail `json:"floating_ips"` - - // InjectedFileContentBytes is the allowed bytes for each injected file. - InjectedFileContentBytes QuotaDetail `json:"injected_file_content_bytes"` - - // InjectedFilePathBytes is allowed bytes for each injected file path. - InjectedFilePathBytes QuotaDetail `json:"injected_file_path_bytes"` - - // InjectedFiles is the number of injected files allowed for each project. - InjectedFiles QuotaDetail `json:"injected_files"` - - // KeyPairs is number of ssh keypairs. - KeyPairs QuotaDetail `json:"key_pairs"` - - // MetadataItems is number of metadata items allowed for each instance. - MetadataItems QuotaDetail `json:"metadata_items"` - - // RAM is megabytes allowed for each instance. - RAM QuotaDetail `json:"ram"` - - // SecurityGroupRules is number of security group rules allowed for each - // security group. - SecurityGroupRules QuotaDetail `json:"security_group_rules"` - - // SecurityGroups is the number of security groups allowed for each project. - SecurityGroups QuotaDetail `json:"security_groups"` - - // Cores is number of instance cores allowed for each project. - Cores QuotaDetail `json:"cores"` - - // Instances is number of instances allowed for each project. - Instances QuotaDetail `json:"instances"` - - // ServerGroups is the number of ServerGroups allowed for the project. - ServerGroups QuotaDetail `json:"server_groups"` - - // ServerGroupMembers is the number of members for each ServerGroup. - ServerGroupMembers QuotaDetail `json:"server_group_members"` -} - -// QuotaDetail is a set of details about a single operational limit that allows -// for control of compute usage. -type QuotaDetail struct { - // InUse is the current number of provisioned/allocated resources of the - // given type. - InUse int `json:"in_use"` - - // Reserved is a transitional state when a claim against quota has been made - // but the resource is not yet fully online. - Reserved int `json:"reserved"` - - // Limit is the maximum number of a given resource that can be - // allocated/provisioned. This is what "quota" usually refers to. - Limit int `json:"limit"` -} - -// QuotaSetPage stores a single page of all QuotaSet results from a List call. -type QuotaSetPage struct { - pagination.SinglePageBase -} - -// IsEmpty determines whether or not a QuotaSetsetPage is empty. -func (page QuotaSetPage) IsEmpty() (bool, error) { - if page.StatusCode == 204 { - return true, nil - } - - ks, err := ExtractQuotaSets(page) - return len(ks) == 0, err -} - -// ExtractQuotaSets interprets a page of results as a slice of QuotaSets. -func ExtractQuotaSets(r pagination.Page) ([]QuotaSet, error) { - var s struct { - QuotaSets []QuotaSet `json:"quotas"` - } - err := (r.(QuotaSetPage)).ExtractInto(&s) - return s.QuotaSets, err -} - -type quotaResult struct { - gophercloud.Result -} - -// Extract is a method that attempts to interpret any QuotaSet resource response -// as a QuotaSet struct. -func (r quotaResult) Extract() (*QuotaSet, error) { - var s struct { - QuotaSet *QuotaSet `json:"quota_set"` - } - err := r.ExtractInto(&s) - return s.QuotaSet, err -} - -// GetResult is the response from a Get operation. Call its Extract method to -// interpret it as a QuotaSet. -type GetResult struct { - quotaResult -} - -// UpdateResult is the response from a Update operation. Call its Extract method -// to interpret it as a QuotaSet. -type UpdateResult struct { - quotaResult -} - -// DeleteResult is the response from a Delete operation. Call its Extract method -// to interpret it as a QuotaSet. -type DeleteResult struct { - quotaResult -} - -type quotaDetailResult struct { - gophercloud.Result -} - -// GetDetailResult is the response from a Get operation. Call its Extract -// method to interpret it as a QuotaSet. -type GetDetailResult struct { - quotaDetailResult -} - -// Extract is a method that attempts to interpret any QuotaDetailSet -// resource response as a set of QuotaDetailSet structs. -func (r quotaDetailResult) Extract() (QuotaDetailSet, error) { - var s struct { - QuotaData QuotaDetailSet `json:"quota_set"` - } - err := r.ExtractInto(&s) - return s.QuotaData, err -} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go deleted file mode 100644 index 37e50215b51..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets/urls.go +++ /dev/null @@ -1,25 +0,0 @@ -package quotasets - -import "github.com/gophercloud/gophercloud" - -const resourcePath = "os-quota-sets" - -func resourceURL(c *gophercloud.ServiceClient) string { - return c.ServiceURL(resourcePath) -} - -func getURL(c *gophercloud.ServiceClient, tenantID string) string { - return c.ServiceURL(resourcePath, tenantID) -} - -func getDetailURL(c *gophercloud.ServiceClient, tenantID string) string { - return c.ServiceURL(resourcePath, tenantID, "detail") -} - -func updateURL(c *gophercloud.ServiceClient, tenantID string) string { - return getURL(c, tenantID) -} - -func deleteURL(c *gophercloud.ServiceClient, tenantID string) string { - return getURL(c, tenantID) -} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/doc.go deleted file mode 100644 index df9d79e0b97..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/doc.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Package usage provides information and interaction with the -SimpleTenantUsage extension for the OpenStack Compute service. - -Due to the way the API responses are formatted, it is not recommended to -query by using the AllPages convenience method. Instead, use the EachPage -method to view each result page-by-page. - -This is because the usage calculations are done _per page_ and not as -an aggregated total of the entire usage set. - -Example to Retrieve Usage for a Single Tenant: - - start := time.Date(2017, 01, 21, 10, 4, 20, 0, time.UTC) - end := time.Date(2017, 01, 21, 10, 4, 20, 0, time.UTC) - - singleTenantOpts := usage.SingleTenantOpts{ - Start: &start, - End: &end, - } - - err := usage.SingleTenant(computeClient, tenantID, singleTenantOpts).EachPage(func(page pagination.Page) (bool, error) { - tenantUsage, err := usage.ExtractSingleTenant(page) - if err != nil { - return false, err - } - - fmt.Printf("%+v\n", tenantUsage) - - return true, nil - }) - - if err != nil { - panic(err) - } - -Example to Retrieve Usage for All Tenants: - - allTenantsOpts := usage.AllTenantsOpts{ - Detailed: true, - } - - err := usage.AllTenants(computeClient, allTenantsOpts).EachPage(func(page pagination.Page) (bool, error) { - allTenantsUsage, err := usage.ExtractAllTenants(page) - if err != nil { - return false, err - } - - fmt.Printf("%+v\n", allTenantsUsage) - - return true, nil - }) - - if err != nil { - panic(err) - } -*/ -package usage diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/requests.go deleted file mode 100644 index eb36f59b7e9..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/requests.go +++ /dev/null @@ -1,134 +0,0 @@ -package usage - -import ( - "net/url" - "time" - - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/pagination" -) - -// SingleTenantOpts are options for fetching usage of a single tenant. -type SingleTenantOpts struct { - // The ending time to calculate usage statistics on compute and storage resources. - End *time.Time `q:"end"` - - // The beginning time to calculate usage statistics on compute and storage resources. - Start *time.Time `q:"start"` - - // Limit limits the amount of results returned by the API. - // This requires the client to be set to microversion 2.40 or later. - Limit int `q:"limit"` - - // Marker instructs the API call where to start listing from. - // This requires the client to be set to microversion 2.40 or later. - Marker string `q:"marker"` -} - -// SingleTenantOptsBuilder allows extensions to add additional parameters to the -// SingleTenant request. -type SingleTenantOptsBuilder interface { - ToUsageSingleTenantQuery() (string, error) -} - -// ToUsageSingleTenantQuery formats a SingleTenantOpts into a query string. -func (opts SingleTenantOpts) ToUsageSingleTenantQuery() (string, error) { - q, err := gophercloud.BuildQueryString(opts) - if err != nil { - return "", err - } - - params := q.Query() - - if opts.Start != nil { - params.Add("start", opts.Start.Format(gophercloud.RFC3339MilliNoZ)) - } - - if opts.End != nil { - params.Add("end", opts.End.Format(gophercloud.RFC3339MilliNoZ)) - } - - q = &url.URL{RawQuery: params.Encode()} - return q.String(), nil -} - -// SingleTenant returns usage data about a single tenant. -func SingleTenant(client *gophercloud.ServiceClient, tenantID string, opts SingleTenantOptsBuilder) pagination.Pager { - url := getTenantURL(client, tenantID) - if opts != nil { - query, err := opts.ToUsageSingleTenantQuery() - if err != nil { - return pagination.Pager{Err: err} - } - url += query - } - return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { - return SingleTenantPage{pagination.LinkedPageBase{PageResult: r}} - }) -} - -// AllTenantsOpts are options for fetching usage of all tenants. -type AllTenantsOpts struct { - // Detailed will return detailed results. - Detailed bool - - // The ending time to calculate usage statistics on compute and storage resources. - End *time.Time `q:"end"` - - // The beginning time to calculate usage statistics on compute and storage resources. - Start *time.Time `q:"start"` - - // Limit limits the amount of results returned by the API. - // This requires the client to be set to microversion 2.40 or later. - Limit int `q:"limit"` - - // Marker instructs the API call where to start listing from. - // This requires the client to be set to microversion 2.40 or later. - Marker string `q:"marker"` -} - -// AllTenantsOptsBuilder allows extensions to add additional parameters to the -// AllTenants request. -type AllTenantsOptsBuilder interface { - ToUsageAllTenantsQuery() (string, error) -} - -// ToUsageAllTenantsQuery formats a AllTenantsOpts into a query string. -func (opts AllTenantsOpts) ToUsageAllTenantsQuery() (string, error) { - q, err := gophercloud.BuildQueryString(opts) - if err != nil { - return "", err - } - - params := q.Query() - - if opts.Start != nil { - params.Add("start", opts.Start.Format(gophercloud.RFC3339MilliNoZ)) - } - - if opts.End != nil { - params.Add("end", opts.End.Format(gophercloud.RFC3339MilliNoZ)) - } - - if opts.Detailed == true { - params.Add("detailed", "1") - } - - q = &url.URL{RawQuery: params.Encode()} - return q.String(), nil -} - -// AllTenants returns usage data about all tenants. -func AllTenants(client *gophercloud.ServiceClient, opts AllTenantsOptsBuilder) pagination.Pager { - url := allTenantsURL(client) - if opts != nil { - query, err := opts.ToUsageAllTenantsQuery() - if err != nil { - return pagination.Pager{Err: err} - } - url += query - } - return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { - return AllTenantsPage{pagination.LinkedPageBase{PageResult: r}} - }) -} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/results.go deleted file mode 100644 index 8c36dde8f21..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/results.go +++ /dev/null @@ -1,191 +0,0 @@ -package usage - -import ( - "encoding/json" - "time" - - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/pagination" -) - -// TenantUsage is a set of usage information about a tenant over the sampling window -type TenantUsage struct { - // ServerUsages is an array of ServerUsage maps - ServerUsages []ServerUsage `json:"server_usages"` - - // Start is the beginning time to calculate usage statistics on compute and storage resources - Start time.Time `json:"-"` - - // Stop is the ending time to calculate usage statistics on compute and storage resources - Stop time.Time `json:"-"` - - // TenantID is the ID of the tenant whose usage is being reported on - TenantID string `json:"tenant_id"` - - // TotalHours is the total duration that servers exist (in hours) - TotalHours float64 `json:"total_hours"` - - // TotalLocalGBUsage multiplies the server disk size (in GiB) by hours the server exists, and then adding that all together for each server - TotalLocalGBUsage float64 `json:"total_local_gb_usage"` - - // TotalMemoryMBUsage multiplies the server memory size (in MB) by hours the server exists, and then adding that all together for each server - TotalMemoryMBUsage float64 `json:"total_memory_mb_usage"` - - // TotalVCPUsUsage multiplies the number of virtual CPUs of the server by hours the server exists, and then adding that all together for each server - TotalVCPUsUsage float64 `json:"total_vcpus_usage"` -} - -// UnmarshalJSON sets *u to a copy of data. -func (u *TenantUsage) UnmarshalJSON(b []byte) error { - type tmp TenantUsage - var s struct { - tmp - Start gophercloud.JSONRFC3339MilliNoZ `json:"start"` - Stop gophercloud.JSONRFC3339MilliNoZ `json:"stop"` - } - - if err := json.Unmarshal(b, &s); err != nil { - return err - } - *u = TenantUsage(s.tmp) - - u.Start = time.Time(s.Start) - u.Stop = time.Time(s.Stop) - - return nil -} - -// ServerUsage is a detailed set of information about a specific instance inside a tenant -type ServerUsage struct { - // EndedAt is the date and time when the server was deleted - EndedAt time.Time `json:"-"` - - // Flavor is the display name of a flavor - Flavor string `json:"flavor"` - - // Hours is the duration that the server exists in hours - Hours float64 `json:"hours"` - - // InstanceID is the UUID of the instance - InstanceID string `json:"instance_id"` - - // LocalGB is the sum of the root disk size of the server and the ephemeral disk size of it (in GiB) - LocalGB int `json:"local_gb"` - - // MemoryMB is the memory size of the server (in MB) - MemoryMB int `json:"memory_mb"` - - // Name is the name assigned to the server when it was created - Name string `json:"name"` - - // StartedAt is the date and time when the server was started - StartedAt time.Time `json:"-"` - - // State is the VM power state - State string `json:"state"` - - // TenantID is the UUID of the tenant in a multi-tenancy cloud - TenantID string `json:"tenant_id"` - - // Uptime is the uptime of the server in seconds - Uptime int `json:"uptime"` - - // VCPUs is the number of virtual CPUs that the server uses - VCPUs int `json:"vcpus"` -} - -// UnmarshalJSON sets *u to a copy of data. -func (u *ServerUsage) UnmarshalJSON(b []byte) error { - type tmp ServerUsage - var s struct { - tmp - EndedAt gophercloud.JSONRFC3339MilliNoZ `json:"ended_at"` - StartedAt gophercloud.JSONRFC3339MilliNoZ `json:"started_at"` - } - - if err := json.Unmarshal(b, &s); err != nil { - return err - } - *u = ServerUsage(s.tmp) - - u.EndedAt = time.Time(s.EndedAt) - u.StartedAt = time.Time(s.StartedAt) - - return nil -} - -// SingleTenantPage stores a single, only page of TenantUsage results from a -// SingleTenant call. -type SingleTenantPage struct { - pagination.LinkedPageBase -} - -// IsEmpty determines whether or not a SingleTenantPage is empty. -func (r SingleTenantPage) IsEmpty() (bool, error) { - if r.StatusCode == 204 { - return true, nil - } - - ks, err := ExtractSingleTenant(r) - return ks == nil, err -} - -// NextPageURL uses the response's embedded link reference to navigate to the -// next page of results. -func (r SingleTenantPage) NextPageURL() (string, error) { - var s struct { - Links []gophercloud.Link `json:"tenant_usage_links"` - } - err := r.ExtractInto(&s) - if err != nil { - return "", err - } - return gophercloud.ExtractNextURL(s.Links) -} - -// ExtractSingleTenant interprets a SingleTenantPage as a TenantUsage result. -func ExtractSingleTenant(page pagination.Page) (*TenantUsage, error) { - var s struct { - TenantUsage *TenantUsage `json:"tenant_usage"` - } - err := (page.(SingleTenantPage)).ExtractInto(&s) - return s.TenantUsage, err -} - -// AllTenantsPage stores a single, only page of TenantUsage results from a -// AllTenants call. -type AllTenantsPage struct { - pagination.LinkedPageBase -} - -// ExtractAllTenants interprets a AllTenantsPage as a TenantUsage result. -func ExtractAllTenants(page pagination.Page) ([]TenantUsage, error) { - var s struct { - TenantUsages []TenantUsage `json:"tenant_usages"` - } - err := (page.(AllTenantsPage)).ExtractInto(&s) - return s.TenantUsages, err -} - -// IsEmpty determines whether or not an AllTenantsPage is empty. -func (r AllTenantsPage) IsEmpty() (bool, error) { - if r.StatusCode == 204 { - return true, nil - } - - usages, err := ExtractAllTenants(r) - return len(usages) == 0, err -} - -// NextPageURL uses the response's embedded link reference to navigate to the -// next page of results. -func (r AllTenantsPage) NextPageURL() (string, error) { - var s struct { - Links []gophercloud.Link `json:"tenant_usages_links"` - } - err := r.ExtractInto(&s) - if err != nil { - return "", err - } - return gophercloud.ExtractNextURL(s.Links) -} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/urls.go deleted file mode 100644 index 50636107010..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage/urls.go +++ /dev/null @@ -1,13 +0,0 @@ -package usage - -import "github.com/gophercloud/gophercloud" - -const resourcePath = "os-simple-tenant-usage" - -func allTenantsURL(client *gophercloud.ServiceClient) string { - return client.ServiceURL(resourcePath) -} - -func getTenantURL(client *gophercloud.ServiceClient, tenantID string) string { - return client.ServiceURL(resourcePath, tenantID) -} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go deleted file mode 100644 index 747966d8d99..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/doc.go +++ /dev/null @@ -1,150 +0,0 @@ -/* -Package flavors provides information and interaction with the flavor API -in the OpenStack Compute service. - -A flavor is an available hardware configuration for a server. Each flavor -has a unique combination of disk space, memory capacity and priority for CPU -time. - -Example to List Flavors - - listOpts := flavors.ListOpts{ - AccessType: flavors.PublicAccess, - } - - allPages, err := flavors.ListDetail(computeClient, listOpts).AllPages() - if err != nil { - panic(err) - } - - allFlavors, err := flavors.ExtractFlavors(allPages) - if err != nil { - panic(err) - } - - for _, flavor := range allFlavors { - fmt.Printf("%+v\n", flavor) - } - -Example to Create a Flavor - - createOpts := flavors.CreateOpts{ - ID: "1", - Name: "m1.tiny", - Disk: gophercloud.IntToPointer(1), - RAM: 512, - VCPUs: 1, - RxTxFactor: 1.0, - } - - flavor, err := flavors.Create(computeClient, createOpts).Extract() - if err != nil { - panic(err) - } - -Example to Update a Flavor - - flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" - - updateOpts := flavors.UpdateOpts{ - Description: "This is a good description" - } - - flavor, err := flavors.Update(computeClient, flavorID, updateOpts).Extract() - if err != nil { - panic(err) - } - -Example to List Flavor Access - - flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" - - allPages, err := flavors.ListAccesses(computeClient, flavorID).AllPages() - if err != nil { - panic(err) - } - - allAccesses, err := flavors.ExtractAccesses(allPages) - if err != nil { - panic(err) - } - - for _, access := range allAccesses { - fmt.Printf("%+v", access) - } - -Example to Grant Access to a Flavor - - flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" - - accessOpts := flavors.AddAccessOpts{ - Tenant: "15153a0979884b59b0592248ef947921", - } - - accessList, err := flavors.AddAccess(computeClient, flavor.ID, accessOpts).Extract() - if err != nil { - panic(err) - } - -Example to Remove/Revoke Access to a Flavor - - flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" - - accessOpts := flavors.RemoveAccessOpts{ - Tenant: "15153a0979884b59b0592248ef947921", - } - - accessList, err := flavors.RemoveAccess(computeClient, flavor.ID, accessOpts).Extract() - if err != nil { - panic(err) - } - -Example to Create Extra Specs for a Flavor - - flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" - - createOpts := flavors.ExtraSpecsOpts{ - "hw:cpu_policy": "CPU-POLICY", - "hw:cpu_thread_policy": "CPU-THREAD-POLICY", - } - createdExtraSpecs, err := flavors.CreateExtraSpecs(computeClient, flavorID, createOpts).Extract() - if err != nil { - panic(err) - } - - fmt.Printf("%+v", createdExtraSpecs) - -Example to Get Extra Specs for a Flavor - - flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" - - extraSpecs, err := flavors.ListExtraSpecs(computeClient, flavorID).Extract() - if err != nil { - panic(err) - } - - fmt.Printf("%+v", extraSpecs) - -Example to Update Extra Specs for a Flavor - - flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" - - updateOpts := flavors.ExtraSpecsOpts{ - "hw:cpu_thread_policy": "CPU-THREAD-POLICY-UPDATED", - } - updatedExtraSpec, err := flavors.UpdateExtraSpec(computeClient, flavorID, updateOpts).Extract() - if err != nil { - panic(err) - } - - fmt.Printf("%+v", updatedExtraSpec) - -Example to Delete an Extra Spec for a Flavor - - flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" - err := flavors.DeleteExtraSpec(computeClient, flavorID, "hw:cpu_thread_policy").ExtractErr() - if err != nil { - panic(err) - } -*/ -package flavors diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go deleted file mode 100644 index 3887cdfdca8..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/requests.go +++ /dev/null @@ -1,364 +0,0 @@ -package flavors - -import ( - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/pagination" -) - -// ListOptsBuilder allows extensions to add additional parameters to the -// List request. -type ListOptsBuilder interface { - ToFlavorListQuery() (string, error) -} - -/* -AccessType maps to OpenStack's Flavor.is_public field. Although the is_public -field is boolean, the request options are ternary, which is why AccessType is -a string. The following values are allowed: - -The AccessType arguement is optional, and if it is not supplied, OpenStack -returns the PublicAccess flavors. -*/ -type AccessType string - -const ( - // PublicAccess returns public flavors and private flavors associated with - // that project. - PublicAccess AccessType = "true" - - // PrivateAccess (admin only) returns private flavors, across all projects. - PrivateAccess AccessType = "false" - - // AllAccess (admin only) returns public and private flavors across all - // projects. - AllAccess AccessType = "None" -) - -/* -ListOpts filters the results returned by the List() function. -For example, a flavor with a minDisk field of 10 will not be returned if you -specify MinDisk set to 20. - -Typically, software will use the last ID of the previous call to List to set -the Marker for the current call. -*/ -type ListOpts struct { - // ChangesSince, if provided, instructs List to return only those things which - // have changed since the timestamp provided. - ChangesSince string `q:"changes-since"` - - // MinDisk and MinRAM, if provided, elides flavors which do not meet your - // criteria. - MinDisk int `q:"minDisk"` - MinRAM int `q:"minRam"` - - // SortDir allows to select sort direction. - // It can be "asc" or "desc" (default). - SortDir string `q:"sort_dir"` - - // SortKey allows to sort by one of the flavors attributes. - // Default is flavorid. - SortKey string `q:"sort_key"` - - // Marker and Limit control paging. - // Marker instructs List where to start listing from. - Marker string `q:"marker"` - - // Limit instructs List to refrain from sending excessively large lists of - // flavors. - Limit int `q:"limit"` - - // AccessType, if provided, instructs List which set of flavors to return. - // If IsPublic not provided, flavors for the current project are returned. - AccessType AccessType `q:"is_public"` -} - -// ToFlavorListQuery formats a ListOpts into a query string. -func (opts ListOpts) ToFlavorListQuery() (string, error) { - q, err := gophercloud.BuildQueryString(opts) - return q.String(), err -} - -// ListDetail instructs OpenStack to provide a list of flavors. -// You may provide criteria by which List curtails its results for easier -// processing. -func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { - url := listURL(client) - if opts != nil { - query, err := opts.ToFlavorListQuery() - if err != nil { - return pagination.Pager{Err: err} - } - url += query - } - return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { - return FlavorPage{pagination.LinkedPageBase{PageResult: r}} - }) -} - -type CreateOptsBuilder interface { - ToFlavorCreateMap() (map[string]interface{}, error) -} - -// CreateOpts specifies parameters used for creating a flavor. -type CreateOpts struct { - // Name is the name of the flavor. - Name string `json:"name" required:"true"` - - // RAM is the memory of the flavor, measured in MB. - RAM int `json:"ram" required:"true"` - - // VCPUs is the number of vcpus for the flavor. - VCPUs int `json:"vcpus" required:"true"` - - // Disk the amount of root disk space, measured in GB. - Disk *int `json:"disk" required:"true"` - - // ID is a unique ID for the flavor. - ID string `json:"id,omitempty"` - - // Swap is the amount of swap space for the flavor, measured in MB. - Swap *int `json:"swap,omitempty"` - - // RxTxFactor alters the network bandwidth of a flavor. - RxTxFactor float64 `json:"rxtx_factor,omitempty"` - - // IsPublic flags a flavor as being available to all projects or not. - IsPublic *bool `json:"os-flavor-access:is_public,omitempty"` - - // Ephemeral is the amount of ephemeral disk space, measured in GB. - Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"` - - // Description is a free form description of the flavor. Limited to - // 65535 characters in length. Only printable characters are allowed. - // New in version 2.55 - Description string `json:"description,omitempty"` -} - -// ToFlavorCreateMap constructs a request body from CreateOpts. -func (opts CreateOpts) ToFlavorCreateMap() (map[string]interface{}, error) { - return gophercloud.BuildRequestBody(opts, "flavor") -} - -// Create requests the creation of a new flavor. -func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { - b, err := opts.ToFlavorCreateMap() - if err != nil { - r.Err = err - return - } - resp, err := client.Post(createURL(client), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200, 201}, - }) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -type UpdateOptsBuilder interface { - ToFlavorUpdateMap() (map[string]interface{}, error) -} - -// UpdateOpts specifies parameters used for updating a flavor. -type UpdateOpts struct { - // Description is a free form description of the flavor. Limited to - // 65535 characters in length. Only printable characters are allowed. - // New in version 2.55 - Description string `json:"description,omitempty"` -} - -// ToFlavorUpdateMap constructs a request body from UpdateOpts. -func (opts UpdateOpts) ToFlavorUpdateMap() (map[string]interface{}, error) { - return gophercloud.BuildRequestBody(opts, "flavor") -} - -// Update requests the update of a new flavor. -func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { - b, err := opts.ToFlavorUpdateMap() - if err != nil { - r.Err = err - return - } - resp, err := client.Put(updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, - }) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// Get retrieves details of a single flavor. Use Extract to convert its -// result into a Flavor. -func Get(client *gophercloud.ServiceClient, id string) (r GetResult) { - resp, err := client.Get(getURL(client, id), &r.Body, nil) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// Delete deletes the specified flavor ID. -func Delete(client *gophercloud.ServiceClient, id string) (r DeleteResult) { - resp, err := client.Delete(deleteURL(client, id), nil) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// ListAccesses retrieves the tenants which have access to a flavor. -func ListAccesses(client *gophercloud.ServiceClient, id string) pagination.Pager { - url := accessURL(client, id) - - return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { - return AccessPage{pagination.SinglePageBase(r)} - }) -} - -// AddAccessOptsBuilder allows extensions to add additional parameters to the -// AddAccess requests. -type AddAccessOptsBuilder interface { - ToFlavorAddAccessMap() (map[string]interface{}, error) -} - -// AddAccessOpts represents options for adding access to a flavor. -type AddAccessOpts struct { - // Tenant is the project/tenant ID to grant access. - Tenant string `json:"tenant"` -} - -// ToFlavorAddAccessMap constructs a request body from AddAccessOpts. -func (opts AddAccessOpts) ToFlavorAddAccessMap() (map[string]interface{}, error) { - return gophercloud.BuildRequestBody(opts, "addTenantAccess") -} - -// AddAccess grants a tenant/project access to a flavor. -func AddAccess(client *gophercloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) { - b, err := opts.ToFlavorAddAccessMap() - if err != nil { - r.Err = err - return - } - resp, err := client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, - }) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// RemoveAccessOptsBuilder allows extensions to add additional parameters to the -// RemoveAccess requests. -type RemoveAccessOptsBuilder interface { - ToFlavorRemoveAccessMap() (map[string]interface{}, error) -} - -// RemoveAccessOpts represents options for removing access to a flavor. -type RemoveAccessOpts struct { - // Tenant is the project/tenant ID to grant access. - Tenant string `json:"tenant"` -} - -// ToFlavorRemoveAccessMap constructs a request body from RemoveAccessOpts. -func (opts RemoveAccessOpts) ToFlavorRemoveAccessMap() (map[string]interface{}, error) { - return gophercloud.BuildRequestBody(opts, "removeTenantAccess") -} - -// RemoveAccess removes/revokes a tenant/project access to a flavor. -func RemoveAccess(client *gophercloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) { - b, err := opts.ToFlavorRemoveAccessMap() - if err != nil { - r.Err = err - return - } - resp, err := client.Post(accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, - }) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// ExtraSpecs requests all the extra-specs for the given flavor ID. -func ListExtraSpecs(client *gophercloud.ServiceClient, flavorID string) (r ListExtraSpecsResult) { - resp, err := client.Get(extraSpecsListURL(client, flavorID), &r.Body, nil) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -func GetExtraSpec(client *gophercloud.ServiceClient, flavorID string, key string) (r GetExtraSpecResult) { - resp, err := client.Get(extraSpecsGetURL(client, flavorID, key), &r.Body, nil) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// CreateExtraSpecsOptsBuilder allows extensions to add additional parameters to the -// CreateExtraSpecs requests. -type CreateExtraSpecsOptsBuilder interface { - ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) -} - -// ExtraSpecsOpts is a map that contains key-value pairs. -type ExtraSpecsOpts map[string]string - -// ToFlavorExtraSpecsCreateMap assembles a body for a Create request based on -// the contents of ExtraSpecsOpts. -func (opts ExtraSpecsOpts) ToFlavorExtraSpecsCreateMap() (map[string]interface{}, error) { - return map[string]interface{}{"extra_specs": opts}, nil -} - -// CreateExtraSpecs will create or update the extra-specs key-value pairs for -// the specified Flavor. -func CreateExtraSpecs(client *gophercloud.ServiceClient, flavorID string, opts CreateExtraSpecsOptsBuilder) (r CreateExtraSpecsResult) { - b, err := opts.ToFlavorExtraSpecsCreateMap() - if err != nil { - r.Err = err - return - } - resp, err := client.Post(extraSpecsCreateURL(client, flavorID), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, - }) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to -// the Update request. -type UpdateExtraSpecOptsBuilder interface { - ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) -} - -// ToFlavorExtraSpecUpdateMap assembles a body for an Update request based on -// the contents of a ExtraSpecOpts. -func (opts ExtraSpecsOpts) ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) { - if len(opts) != 1 { - err := gophercloud.ErrInvalidInput{} - err.Argument = "flavors.ExtraSpecOpts" - err.Info = "Must have 1 and only one key-value pair" - return nil, "", err - } - - var key string - for k := range opts { - key = k - } - - return opts, key, nil -} - -// UpdateExtraSpec will updates the value of the specified flavor's extra spec -// for the key in opts. -func UpdateExtraSpec(client *gophercloud.ServiceClient, flavorID string, opts UpdateExtraSpecOptsBuilder) (r UpdateExtraSpecResult) { - b, key, err := opts.ToFlavorExtraSpecUpdateMap() - if err != nil { - r.Err = err - return - } - resp, err := client.Put(extraSpecUpdateURL(client, flavorID, key), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, - }) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} - -// DeleteExtraSpec will delete the key-value pair with the given key for the given -// flavor ID. -func DeleteExtraSpec(client *gophercloud.ServiceClient, flavorID, key string) (r DeleteExtraSpecResult) { - resp, err := client.Delete(extraSpecDeleteURL(client, flavorID, key), &gophercloud.RequestOpts{ - OkCodes: []int{200}, - }) - _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) - return -} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go deleted file mode 100644 index 4da14118a35..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/results.go +++ /dev/null @@ -1,271 +0,0 @@ -package flavors - -import ( - "encoding/json" - "strconv" - - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/pagination" -) - -type commonResult struct { - gophercloud.Result -} - -// CreateResult is the response of a Get operations. Call its Extract method to -// interpret it as a Flavor. -type CreateResult struct { - commonResult -} - -// UpdateResult is the response of a Put operation. Call its Extract method to -// interpret it as a Flavor. -type UpdateResult struct { - commonResult -} - -// GetResult is the response of a Get operations. Call its Extract method to -// interpret it as a Flavor. -type GetResult struct { - commonResult -} - -// DeleteResult is the result from a Delete operation. Call its ExtractErr -// method to determine if the call succeeded or failed. -type DeleteResult struct { - gophercloud.ErrResult -} - -// Extract provides access to the individual Flavor returned by the Get and -// Create functions. -func (r commonResult) Extract() (*Flavor, error) { - var s struct { - Flavor *Flavor `json:"flavor"` - } - err := r.ExtractInto(&s) - return s.Flavor, err -} - -// Flavor represent (virtual) hardware configurations for server resources -// in a region. -type Flavor struct { - // ID is the flavor's unique ID. - ID string `json:"id"` - - // Disk is the amount of root disk, measured in GB. - Disk int `json:"disk"` - - // RAM is the amount of memory, measured in MB. - RAM int `json:"ram"` - - // Name is the name of the flavor. - Name string `json:"name"` - - // RxTxFactor describes bandwidth alterations of the flavor. - RxTxFactor float64 `json:"rxtx_factor"` - - // Swap is the amount of swap space, measured in MB. - Swap int `json:"-"` - - // VCPUs indicates how many (virtual) CPUs are available for this flavor. - VCPUs int `json:"vcpus"` - - // IsPublic indicates whether the flavor is public. - IsPublic bool `json:"os-flavor-access:is_public"` - - // Ephemeral is the amount of ephemeral disk space, measured in GB. - Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"` - - // Description is a free form description of the flavor. Limited to - // 65535 characters in length. Only printable characters are allowed. - // New in version 2.55 - Description string `json:"description"` -} - -func (r *Flavor) UnmarshalJSON(b []byte) error { - type tmp Flavor - var s struct { - tmp - Swap interface{} `json:"swap"` - } - err := json.Unmarshal(b, &s) - if err != nil { - return err - } - - *r = Flavor(s.tmp) - - switch t := s.Swap.(type) { - case float64: - r.Swap = int(t) - case string: - switch t { - case "": - r.Swap = 0 - default: - swap, err := strconv.ParseFloat(t, 64) - if err != nil { - return err - } - r.Swap = int(swap) - } - } - - return nil -} - -// FlavorPage contains a single page of all flavors from a ListDetails call. -type FlavorPage struct { - pagination.LinkedPageBase -} - -// IsEmpty determines if a FlavorPage contains any results. -func (page FlavorPage) IsEmpty() (bool, error) { - if page.StatusCode == 204 { - return true, nil - } - - flavors, err := ExtractFlavors(page) - return len(flavors) == 0, err -} - -// NextPageURL uses the response's embedded link reference to navigate to the -// next page of results. -func (page FlavorPage) NextPageURL() (string, error) { - var s struct { - Links []gophercloud.Link `json:"flavors_links"` - } - err := page.ExtractInto(&s) - if err != nil { - return "", err - } - return gophercloud.ExtractNextURL(s.Links) -} - -// ExtractFlavors provides access to the list of flavors in a page acquired -// from the ListDetail operation. -func ExtractFlavors(r pagination.Page) ([]Flavor, error) { - var s struct { - Flavors []Flavor `json:"flavors"` - } - err := (r.(FlavorPage)).ExtractInto(&s) - return s.Flavors, err -} - -// AccessPage contains a single page of all FlavorAccess entries for a flavor. -type AccessPage struct { - pagination.SinglePageBase -} - -// IsEmpty indicates whether an AccessPage is empty. -func (page AccessPage) IsEmpty() (bool, error) { - if page.StatusCode == 204 { - return true, nil - } - - v, err := ExtractAccesses(page) - return len(v) == 0, err -} - -// ExtractAccesses interprets a page of results as a slice of FlavorAccess. -func ExtractAccesses(r pagination.Page) ([]FlavorAccess, error) { - var s struct { - FlavorAccesses []FlavorAccess `json:"flavor_access"` - } - err := (r.(AccessPage)).ExtractInto(&s) - return s.FlavorAccesses, err -} - -type accessResult struct { - gophercloud.Result -} - -// AddAccessResult is the response of an AddAccess operation. Call its -// Extract method to interpret it as a slice of FlavorAccess. -type AddAccessResult struct { - accessResult -} - -// RemoveAccessResult is the response of a RemoveAccess operation. Call its -// Extract method to interpret it as a slice of FlavorAccess. -type RemoveAccessResult struct { - accessResult -} - -// Extract provides access to the result of an access create or delete. -// The result will be all accesses that the flavor has. -func (r accessResult) Extract() ([]FlavorAccess, error) { - var s struct { - FlavorAccesses []FlavorAccess `json:"flavor_access"` - } - err := r.ExtractInto(&s) - return s.FlavorAccesses, err -} - -// FlavorAccess represents an ACL of tenant access to a specific Flavor. -type FlavorAccess struct { - // FlavorID is the unique ID of the flavor. - FlavorID string `json:"flavor_id"` - - // TenantID is the unique ID of the tenant. - TenantID string `json:"tenant_id"` -} - -// Extract interprets any extraSpecsResult as ExtraSpecs, if possible. -func (r extraSpecsResult) Extract() (map[string]string, error) { - var s struct { - ExtraSpecs map[string]string `json:"extra_specs"` - } - err := r.ExtractInto(&s) - return s.ExtraSpecs, err -} - -// extraSpecsResult contains the result of a call for (potentially) multiple -// key-value pairs. Call its Extract method to interpret it as a -// map[string]interface. -type extraSpecsResult struct { - gophercloud.Result -} - -// ListExtraSpecsResult contains the result of a Get operation. Call its Extract -// method to interpret it as a map[string]interface. -type ListExtraSpecsResult struct { - extraSpecsResult -} - -// CreateExtraSpecResult contains the result of a Create operation. Call its -// Extract method to interpret it as a map[string]interface. -type CreateExtraSpecsResult struct { - extraSpecsResult -} - -// extraSpecResult contains the result of a call for individual a single -// key-value pair. -type extraSpecResult struct { - gophercloud.Result -} - -// GetExtraSpecResult contains the result of a Get operation. Call its Extract -// method to interpret it as a map[string]interface. -type GetExtraSpecResult struct { - extraSpecResult -} - -// UpdateExtraSpecResult contains the result of an Update operation. Call its -// Extract method to interpret it as a map[string]interface. -type UpdateExtraSpecResult struct { - extraSpecResult -} - -// DeleteExtraSpecResult contains the result of a Delete operation. Call its -// ExtractErr method to determine if the call succeeded or failed. -type DeleteExtraSpecResult struct { - gophercloud.ErrResult -} - -// Extract interprets any extraSpecResult as an ExtraSpec, if possible. -func (r extraSpecResult) Extract() (map[string]string, error) { - var s map[string]string - err := r.ExtractInto(&s) - return s, err -} diff --git a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go b/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go deleted file mode 100644 index 65bbb654012..00000000000 --- a/vendor/github.com/gophercloud/gophercloud/openstack/compute/v2/flavors/urls.go +++ /dev/null @@ -1,53 +0,0 @@ -package flavors - -import ( - "github.com/gophercloud/gophercloud" -) - -func getURL(client *gophercloud.ServiceClient, id string) string { - return client.ServiceURL("flavors", id) -} - -func listURL(client *gophercloud.ServiceClient) string { - return client.ServiceURL("flavors", "detail") -} - -func createURL(client *gophercloud.ServiceClient) string { - return client.ServiceURL("flavors") -} - -func updateURL(client *gophercloud.ServiceClient, id string) string { - return client.ServiceURL("flavors", id) -} - -func deleteURL(client *gophercloud.ServiceClient, id string) string { - return client.ServiceURL("flavors", id) -} - -func accessURL(client *gophercloud.ServiceClient, id string) string { - return client.ServiceURL("flavors", id, "os-flavor-access") -} - -func accessActionURL(client *gophercloud.ServiceClient, id string) string { - return client.ServiceURL("flavors", id, "action") -} - -func extraSpecsListURL(client *gophercloud.ServiceClient, id string) string { - return client.ServiceURL("flavors", id, "os-extra_specs") -} - -func extraSpecsGetURL(client *gophercloud.ServiceClient, id, key string) string { - return client.ServiceURL("flavors", id, "os-extra_specs", key) -} - -func extraSpecsCreateURL(client *gophercloud.ServiceClient, id string) string { - return client.ServiceURL("flavors", id, "os-extra_specs") -} - -func extraSpecUpdateURL(client *gophercloud.ServiceClient, id, key string) string { - return client.ServiceURL("flavors", id, "os-extra_specs", key) -} - -func extraSpecDeleteURL(client *gophercloud.ServiceClient, id, key string) string { - return client.ServiceURL("flavors", id, "os-extra_specs", key) -} diff --git a/vendor/github.com/gophercloud/gophercloud/params.go b/vendor/github.com/gophercloud/gophercloud/params.go index 17b200cd239..5abc2c55899 100644 --- a/vendor/github.com/gophercloud/gophercloud/params.go +++ b/vendor/github.com/gophercloud/gophercloud/params.go @@ -318,8 +318,15 @@ converted into query parameters based on a "q" tag. For example: will be converted into "?x_bar=AAA&lorem_ipsum=BBB". -The struct's fields may be strings, integers, or boolean values. Fields left at -their type's zero value will be omitted from the query. +The struct's fields may be strings, integers, slices, or boolean values. Fields +left at their type's zero value will be omitted from the query. + +Slice are handled in one of two ways: + + type struct Something { + Bar []string `q:"bar"` // E.g. ?bar=1&bar=2 + Baz []int `q:"baz" format="comma-separated"` // E.g. ?baz=1,2 + } */ func BuildQueryString(opts interface{}) (*url.URL, error) { optsValue := reflect.ValueOf(opts) @@ -358,16 +365,22 @@ func BuildQueryString(opts interface{}) (*url.URL, error) { case reflect.Bool: params.Add(tags[0], strconv.FormatBool(v.Bool())) case reflect.Slice: + var values []string switch v.Type().Elem() { case reflect.TypeOf(0): for i := 0; i < v.Len(); i++ { - params.Add(tags[0], strconv.FormatInt(v.Index(i).Int(), 10)) + values = append(values, strconv.FormatInt(v.Index(i).Int(), 10)) } default: for i := 0; i < v.Len(); i++ { - params.Add(tags[0], v.Index(i).String()) + values = append(values, v.Index(i).String()) } } + if sliceFormat := f.Tag.Get("format"); sliceFormat == "comma-separated" { + params.Add(tags[0], strings.Join(values, ",")) + } else { + params[tags[0]] = append(params[tags[0]], values...) + } case reflect.Map: if v.Type().Key().Kind() == reflect.String && v.Type().Elem().Kind() == reflect.String { var s []string diff --git a/vendor/github.com/gophercloud/gophercloud/provider_client.go b/vendor/github.com/gophercloud/gophercloud/provider_client.go index e53b713cd4f..7e25882c873 100644 --- a/vendor/github.com/gophercloud/gophercloud/provider_client.go +++ b/vendor/github.com/gophercloud/gophercloud/provider_client.go @@ -14,7 +14,7 @@ import ( // DefaultUserAgent is the default User-Agent string set in the request header. const ( - DefaultUserAgent = "gophercloud/v1.7.0" + DefaultUserAgent = "gophercloud/v1.14.1" DefaultMaxBackoffRetries = 60 ) diff --git a/vendor/github.com/gophercloud/gophercloud/service_client.go b/vendor/github.com/gophercloud/gophercloud/service_client.go index dd54abe30ef..94a161e3408 100644 --- a/vendor/github.com/gophercloud/gophercloud/service_client.go +++ b/vendor/github.com/gophercloud/gophercloud/service_client.go @@ -47,7 +47,7 @@ func (client *ServiceClient) ServiceURL(parts ...string) string { return client.ResourceBaseURL() + strings.Join(parts, "/") } -func (client *ServiceClient) initReqOpts(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) { +func (client *ServiceClient) initReqOpts(JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) { if v, ok := (JSONBody).(io.Reader); ok { opts.RawBody = v } else if JSONBody != nil { @@ -57,14 +57,6 @@ func (client *ServiceClient) initReqOpts(url string, JSONBody interface{}, JSONR if JSONResponse != nil { opts.JSONResponse = JSONResponse } - - if opts.MoreHeaders == nil { - opts.MoreHeaders = make(map[string]string) - } - - if client.Microversion != "" { - client.setMicroversionHeader(opts) - } } // Get calls `Request` with the "GET" HTTP verb. @@ -72,7 +64,7 @@ func (client *ServiceClient) Get(url string, JSONResponse interface{}, opts *Req if opts == nil { opts = new(RequestOpts) } - client.initReqOpts(url, nil, JSONResponse, opts) + client.initReqOpts(nil, JSONResponse, opts) return client.Request("GET", url, opts) } @@ -81,7 +73,7 @@ func (client *ServiceClient) Post(url string, JSONBody interface{}, JSONResponse if opts == nil { opts = new(RequestOpts) } - client.initReqOpts(url, JSONBody, JSONResponse, opts) + client.initReqOpts(JSONBody, JSONResponse, opts) return client.Request("POST", url, opts) } @@ -90,7 +82,7 @@ func (client *ServiceClient) Put(url string, JSONBody interface{}, JSONResponse if opts == nil { opts = new(RequestOpts) } - client.initReqOpts(url, JSONBody, JSONResponse, opts) + client.initReqOpts(JSONBody, JSONResponse, opts) return client.Request("PUT", url, opts) } @@ -99,7 +91,7 @@ func (client *ServiceClient) Patch(url string, JSONBody interface{}, JSONRespons if opts == nil { opts = new(RequestOpts) } - client.initReqOpts(url, JSONBody, JSONResponse, opts) + client.initReqOpts(JSONBody, JSONResponse, opts) return client.Request("PATCH", url, opts) } @@ -108,7 +100,7 @@ func (client *ServiceClient) Delete(url string, opts *RequestOpts) (*http.Respon if opts == nil { opts = new(RequestOpts) } - client.initReqOpts(url, nil, nil, opts) + client.initReqOpts(nil, nil, opts) return client.Request("DELETE", url, opts) } @@ -117,7 +109,7 @@ func (client *ServiceClient) Head(url string, opts *RequestOpts) (*http.Response if opts == nil { opts = new(RequestOpts) } - client.initReqOpts(url, nil, nil, opts) + client.initReqOpts(nil, nil, opts) return client.Request("HEAD", url, opts) } @@ -142,10 +134,19 @@ func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { // Request carries out the HTTP operation for the service client func (client *ServiceClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { + if options.MoreHeaders == nil { + options.MoreHeaders = make(map[string]string) + } + + if client.Microversion != "" { + client.setMicroversionHeader(options) + } + if len(client.MoreHeaders) > 0 { if options == nil { options = new(RequestOpts) } + for k, v := range client.MoreHeaders { options.MoreHeaders[k] = v } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md b/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md index 2cfef4fd828..95bc952b159 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md +++ b/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md @@ -1,3 +1,67 @@ +## v2.8.0 (2025-08-18) + +* [GH-3348](https://github.com/gophercloud/gophercloud/pull/3348) [v2] [networking] add ExtractRoutersInto func helper to routers +* [GH-3354](https://github.com/gophercloud/gophercloud/pull/3354) [v2] Fix a small typo +* [GH-3358](https://github.com/gophercloud/gophercloud/pull/3358) [v2] tests: fix devstack master branch tests +* [GH-3361](https://github.com/gophercloud/gophercloud/pull/3361) [v2] octavia: fix http_version type to float +* [GH-3362](https://github.com/gophercloud/gophercloud/pull/3362) [v2] tests: fix containerinfra template creation +* [GH-3367](https://github.com/gophercloud/gophercloud/pull/3367) [v2] Use Makefile for CI jobs +* [GH-3375](https://github.com/gophercloud/gophercloud/pull/3375) [v2] core: add missing Builder interfaces +* [GH-3378](https://github.com/gophercloud/gophercloud/pull/3378) [v2] tests: fix failing rabbitmq service +* [GH-3379](https://github.com/gophercloud/gophercloud/pull/3379) [v2] CI: Remove Bobcat +* [GH-3384](https://github.com/gophercloud/gophercloud/pull/3384) [v2] Move master CI jobs to Ubuntu 24.04 +* [GH-3386](https://github.com/gophercloud/gophercloud/pull/3386) [v2] tests: Fix TestBGPAgentCRUD +* [GH-3387](https://github.com/gophercloud/gophercloud/pull/3387) [v2] Update the doc of openstack.AuthOptionsFromEnv function +* [GH-3389](https://github.com/gophercloud/gophercloud/pull/3389) [v2] networking: add constants for statuses +* [GH-3391](https://github.com/gophercloud/gophercloud/pull/3391) [v2] CI: Add Epoxy +* [GH-3393](https://github.com/gophercloud/gophercloud/pull/3393) [v2] dns: implement shared zones list +* [GH-3394](https://github.com/gophercloud/gophercloud/pull/3394) [v2] acceptance: Prevent 409 when bulk-creating secgroup rules +* [GH-3396](https://github.com/gophercloud/gophercloud/pull/3396) [v2] identity: add support for string boolean in users' enabled member +* [GH-3397](https://github.com/gophercloud/gophercloud/pull/3397) [v2] Adjust List func to accept a Builder in tenants, routers and security groups packages +* [GH-3399](https://github.com/gophercloud/gophercloud/pull/3399) [v2] blockstorage: add manage-existing and unmanage api call +* [GH-3401](https://github.com/gophercloud/gophercloud/pull/3401) [v2] Added address groups to Networking extensions, with tests. +* [GH-3407](https://github.com/gophercloud/gophercloud/pull/3407) [v2] neutron: add segment_id support to subnets +* [GH-3413](https://github.com/gophercloud/gophercloud/pull/3413) [v2] build(deps): bump joelanford/go-apidiff from 0.8.2 to 0.8.3 +* [GH-3416](https://github.com/gophercloud/gophercloud/pull/3416) [v2] tests: bump devstack-action +* [GH-3422](https://github.com/gophercloud/gophercloud/pull/3422) [v2] Fix documentation for gateway_ip in subnet update +* [GH-3431](https://github.com/gophercloud/gophercloud/pull/3431) [v2] Use container-infra for OpenStack-API-Version +* [GH-3433](https://github.com/gophercloud/gophercloud/pull/3433) [v2] make: Use fixed version of gotestsum +* [GH-3434](https://github.com/gophercloud/gophercloud/pull/3434) [v2] Randomize test order for unit tests +* [GH-3435](https://github.com/gophercloud/gophercloud/pull/3435) [v2] Add versioned endpoint discovery +* [GH-3438](https://github.com/gophercloud/gophercloud/pull/3438) [v2] dns: add support for /v2/quotas +* [GH-3439](https://github.com/gophercloud/gophercloud/pull/3439) [v2] neutron: add segments extension package +* [GH-3446](https://github.com/gophercloud/gophercloud/pull/3446) nova: add support for hostname updates +* [GH-3452](https://github.com/gophercloud/gophercloud/pull/3452) [v2] neutron: allow omission of subnet_id for IP address +* [GH-3454](https://github.com/gophercloud/gophercloud/pull/3454) [v2] blockstorage: add isPublic query option for volume types +* [GH-3458](https://github.com/gophercloud/gophercloud/pull/3458) [v2] Fix pagination for messaging client +* [GH-3465](https://github.com/gophercloud/gophercloud/pull/3465) [v2] tests: Fix TestVLANTransparentCRUD test +* [GH-3466](https://github.com/gophercloud/gophercloud/pull/3466) [v2] tests: fix tests for recent PR backports +* [GH-3469](https://github.com/gophercloud/gophercloud/pull/3469) [v2] tests: shorten GH-A job names +* [GH-3473](https://github.com/gophercloud/gophercloud/pull/3473) [v2] core: clone service type aliases instead of referencing global slice +* [GH-3475](https://github.com/gophercloud/gophercloud/pull/3475) [v2] Implement update & delete traits on resource provider +* [GH-3476](https://github.com/gophercloud/gophercloud/pull/3476) [v2] tests: fix volumetypes unit tests +* [GH-3477](https://github.com/gophercloud/gophercloud/pull/3477) [v2] script: Improve getenvvar helper +* [GH-3481](https://github.com/gophercloud/gophercloud/pull/3481) [v2] Implement hypervisors.GetExt: Get with Query parameter +* [GH-3487](https://github.com/gophercloud/gophercloud/pull/3487) [v2] Add networking taas tapmirror suite +* [GH-3489](https://github.com/gophercloud/gophercloud/pull/3489) [v2] Fix incorrect ICMP field description in PortRangeMax comment +* [GH-3494](https://github.com/gophercloud/gophercloud/pull/3494) [v2] Networking v2: Support two time formats for subnet, router, SG rule (#3492) +* [GH-3495](https://github.com/gophercloud/gophercloud/pull/3495) [v2] build(deps): bump actions/checkout from 4 to 5 + +## v2.7.0 (2025-04-03) + +* [GH-3306](https://github.com/gophercloud/gophercloud/pull/3306) [v2] identity: Add Get endpoint by ID +* [GH-3325](https://github.com/gophercloud/gophercloud/pull/3325) [v2] Switch to a version of gocovmerge compatible with go 1.22 +* [GH-3327](https://github.com/gophercloud/gophercloud/pull/3327) Merge pull request #3209 from shiftstack/proper-service-discovery +* [GH-3328](https://github.com/gophercloud/gophercloud/pull/3328) [v2] Improve support for `network standard-attr-*` extensions +* [GH-3330](https://github.com/gophercloud/gophercloud/pull/3330) [v2] Enhance Snapshot struct and add ListDetail function in V3 blockstorage +* [GH-3333](https://github.com/gophercloud/gophercloud/pull/3333) [v2] vpnaas: add support for more ciphers (auth, encryption, pfs modes) +* [GH-3334](https://github.com/gophercloud/gophercloud/pull/3334) [v2] Added support for VIF's in Baremetal +* [GH-3335](https://github.com/gophercloud/gophercloud/pull/3335) [v2] Baremetal virtual media Get API + +## v2.6.0 (2025-03-03) + +* [GH-3309](https://github.com/gophercloud/gophercloud/pull/3309) Backport: Added support for hypervisor_hostname to v2 + ## v2.5.0 (2025-02-11) * [GH-3278](https://github.com/gophercloud/gophercloud/pull/3278) [v2] test: Ensure that randomly created secgroup rules don't conflict diff --git a/vendor/github.com/gophercloud/gophercloud/v2/Makefile b/vendor/github.com/gophercloud/gophercloud/v2/Makefile index 2a0618a6b6e..c63adb8d031 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/Makefile +++ b/vendor/github.com/gophercloud/gophercloud/v2/Makefile @@ -1,7 +1,9 @@ undefine GOFLAGS GOLANGCI_LINT_VERSION?=v1.62.2 -GO_TEST?=go run gotest.tools/gotestsum@latest --format testname -- +GOTESTSUM_VERSION?=v1.12.2 +GO_TEST?=go run gotest.tools/gotestsum@$(GOTESTSUM_VERSION) --format testname -- +TIMEOUT := "60m" ifeq ($(shell command -v podman 2> /dev/null),) RUNNER=docker @@ -9,15 +11,18 @@ else RUNNER=podman endif -# if the golangci-lint steps fails with the following error message: +# if the golangci-lint steps fails with one of the following error messages: # # directory prefix . does not contain main module or its selected dependencies # +# failed to initialize build cache at /root/.cache/golangci-lint: mkdir /root/.cache/golangci-lint: permission denied +# # you probably have to fix the SELinux security context for root directory plus your cache # # chcon -Rt svirt_sandbox_file_t . # chcon -Rt svirt_sandbox_file_t ~/.cache/golangci-lint lint: + mkdir -p ~/.cache/golangci-lint/$(GOLANGCI_LINT_VERSION) $(RUNNER) run -t --rm \ -v $(shell pwd):/app \ -v ~/.cache/golangci-lint/$(GOLANGCI_LINT_VERSION):/root/.cache \ @@ -31,84 +36,88 @@ format: .PHONY: format unit: - $(GO_TEST) ./... + $(GO_TEST) -shuffle on ./... .PHONY: unit coverage: - $(GO_TEST) -covermode count -coverprofile cover.out -coverpkg=./... ./... + $(GO_TEST) -shuffle on -covermode count -coverprofile cover.out -coverpkg=./... ./... .PHONY: coverage -acceptance: acceptance-baremetal acceptance-blockstorage acceptance-compute acceptance-container acceptance-containerinfra acceptance-db acceptance-dns acceptance-identity acceptance-imageservice acceptance-keymanager acceptance-loadbalancer acceptance-messaging acceptance-networking acceptance-objectstorage acceptance-orchestration acceptance-placement acceptance-sharedfilesystems acceptance-workflow +acceptance: acceptance-basic acceptance-baremetal acceptance-blockstorage acceptance-compute acceptance-container acceptance-containerinfra acceptance-db acceptance-dns acceptance-identity acceptance-image acceptance-keymanager acceptance-loadbalancer acceptance-messaging acceptance-networking acceptance-objectstorage acceptance-orchestration acceptance-placement acceptance-sharedfilesystems acceptance-workflow .PHONY: acceptance +acceptance-basic: + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack +.PHONY: acceptance-basic + acceptance-baremetal: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/baremetal/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/baremetal/... .PHONY: acceptance-baremetal acceptance-blockstorage: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/blockstorage/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/blockstorage/... .PHONY: acceptance-blockstorage acceptance-compute: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/compute/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/compute/... .PHONY: acceptance-compute acceptance-container: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/container/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/container/... .PHONY: acceptance-container acceptance-containerinfra: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/containerinfra/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/containerinfra/... .PHONY: acceptance-containerinfra acceptance-db: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/db/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/db/... .PHONY: acceptance-db acceptance-dns: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/dns/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/dns/... .PHONY: acceptance-dns acceptance-identity: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/identity/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/identity/... .PHONY: acceptance-identity acceptance-image: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/imageservice/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/image/... .PHONY: acceptance-image acceptance-keymanager: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/keymanager/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/keymanager/... .PHONY: acceptance-keymanager acceptance-loadbalancer: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/loadbalancer/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/loadbalancer/... .PHONY: acceptance-loadbalancer acceptance-messaging: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/messaging/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/messaging/... .PHONY: acceptance-messaging acceptance-networking: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/networking/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/networking/... .PHONY: acceptance-networking acceptance-objectstorage: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/objectstorage/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/objectstorage/... .PHONY: acceptance-objectstorage acceptance-orchestration: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/orchestration/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/orchestration/... .PHONY: acceptance-orchestration acceptance-placement: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/placement/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/placement/... .PHONY: acceptance-placement acceptance-sharedfilesystems: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/sharedfilesystems/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/sharedfilesystems/... .PHONY: acceptance-sharefilesystems acceptance-workflow: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/workflow/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/workflow/... .PHONY: acceptance-workflow diff --git a/vendor/github.com/gophercloud/gophercloud/v2/endpoint_search.go b/vendor/github.com/gophercloud/gophercloud/v2/endpoint_search.go index 2fbc3c97f14..34d76a1b8da 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/endpoint_search.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/endpoint_search.go @@ -1,5 +1,7 @@ package gophercloud +import "slices" + // Availability indicates to whom a specific service endpoint is accessible: // the internet at large, internal networks only, or only to administrators. // Different identity services use different terminology for these. Identity v2 @@ -22,6 +24,31 @@ const ( AvailabilityInternal Availability = "internal" ) +// ServiceTypeAliases contains a mapping of service types to any aliases, as +// defined by the OpenStack Service Types Authority. Only service types that +// we support are included. +var ServiceTypeAliases = map[string][]string{ + "application-container": {"container"}, + "baremetal": {"bare-metal"}, + "baremetal-introspection": {}, + "block-storage": {"block-store", "volume", "volumev2", "volumev3"}, + "compute": {}, + "container-infrastructure-management": {"container-infrastructure", "container-infra"}, + "database": {}, + "dns": {}, + "identity": {}, + "image": {}, + "key-manager": {}, + "load-balancer": {}, + "message": {"messaging"}, + "networking": {}, + "object-store": {}, + "orchestration": {}, + "placement": {}, + "shared-file-system": {"sharev2", "share"}, + "workflow": {"workflowv2"}, +} + // EndpointOpts specifies search criteria used by queries against an // OpenStack service catalog. The options must contain enough information to // unambiguously identify one, and only one, endpoint within the catalog. @@ -30,8 +57,9 @@ const ( // package, like "openstack.NewComputeV2()". type EndpointOpts struct { // Type [required] is the service type for the client (e.g., "compute", - // "object-store"). Generally, this will be supplied by the service client - // function, but a user-given value will be honored if provided. + // "object-store"), as defined by the OpenStack Service Types Authority. + // This will generally be supplied by the service client function, but a + // user-given value will be honored if provided. Type string // Name [optional] is the service name for the client (e.g., "nova") as it @@ -39,11 +67,23 @@ type EndpointOpts struct { // different Name, which is why both Type and Name are sometimes needed. Name string + // Aliases [optional] is the set of aliases of the service type (e.g. + // "volumev2"/"volumev3", "volume" and "block-store" for the + // "block-storage" service type), as defined by the OpenStack Service Types + // Authority. As with Type, this will generally be supplied by the service + // client function, but a user-given value will be honored if provided. + Aliases []string + // Region [required] is the geographic region in which the endpoint resides, // generally specifying which datacenter should house your resources. // Required only for services that span multiple regions. Region string + // Version [optional] is the major version of the service required. It it not + // a microversion. Use this to ensure the correct endpoint is selected when + // multiple API versions are available. + Version int + // Availability [optional] is the visibility of the endpoint to be returned. // Valid types include the constants AvailabilityPublic, AvailabilityInternal, // or AvailabilityAdmin from this package. @@ -73,4 +113,26 @@ func (eo *EndpointOpts) ApplyDefaults(t string) { if eo.Availability == "" { eo.Availability = AvailabilityPublic } + if len(eo.Aliases) == 0 { + if aliases, ok := ServiceTypeAliases[eo.Type]; ok { + // happy path: user requested a service type by its official name + eo.Aliases = slices.Clone(aliases) + } else { + // unhappy path: user requested a service type by its alias or an + // invalid/unsupported service type + // TODO(stephenfin): This should probably be an error in v3 + for t, aliases := range ServiceTypeAliases { + if slices.Contains(aliases, eo.Type) { + // we intentionally override the service type, even if it + // was explicitly requested by the user + eo.Type = t + eo.Aliases = slices.Clone(aliases) + } + } + } + } +} + +func (eo *EndpointOpts) Types() []string { + return append([]string{eo.Type}, eo.Aliases...) } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/auth_env.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/auth_env.go index 893787b7874..9ecc5b4efea 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/auth_env.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/auth_env.go @@ -24,8 +24,8 @@ OS_PROJECT_NAME and the latter are expected against a v3 auth api. If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will still be referred as "tenant" in Gophercloud. -If OS_PROJECT_NAME is set, it requires OS_PROJECT_ID to be set as well to -handle projects not on the default domain. +If OS_PROJECT_NAME is set, it requires OS_DOMAIN_ID or OS_DOMAIN_NAME to be +set as well to handle projects not on the default domain. To use this function, first set the OS_* environment variables (for example, by sourcing an `openrc` file), then: diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/requests.go index 644e9cb1d30..85b67df8793 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/requests.go @@ -122,6 +122,21 @@ func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pa }) } +// ListDetail returns Snapshots with additional details optionally limited by the conditions provided in ListOpts. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailsURL(client) + if opts != nil { + query, err := opts.ToSnapshotListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return SnapshotPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + // UpdateOptsBuilder allows extensions to add additional parameters to the // Update request. type UpdateOptsBuilder interface { diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/results.go index 8a1440da42f..87a2dec2c68 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/results.go @@ -36,6 +36,21 @@ type Snapshot struct { // User-defined key-value pairs. Metadata map[string]string `json:"metadata"` + + // Progress of the snapshot creation. + Progress string `json:"os-extended-snapshot-attributes:progress"` + + // Project ID that owns the snapshot. + ProjectID string `json:"os-extended-snapshot-attributes:project_id"` + + // ID of the group snapshot, if applicable. + GroupSnapshotID string `json:"group_snapshot_id"` + + // User ID that created the snapshot. + UserID string `json:"user_id"` + + // Indicates whether the snapshot consumes quota. + ConsumesQuota bool `json:"consumes_quota"` } // CreateResult contains the response body and error from a Create request. diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/urls.go index b68cee9ccf4..de6a83cc32c 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/urls.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots/urls.go @@ -18,6 +18,10 @@ func listURL(c *gophercloud.ServiceClient) string { return createURL(c) } +func listDetailsURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("snapshots", "detail") +} + func updateURL(c *gophercloud.ServiceClient, id string) string { return deleteURL(c, id) } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/doc.go index 2ab4af93ee3..e018b57a8d4 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/doc.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/doc.go @@ -157,5 +157,12 @@ Example of Attaching a Volume to an Instance if err != nil { panic(err) } + +Example of Unmanaging a Volume + + err := volumes.Unmanage(context.TODO(), client, volume.ID).ExtractErr() + if err != nil { + panic(err) + } */ package volumes diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/requests.go index 77210943b53..1026d1ecaa5 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/requests.go @@ -623,6 +623,12 @@ func SetImageMetadata(ctx context.Context, client *gophercloud.ServiceClient, id return } +// BootableOptsBuilder allows extensions to add additional parameters to the +// SetBootable request. +type BootableOptsBuilder interface { + ToBootableMap() (map[string]any, error) +} + // BootableOpts contains options for setting bootable status to a volume. type BootableOpts struct { // Enables or disables the bootable attribute. You can boot an instance from a bootable volume. @@ -636,7 +642,7 @@ func (opts BootableOpts) ToBootableMap() (map[string]any, error) { } // SetBootable will set bootable status on a volume based on the values in BootableOpts -func SetBootable(ctx context.Context, client *gophercloud.ServiceClient, id string, opts BootableOpts) (r SetBootableResult) { +func SetBootable(ctx context.Context, client *gophercloud.ServiceClient, id string, opts BootableOptsBuilder) (r SetBootableResult) { b, err := opts.ToBootableMap() if err != nil { r.Err = err @@ -697,6 +703,12 @@ func ChangeType(ctx context.Context, client *gophercloud.ServiceClient, id strin return } +// ReImageOptsBuilder allows extensions to add additional parameters to the +// ReImage request. +type ReImageOptsBuilder interface { + ToReImageMap() (map[string]any, error) +} + // ReImageOpts contains options for Re-image a volume. type ReImageOpts struct { // New image id @@ -711,7 +723,7 @@ func (opts ReImageOpts) ToReImageMap() (map[string]any, error) { } // ReImage will re-image a volume based on the values in ReImageOpts -func ReImage(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ReImageOpts) (r ReImageResult) { +func ReImage(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ReImageOptsBuilder) (r ReImageResult) { b, err := opts.ToReImageMap() if err != nil { r.Err = err @@ -763,3 +775,14 @@ func ResetStatus(ctx context.Context, client *gophercloud.ServiceClient, id stri _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return } + +// Unmanage removes a volume from Block Storage management without +// removing the back-end storage object that is associated with it. +func Unmanage(ctx context.Context, client *gophercloud.ServiceClient, id string) (r UnmanageResult) { + body := map[string]any{"os-unmanage": make(map[string]any)} + resp, err := client.Post(ctx, actionURL(client, id), body, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/results.go index 3f184b398ef..e99ef5e197c 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/results.go @@ -399,3 +399,8 @@ type ReImageResult struct { type ResetStatusResult struct { gophercloud.ErrResult } + +// UnmanageResult contains the response error from a Unmanage request. +type UnmanageResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumetypes/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumetypes/requests.go index d419d75d9ba..8849c394fbd 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumetypes/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumetypes/requests.go @@ -73,6 +73,9 @@ type ListOptsBuilder interface { // ListOpts holds options for listing Volume Types. It is passed to the volumetypes.List // function. type ListOpts struct { + // Specifies whether the query should include public or private Volume Types. + // By default, it queries both types. + IsPublic visibility `q:"is_public"` // Comma-separated list of sort keys and optional sort directions in the // form of [:]. Sort string `q:"sort"` @@ -84,8 +87,22 @@ type ListOpts struct { Marker string `q:"marker"` } +type visibility string + +const ( + // VisibilityDefault enables querying both public and private Volume Types. + VisibilityDefault visibility = "None" + // VisibilityPublic restricts the query to only public Volume Types. + VisibilityPublic visibility = "true" + // VisibilityPrivate restricts the query to only private Volume Types. + VisibilityPrivate visibility = "false" +) + // ToVolumeTypeListQuery formats a ListOpts into a query string. func (opts ListOpts) ToVolumeTypeListQuery() (string, error) { + if opts.IsPublic == "" { + opts.IsPublic = VisibilityDefault + } q, err := gophercloud.BuildQueryString(opts) return q.String(), err } @@ -93,14 +110,14 @@ func (opts ListOpts) ToVolumeTypeListQuery() (string, error) { // List returns Volume types. func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { url := listURL(client) - - if opts != nil { - query, err := opts.ToVolumeTypeListQuery() - if err != nil { - return pagination.Pager{Err: err} - } - url += query + if opts == nil { + opts = ListOpts{} + } + query, err := opts.ToVolumeTypeListQuery() + if err != nil { + return pagination.Pager{Err: err} } + url += query return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { return VolumeTypePage{pagination.LinkedPageBase{PageResult: r}} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go index 43b569d3b48..73ca5c56d50 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go @@ -2,6 +2,7 @@ package openstack import ( "context" + "errors" "fmt" "reflect" "strings" @@ -162,7 +163,7 @@ func v2auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint st } } client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { - return V2EndpointURL(catalog, opts) + return V2Endpoint(context.TODO(), client, catalog, opts) } return nil @@ -283,7 +284,7 @@ func v3auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint st } } client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { - return V3EndpointURL(catalog, opts) + return V3Endpoint(context.TODO(), client, catalog, opts) } return nil @@ -344,13 +345,21 @@ func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOp }, nil } -func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string) (*gophercloud.ServiceClient, error) { +// TODO(stephenfin): Allow passing aliases to all New${SERVICE}V${VERSION} methods in v3 +func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string, version int) (*gophercloud.ServiceClient, error) { sc := new(gophercloud.ServiceClient) + eo.ApplyDefaults(clientType) + if eo.Version != 0 && eo.Version != version { + return sc, errors.New("Conflict between requested service major version and manually set version") + } + eo.Version = version + url, err := client.EndpointLocator(eo) if err != nil { return sc, err } + sc.ProviderClient = client sc.Endpoint = url sc.Type = clientType @@ -360,7 +369,7 @@ func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointO // NewBareMetalV1 creates a ServiceClient that may be used with the v1 // bare metal package. func NewBareMetalV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - sc, err := initClientOpts(client, eo, "baremetal") + sc, err := initClientOpts(client, eo, "baremetal", 1) if !strings.HasSuffix(strings.TrimSuffix(sc.Endpoint, "/"), "v1") { sc.ResourceBase = sc.Endpoint + "v1/" } @@ -370,66 +379,67 @@ func NewBareMetalV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointO // NewBareMetalIntrospectionV1 creates a ServiceClient that may be used with the v1 // bare metal introspection package. func NewBareMetalIntrospectionV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "baremetal-introspection") + return initClientOpts(client, eo, "baremetal-introspection", 1) } // NewObjectStorageV1 creates a ServiceClient that may be used with the v1 // object storage package. func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "object-store") + return initClientOpts(client, eo, "object-store", 1) } // NewComputeV2 creates a ServiceClient that may be used with the v2 compute // package. func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "compute") + return initClientOpts(client, eo, "compute", 2) } // NewNetworkV2 creates a ServiceClient that may be used with the v2 network // package. func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - sc, err := initClientOpts(client, eo, "network") + sc, err := initClientOpts(client, eo, "network", 2) sc.ResourceBase = sc.Endpoint + "v2.0/" return sc, err } +// TODO(stephenfin): Remove this in v3. We no longer support the V1 Block Storage service. // NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 // block storage service. func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "volume") + return initClientOpts(client, eo, "volume", 1) } // NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 // block storage service. func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "volumev2") + return initClientOpts(client, eo, "block-storage", 2) } // NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service. func NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "volumev3") + return initClientOpts(client, eo, "block-storage", 3) } // NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service. func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "sharev2") + return initClientOpts(client, eo, "shared-file-system", 2) } // NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 // orchestration service. func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "orchestration") + return initClientOpts(client, eo, "orchestration", 1) } // NewDBV1 creates a ServiceClient that may be used to access the v1 DB service. func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "database") + return initClientOpts(client, eo, "database", 1) } // NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS // service. func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - sc, err := initClientOpts(client, eo, "dns") + sc, err := initClientOpts(client, eo, "dns", 2) sc.ResourceBase = sc.Endpoint + "v2/" return sc, err } @@ -437,7 +447,7 @@ func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) ( // NewImageV2 creates a ServiceClient that may be used to access the v2 image // service. func NewImageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - sc, err := initClientOpts(client, eo, "image") + sc, err := initClientOpts(client, eo, "image", 2) sc.ResourceBase = sc.Endpoint + "v2/" return sc, err } @@ -445,7 +455,7 @@ func NewImageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) // NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2 // load balancer service. func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - sc, err := initClientOpts(client, eo, "load-balancer") + sc, err := initClientOpts(client, eo, "load-balancer", 2) // Fixes edge case having an OpenStack lb endpoint with trailing version number. endpoint := strings.Replace(sc.Endpoint, "v2.0/", "", -1) @@ -457,20 +467,20 @@ func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.Endpoi // NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging // service. func NewMessagingV2(client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - sc, err := initClientOpts(client, eo, "messaging") + sc, err := initClientOpts(client, eo, "message", 2) sc.MoreHeaders = map[string]string{"Client-ID": clientID} return sc, err } // NewContainerV1 creates a ServiceClient that may be used with v1 container package func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "container") + return initClientOpts(client, eo, "application-container", 1) } // NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key // manager service. func NewKeyManagerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - sc, err := initClientOpts(client, eo, "key-manager") + sc, err := initClientOpts(client, eo, "key-manager", 1) sc.ResourceBase = sc.Endpoint + "v1/" return sc, err } @@ -478,15 +488,15 @@ func NewKeyManagerV1(client *gophercloud.ProviderClient, eo gophercloud.Endpoint // NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management // package. func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "container-infra") + return initClientOpts(client, eo, "container-infrastructure-management", 1) } // NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package. func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "workflowv2") + return initClientOpts(client, eo, "workflow", 2) } // NewPlacementV1 creates a ServiceClient that may be used with the placement package. func NewPlacementV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { - return initClientOpts(client, eo, "placement") + return initClientOpts(client, eo, "placement", 1) } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/requests.go index dd3b132d1d0..34e4e00d41b 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/requests.go @@ -508,6 +508,9 @@ type CreateOpts struct { // DiskConfig [optional] controls how the created server's disk is partitioned. DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` + + // HypervisorHostname is the name of the hypervisor to which the server is scheduled. + HypervisorHostname string `json:"hypervisor_hostname,omitempty"` } // ToServerCreateMap assembles a request body based on the contents of a @@ -648,6 +651,12 @@ type UpdateOpts struct { // AccessIPv6 provides a new IPv6 address for the instance. AccessIPv6 string `json:"accessIPv6,omitempty"` + + // Hostname changes the hostname of the server. + // Requires microversion 2.90 or later. + // Note: This information is published via the metadata service and requires + // application such as cloud-init to propagate it through to the instance. + Hostname *string `json:"hostname,omitempty"` } // ToServerUpdateMap formats an UpdateOpts structure into a request body. diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint.go new file mode 100644 index 00000000000..6178434423e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint.go @@ -0,0 +1,190 @@ +package openstack + +import ( + "context" + "regexp" + "slices" + "strconv" + + "github.com/gophercloud/gophercloud/v2" + tokens2 "github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/v2/openstack/utils" +) + +var versionedServiceTypeAliasRegexp = regexp.MustCompile(`^.*v(\d)$`) + +func extractServiceTypeVersion(serviceType string) int { + matches := versionedServiceTypeAliasRegexp.FindAllStringSubmatch(serviceType, 1) + if matches != nil { + // no point converting to an int + ret, err := strconv.Atoi(matches[0][1]) + if err != nil { + return 0 + } + return ret + } + return 0 +} + +func endpointSupportsVersion(ctx context.Context, client *gophercloud.ProviderClient, serviceType, endpointURL string, expectedVersion int) (bool, error) { + // Swift doesn't support version discovery :( + if expectedVersion == 0 || serviceType == "object-store" { + return true, nil + } + + // Repeating verbatim from keystoneauth1 [1]: + // + // > The sins of our fathers become the blood on our hands. + // > If a user requests an old-style service type such as volumev2, then they + // > are inherently requesting the major API version 2. It's not a good + // > interface, but it's the one that was imposed on the world years ago + // > because the client libraries all hid the version discovery document. + // > In order to be able to ensure that a user who requests volumev2 does not + // > get a block-storage endpoint that only provides v3 of the block-storage + // > service, we need to pull the version out of the service_type. The + // > service-types-authority will prevent the growth of new monstrosities such + // > as this, but in order to move forward without breaking people, we have + // > to just cry in the corner while striking ourselves with thorned branches. + // > That said, for sure only do this hack for officially known service_types. + // + // So yeah, what mordred said. + // + // https://github.com/openstack/keystoneauth/blob/5.10.0/keystoneauth1/discover.py#L270-L290 + impliedVersion := extractServiceTypeVersion(serviceType) + if impliedVersion != 0 && impliedVersion != expectedVersion { + return false, nil + } + + // NOTE(stephenfin) In addition to the above, keystoneauth also supports a URL + // hack whereby it will extract the version from the URL. We may wish to + // implement this too. + + endpointURL, err := utils.BaseVersionedEndpoint(endpointURL) + if err != nil { + return false, err + } + + supportedVersions, err := utils.GetServiceVersions(ctx, client, endpointURL, false) + if err != nil { + return false, err + } + + for _, supportedVersion := range supportedVersions { + if supportedVersion.Major == expectedVersion { + return true, nil + } + } + + return false, nil +} + +/* +V2Endpoint discovers the endpoint URL for a specific service from a +ServiceCatalog acquired during the v2 identity service. + +The specified EndpointOpts are used to identify a unique, unambiguous endpoint +to return. It's an error both when multiple endpoints match the provided +criteria and when none do. The minimum that can be specified is a Type, but you +will also often need to specify a Name and/or a Region depending on what's +available on your OpenStack deployment. +*/ +func V2Endpoint(ctx context.Context, client *gophercloud.ProviderClient, catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. + // + // If multiple endpoints are found, we return the first result and disregard the rest. + // This behavior matches the Python library. See GH-1764. + for _, entry := range catalog.Entries { + if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Region != "" && endpoint.Region != opts.Region { + continue + } + + var endpointURL string + switch opts.Availability { + case gophercloud.AvailabilityPublic: + endpointURL = gophercloud.NormalizeURL(endpoint.PublicURL) + case gophercloud.AvailabilityInternal: + endpointURL = gophercloud.NormalizeURL(endpoint.InternalURL) + case gophercloud.AvailabilityAdmin: + endpointURL = gophercloud.NormalizeURL(endpoint.AdminURL) + default: + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + + endpointSupportsVersion, err := endpointSupportsVersion(ctx, client, entry.Type, endpointURL, opts.Version) + if err != nil { + return "", err + } + if !endpointSupportsVersion { + continue + } + + return endpointURL, nil + } + } + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} + +/* +V3Endpoint discovers the endpoint URL for a specific service from a Catalog +acquired during the v3 identity service. + +The specified EndpointOpts are used to identify a unique, unambiguous endpoint +to return. It's an error both when multiple endpoints match the provided +criteria and when none do. The minimum that can be specified is a Type, but you +will also often need to specify a Name and/or a Region depending on what's +available on your OpenStack deployment. +*/ +func V3Endpoint(ctx context.Context, client *gophercloud.ProviderClient, catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + if opts.Availability != gophercloud.AvailabilityAdmin && + opts.Availability != gophercloud.AvailabilityPublic && + opts.Availability != gophercloud.AvailabilityInternal { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + + // Extract Endpoints from the catalog entries that match the requested Type, Interface, + // Name if provided, and Region if provided. + // + // If multiple endpoints are found, we return the first result and disregard the rest. + // This behavior matches the Python library. See GH-1764. + for _, entry := range catalog.Entries { + if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Availability != gophercloud.Availability(endpoint.Interface) { + continue + } + if opts.Region != "" && endpoint.Region != opts.Region && endpoint.RegionID != opts.Region { + continue + } + + endpointURL := gophercloud.NormalizeURL(endpoint.URL) + + endpointSupportsVersion, err := endpointSupportsVersion(ctx, client, entry.Type, endpointURL, opts.Version) + if err != nil { + return "", err + } + if !endpointSupportsVersion { + continue + } + + return endpointURL, nil + } + } + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint_location.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint_location.go index 2cdbd3e7f70..573c1f06f42 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint_location.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint_location.go @@ -1,11 +1,15 @@ package openstack import ( + "slices" + "github.com/gophercloud/gophercloud/v2" tokens2 "github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens" tokens3 "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" ) +// TODO(stephenfin): Remove this module in v3. The functions below are no longer used. + /* V2EndpointURL discovers the endpoint URL for a specific service from a ServiceCatalog acquired during the v2 identity service. @@ -18,39 +22,33 @@ available on your OpenStack deployment. */ func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. - var endpoints = make([]tokens2.Endpoint, 0, 1) + // + // If multiple endpoints are found, we return the first result and disregard the rest. + // This behavior matches the Python library. See GH-1764. for _, entry := range catalog.Entries { - if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { for _, endpoint := range entry.Endpoints { - if opts.Region == "" || endpoint.Region == opts.Region { - endpoints = append(endpoints, endpoint) + if opts.Region != "" && endpoint.Region != opts.Region { + continue } - } - } - } - // If multiple endpoints were found, use the first result - // and disregard the other endpoints. - // - // This behavior matches the Python library. See GH-1764. - if len(endpoints) > 1 { - endpoints = endpoints[0:1] - } + var endpointURL string + switch opts.Availability { + case gophercloud.AvailabilityPublic: + endpointURL = gophercloud.NormalizeURL(endpoint.PublicURL) + case gophercloud.AvailabilityInternal: + endpointURL = gophercloud.NormalizeURL(endpoint.InternalURL) + case gophercloud.AvailabilityAdmin: + endpointURL = gophercloud.NormalizeURL(endpoint.AdminURL) + default: + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } - // Extract the appropriate URL from the matching Endpoint. - for _, endpoint := range endpoints { - switch opts.Availability { - case gophercloud.AvailabilityPublic: - return gophercloud.NormalizeURL(endpoint.PublicURL), nil - case gophercloud.AvailabilityInternal: - return gophercloud.NormalizeURL(endpoint.InternalURL), nil - case gophercloud.AvailabilityAdmin: - return gophercloud.NormalizeURL(endpoint.AdminURL), nil - default: - err := &ErrInvalidAvailabilityProvided{} - err.Argument = "Availability" - err.Value = opts.Availability - return "", err + return endpointURL, nil + } } } @@ -70,41 +68,35 @@ will also often need to specify a Name and/or a Region depending on what's available on your OpenStack deployment. */ func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + if opts.Availability != gophercloud.AvailabilityAdmin && + opts.Availability != gophercloud.AvailabilityPublic && + opts.Availability != gophercloud.AvailabilityInternal { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + // Extract Endpoints from the catalog entries that match the requested Type, Interface, // Name if provided, and Region if provided. - var endpoints = make([]tokens3.Endpoint, 0, 1) + // + // If multiple endpoints are found, we return the first result and disregard the rest. + // This behavior matches the Python library. See GH-1764. for _, entry := range catalog.Entries { - if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { for _, endpoint := range entry.Endpoints { - if opts.Availability != gophercloud.AvailabilityAdmin && - opts.Availability != gophercloud.AvailabilityPublic && - opts.Availability != gophercloud.AvailabilityInternal { - err := &ErrInvalidAvailabilityProvided{} - err.Argument = "Availability" - err.Value = opts.Availability - return "", err + if opts.Availability != gophercloud.Availability(endpoint.Interface) { + continue } - if (opts.Availability == gophercloud.Availability(endpoint.Interface)) && - (opts.Region == "" || endpoint.Region == opts.Region || endpoint.RegionID == opts.Region) { - endpoints = append(endpoints, endpoint) + if opts.Region != "" && endpoint.Region != opts.Region && endpoint.RegionID != opts.Region { + continue } + + return gophercloud.NormalizeURL(endpoint.URL), nil } } } - // If multiple endpoints were found, use the first result - // and disregard the other endpoints. - // - // This behavior matches the Python library. See GH-1764. - if len(endpoints) > 1 { - endpoints = endpoints[0:1] - } - - // Extract the URL from the matching Endpoint. - for _, endpoint := range endpoints { - return gophercloud.NormalizeURL(endpoint.URL), nil - } - // Report an error if there were no matching endpoints. err := &gophercloud.ErrEndpointNotFound{} return "", err diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/requests.go index a08980df2c0..84a8b9df1d5 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/requests.go @@ -7,6 +7,12 @@ import ( "github.com/gophercloud/gophercloud/v2/pagination" ) +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToTenantListQuery() (string, error) +} + // ListOpts filters the Tenants that are returned by the List call. type ListOpts struct { // Marker is the ID of the last Tenant on the previous page. @@ -16,15 +22,21 @@ type ListOpts struct { Limit int `q:"limit"` } +// ToTenantListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTenantListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + // List enumerates the Tenants to which the current token has access. -func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager { +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { url := listURL(client) if opts != nil { - q, err := gophercloud.BuildQueryString(opts) + query, err := opts.ToTenantListQuery() if err != nil { return pagination.Pager{Err: err} } - url += q.String() + url += query } return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { return TenantPage{pagination.LinkedPageBase{PageResult: r}} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/requests.go index 8c66b36e20c..0b23269ffad 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/requests.go @@ -214,6 +214,12 @@ func GetConsumer(ctx context.Context, client *gophercloud.ServiceClient, id stri return } +// UpdateConsumerOptsBuilder allows extensions to add additional parameters to the +// UpdateConsumer request. +type UpdateConsumerOptsBuilder interface { + ToOAuth1UpdateConsumerMap() (map[string]any, error) +} + // UpdateConsumerOpts provides options used to update a consumer. type UpdateConsumerOpts struct { // Description is the consumer description. @@ -227,7 +233,7 @@ func (opts UpdateConsumerOpts) ToOAuth1UpdateConsumerMap() (map[string]any, erro } // UpdateConsumer updates an existing Consumer. -func UpdateConsumer(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateConsumerOpts) (r UpdateConsumerResult) { +func UpdateConsumer(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateConsumerOptsBuilder) (r UpdateConsumerResult) { b, err := opts.ToOAuth1UpdateConsumerMap() if err != nil { r.Err = err diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/requests.go index 62a4f179eef..ab0b22c6bc5 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/requests.go @@ -263,6 +263,12 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts U return } +// CreateRuleOptsBuilder allows extensions to add additional parameters to the +// CreateRule request. +type CreateRuleOptsBuilder interface { + ToRuleCreateMap() (map[string]any, error) +} + // CreateRuleOpts is the common options struct used in this package's CreateRule // operation. type CreateRuleOpts struct { @@ -300,7 +306,7 @@ func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]any, error) { } // CreateRule will create and associate a Rule with a particular L7Policy. -func CreateRule(ctx context.Context, c *gophercloud.ServiceClient, policyID string, opts CreateRuleOpts) (r CreateRuleResult) { +func CreateRule(ctx context.Context, c *gophercloud.ServiceClient, policyID string, opts CreateRuleOptsBuilder) (r CreateRuleResult) { b, err := opts.ToRuleCreateMap() if err != nil { r.Err = err diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/requests.go index 3216fbddd04..abd5d089701 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/requests.go @@ -380,7 +380,7 @@ func (opts UpdateOpts) ToListenerUpdateMap() (map[string]any, error) { // Update is an operation which modifies the attributes of the specified // Listener. -func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { b, err := opts.ToListenerUpdateMap() if err != nil { r.Err = err diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/requests.go index f815806f398..095170edd37 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/requests.go @@ -208,7 +208,7 @@ func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]any, error) { // Update is an operation which modifies the attributes of the specified // LoadBalancer. -func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOpts) (r UpdateResult) { +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { b, err := opts.ToLoadBalancerUpdateMap() if err != nil { r.Err = err diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/requests.go index be5701c5f4f..15a503badc4 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/requests.go @@ -2,6 +2,7 @@ package monitors import ( "context" + "strconv" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" @@ -153,7 +154,25 @@ type CreateOpts struct { // ToMonitorCreateMap builds a request body from CreateOpts. func (opts CreateOpts) ToMonitorCreateMap() (map[string]any, error) { - return gophercloud.BuildRequestBody(opts, "healthmonitor") + b, err := gophercloud.BuildRequestBody(opts, "healthmonitor") + if err != nil { + return nil, err + } + + if v, ok := b["healthmonitor"]; ok { + if m, ok := v.(map[string]any); ok { + if v, ok := m["http_version"]; ok { + if v, ok := v.(string); ok { + m["http_version"], err = strconv.ParseFloat(v, 64) + if err != nil { + return nil, err + } + } + } + } + } + + return b, nil } /* @@ -247,7 +266,25 @@ type UpdateOpts struct { // ToMonitorUpdateMap builds a request body from UpdateOpts. func (opts UpdateOpts) ToMonitorUpdateMap() (map[string]any, error) { - return gophercloud.BuildRequestBody(opts, "healthmonitor") + b, err := gophercloud.BuildRequestBody(opts, "healthmonitor") + if err != nil { + return nil, err + } + + if v, ok := b["healthmonitor"]; ok { + if m, ok := v.(map[string]any); ok { + if v, ok := m["http_version"]; ok { + if v, ok := v.(string); ok { + m["http_version"], err = strconv.ParseFloat(v, 64) + if err != nil { + return nil, err + } + } + } + } + } + + return b, nil } // Update is an operation which modifies the attributes of the specified diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/results.go index 644ef187003..6e8563faaa1 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/results.go @@ -1,6 +1,9 @@ package monitors import ( + "encoding/json" + "strconv" + "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" ) @@ -61,7 +64,7 @@ type Monitor struct { HTTPMethod string `json:"http_method"` // The HTTP version that the monitor uses for requests. - HTTPVersion string `json:"http_version"` + HTTPVersion string `json:"-"` // The HTTP path of the request sent by the monitor to test the health of a // member. Must be a string beginning with a forward slash (/). @@ -96,6 +99,26 @@ type Monitor struct { Tags []string `json:"tags"` } +func (r *Monitor) UnmarshalJSON(b []byte) error { + type tmp Monitor + var s struct { + tmp + HTTPVersion float64 `json:"http_version"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Monitor(s.tmp) + if s.HTTPVersion != 0 { + r.HTTPVersion = strconv.FormatFloat(s.HTTPVersion, 'f', 1, 64) + } + + return nil +} + // MonitorPage is the page returned by a pager when traversing over a // collection of health monitors. type MonitorPage struct { diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/constants.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/constants.go new file mode 100644 index 00000000000..85dff7818c3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/constants.go @@ -0,0 +1,7 @@ +package floatingips + +const ( + StatusActive = "ACTIVE" + StatusDown = "DOWN" + StatusError = "ERROR" +) diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/requests.go index a3afb0403c9..be8949d6939 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/requests.go @@ -2,6 +2,7 @@ package floatingips import ( "context" + "fmt" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" @@ -37,6 +38,7 @@ type ListOpts struct { TagsAny string `q:"tags-any"` NotTags string `q:"not-tags"` NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` } // ToNetworkListQuery formats a ListOpts into a query string. @@ -144,6 +146,11 @@ type UpdateOpts struct { Description *string `json:"description,omitempty"` PortID *string `json:"port_id,omitempty"` FixedIP string `json:"fixed_ip_address,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` } // ToFloatingIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder @@ -171,8 +178,19 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts U r.Err = err return } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, + MoreHeaders: h, + OkCodes: []int{200}, }) _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/results.go index 50740ebf300..7ea6160032c 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/results.go @@ -56,6 +56,9 @@ type FloatingIP struct { // Tags optionally set via extensions/attributestags Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` } func (r *FloatingIP) UnmarshalJSON(b []byte) error { diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/requests.go index dcc7977a460..def4699db3c 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/requests.go @@ -2,33 +2,50 @@ package routers import ( "context" + "fmt" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" ) +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToRouterListQuery() (string, error) +} + // ListOpts allows the filtering and sorting of paginated collections through // the API. Filtering is achieved by passing in struct field values that map to // the floating IP attributes you want to see returned. SortKey allows you to // sort by a particular network attribute. SortDir sets the direction, and is // either `asc' or `desc'. Marker and Limit are used for pagination. type ListOpts struct { - ID string `q:"id"` - Name string `q:"name"` - Description string `q:"description"` - AdminStateUp *bool `q:"admin_state_up"` - Distributed *bool `q:"distributed"` - Status string `q:"status"` - TenantID string `q:"tenant_id"` - ProjectID string `q:"project_id"` - Limit int `q:"limit"` - Marker string `q:"marker"` - SortKey string `q:"sort_key"` - SortDir string `q:"sort_dir"` - Tags string `q:"tags"` - TagsAny string `q:"tags-any"` - NotTags string `q:"not-tags"` - NotTagsAny string `q:"not-tags-any"` + ID string `q:"id"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + Distributed *bool `q:"distributed"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` +} + +// ToRouterListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRouterListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return "", err + } + return q.String(), nil } // List returns a Pager which allows you to iterate over a collection of @@ -37,13 +54,16 @@ type ListOpts struct { // // Default policy settings return only those routers that are owned by the // tenant who submits the request, unless an admin user submits the request. -func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { - q, err := gophercloud.BuildQueryString(&opts) - if err != nil { - return pagination.Pager{Err: err} +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToRouterListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query } - u := rootURL(c) + q.String() - return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { return RouterPage{pagination.LinkedPageBase{PageResult: r}} }) } @@ -112,6 +132,11 @@ type UpdateOpts struct { Distributed *bool `json:"distributed,omitempty"` GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` Routes *[]Route `json:"routes,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` } // ToRouterUpdateMap builds an update body based on UpdateOpts. @@ -130,8 +155,19 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts U r.Err = err return } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, + MoreHeaders: h, + OkCodes: []int{200}, }) _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/results.go index 25e8ab7b597..d657160ba25 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/results.go @@ -21,7 +21,7 @@ type GatewayInfo struct { // router. type ExternalFixedIP struct { IPAddress string `json:"ip_address,omitempty"` - SubnetID string `json:"subnet_id"` + SubnetID string `json:"subnet_id,omitempty"` } // Route is a possible route in a router. @@ -77,6 +77,53 @@ type Router struct { // Tags optionally set via extensions/attributestags Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` + + // Timestamp when the router was created + CreatedAt time.Time `json:"-"` + + // Timestamp when the router was last updated + UpdatedAt time.Time `json:"-"` +} + +func (r *Router) UnmarshalJSON(b []byte) error { + type tmp Router + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Router(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Router(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil } // RouterPage is the page returned by a pager when traversing over a @@ -113,11 +160,14 @@ func (r RouterPage) IsEmpty() (bool, error) { // and extracts the elements into a slice of Router structs. In other words, // a generic collection is mapped into a relevant slice. func ExtractRouters(r pagination.Page) ([]Router, error) { - var s struct { - Routers []Router `json:"routers"` - } - err := (r.(RouterPage)).ExtractInto(&s) - return s.Routers, err + var s []Router + err := ExtractRoutersInto(r, &s) + return s, err +} + +// ExtractRoutersInto extracts the elements into a slice of Router structs. +func ExtractRoutersInto(r pagination.Page, v any) error { + return r.(RouterPage).Result.ExtractIntoSlicePtr(v, "routers") } type commonResult struct { diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/requests.go index fabb744114f..95f7bfabdfd 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/requests.go @@ -2,6 +2,7 @@ package groups import ( "context" + "fmt" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" @@ -13,20 +14,21 @@ import ( // sort by a particular network attribute. SortDir sets the direction, and is // either `asc' or `desc'. Marker and Limit are used for pagination. type ListOpts struct { - ID string `q:"id"` - Name string `q:"name"` - Description string `q:"description"` - Stateful *bool `q:"stateful"` - TenantID string `q:"tenant_id"` - ProjectID string `q:"project_id"` - Limit int `q:"limit"` - Marker string `q:"marker"` - SortKey string `q:"sort_key"` - SortDir string `q:"sort_dir"` - Tags string `q:"tags"` - TagsAny string `q:"tags-any"` - NotTags string `q:"not-tags"` - NotTagsAny string `q:"not-tags-any"` + ID string `q:"id"` + Name string `q:"name"` + Description string `q:"description"` + Stateful *bool `q:"stateful"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` } // List returns a Pager which allows you to iterate over a collection of @@ -104,6 +106,11 @@ type UpdateOpts struct { // Stateful indicates if the security group is stateful or stateless. Stateful *bool `json:"stateful,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` } // ToSecGroupUpdateMap builds a request body from UpdateOpts. @@ -118,9 +125,20 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts U r.Err = err return } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, + MoreHeaders: h, + OkCodes: []int{200}, }) _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/results.go index b3aa2efb48d..6a8a16fa869 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/results.go @@ -41,6 +41,9 @@ type SecGroup struct { // Tags optionally set via extensions/attributestags Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` } func (r *SecGroup) UnmarshalJSON(b []byte) error { diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/requests.go index 8976224b42c..edd253f037a 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/requests.go @@ -7,40 +7,60 @@ import ( "github.com/gophercloud/gophercloud/v2/pagination" ) +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToSecGroupListQuery() (string, error) +} + // ListOpts allows the filtering and sorting of paginated collections through // the API. Filtering is achieved by passing in struct field values that map to // the security group rule attributes you want to see returned. SortKey allows // you to sort by a particular network attribute. SortDir sets the direction, // and is either `asc' or `desc'. Marker and Limit are used for pagination. type ListOpts struct { - Direction string `q:"direction"` - EtherType string `q:"ethertype"` - ID string `q:"id"` - Description string `q:"description"` - PortRangeMax int `q:"port_range_max"` - PortRangeMin int `q:"port_range_min"` - Protocol string `q:"protocol"` - RemoteGroupID string `q:"remote_group_id"` - RemoteIPPrefix string `q:"remote_ip_prefix"` - SecGroupID string `q:"security_group_id"` - TenantID string `q:"tenant_id"` - ProjectID string `q:"project_id"` - Limit int `q:"limit"` - Marker string `q:"marker"` - SortKey string `q:"sort_key"` - SortDir string `q:"sort_dir"` + Direction string `q:"direction"` + EtherType string `q:"ethertype"` + ID string `q:"id"` + Description string `q:"description"` + PortRangeMax int `q:"port_range_max"` + PortRangeMin int `q:"port_range_min"` + Protocol string `q:"protocol"` + RemoteAddressGroupID string `q:"remote_address_group_id"` + RemoteGroupID string `q:"remote_group_id"` + RemoteIPPrefix string `q:"remote_ip_prefix"` + SecGroupID string `q:"security_group_id"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + RevisionNumber *int `q:"revision_number"` +} + +// ToSecGroupListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSecGroupListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return "", err + } + return q.String(), nil } // List returns a Pager which allows you to iterate over a collection of // security group rules. It accepts a ListOpts struct, which allows you to filter // and sort the returned collection for greater efficiency. -func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { - q, err := gophercloud.BuildQueryString(&opts) - if err != nil { - return pagination.Pager{Err: err} +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToSecGroupListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query } - u := rootURL(c) + q.String() - return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { return SecGroupRulePage{pagination.LinkedPageBase{PageResult: r}} }) } @@ -105,7 +125,7 @@ type CreateOpts struct { // The maximum port number in the range that is matched by the security group // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If - // the protocol is ICMP, this value must be an ICMP type. + // the protocol is ICMP, this value must be an ICMP code. PortRangeMax int `json:"port_range_max,omitempty"` // The minimum port number in the range that is matched by the security group @@ -118,12 +138,16 @@ type CreateOpts struct { // "tcp", "udp", "icmp" or an empty string. Protocol RuleProtocol `json:"protocol,omitempty"` + // The remote address group ID to be associated with this security group rule. + // You can specify either RemoteAddressGroupID, RemoteGroupID, or RemoteIPPrefix + RemoteAddressGroupID string `json:"remote_address_group_id,omitempty"` + // The remote group ID to be associated with this security group rule. You can - // specify either RemoteGroupID or RemoteIPPrefix. + // specify either RemoteAddressGroupID,RemoteGroupID or RemoteIPPrefix. RemoteGroupID string `json:"remote_group_id,omitempty"` // The remote IP prefix to be associated with this security group rule. You can - // specify either RemoteGroupID or RemoteIPPrefix. This attribute matches the + // specify either RemoteAddressGroupID,RemoteGroupID or RemoteIPPrefix. This attribute matches the // specified IP prefix as the source IP address of the IP packet. RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"` diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/results.go index 0901ced578d..03696ac203b 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/results.go @@ -1,6 +1,9 @@ package rules import ( + "encoding/json" + "time" + "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" ) @@ -42,6 +45,10 @@ type SecGroupRule struct { // "tcp", "udp", "icmp" or an empty string. Protocol string + // The remote address group ID to be associated with this security group rule. + // You can specify either RemoteAddressGroupID, RemoteGroupID, or RemoteIPPrefix + RemoteAddressGroupID string `json:"remote_address_group_id"` + // The remote group ID to be associated with this security group rule. You // can specify either RemoteGroupID or RemoteIPPrefix. RemoteGroupID string `json:"remote_group_id"` @@ -56,6 +63,53 @@ type SecGroupRule struct { // ProjectID is the project owner of this security group rule. ProjectID string `json:"project_id"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` + + // Timestamp when the rule was created + CreatedAt time.Time `json:"-"` + + // Timestamp when the rule was last updated + UpdatedAt time.Time `json:"-"` +} + +func (r *SecGroupRule) UnmarshalJSON(b []byte) error { + type tmp SecGroupRule + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = SecGroupRule(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = SecGroupRule(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil } // SecGroupRulePage is the page returned by a pager when traversing over a diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/constants.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/constants.go new file mode 100644 index 00000000000..6bec77fa794 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/constants.go @@ -0,0 +1,9 @@ +package trunks + +const ( + StatusActive = "ACTIVE" + StatusBuild = "BUILD" + StatusDegraded = "DEGRADED" + StatusDown = "DOWN" + StatusError = "ERROR" +) diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/requests.go index a58d81ac176..ea4dba2507f 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/requests.go @@ -2,6 +2,7 @@ package trunks import ( "context" + "fmt" "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" @@ -63,21 +64,22 @@ type ListOptsBuilder interface { // by a particular trunk attribute. SortDir sets the direction, and is either // `asc' or `desc'. Marker and Limit are used for pagination. type ListOpts struct { - AdminStateUp *bool `q:"admin_state_up"` - Description string `q:"description"` - ID string `q:"id"` - Name string `q:"name"` - PortID string `q:"port_id"` + AdminStateUp *bool `q:"admin_state_up"` + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` + PortID string `q:"port_id"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + SortDir string `q:"sort_dir"` + SortKey string `q:"sort_key"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + // TODO change type to *int for consistency RevisionNumber string `q:"revision_number"` - Status string `q:"status"` - TenantID string `q:"tenant_id"` - ProjectID string `q:"project_id"` - SortDir string `q:"sort_dir"` - SortKey string `q:"sort_key"` - Tags string `q:"tags"` - TagsAny string `q:"tags-any"` - NotTags string `q:"not-tags"` - NotTagsAny string `q:"not-tags-any"` } // ToTrunkListQuery formats a ListOpts into a query string. @@ -122,6 +124,11 @@ type UpdateOpts struct { AdminStateUp *bool `json:"admin_state_up,omitempty"` Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` } func (opts UpdateOpts) ToTrunkUpdateMap() (map[string]any, error) { @@ -134,8 +141,19 @@ func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts U r.Err = err return } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } resp, err := c.Put(ctx, updateURL(c, id), body, &r.Body, &gophercloud.RequestOpts{ - OkCodes: []int{200}, + MoreHeaders: h, + OkCodes: []int{200}, }) _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) return diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/results.go index 20edcf9a88f..72efd636fb5 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/results.go @@ -81,6 +81,7 @@ type Trunk struct { // if the resource has not been updated, this field will show as null. UpdatedAt time.Time `json:"updated_at"` + // RevisionNumber optionally set via extensions/standard-attr-revisions RevisionNumber int `json:"revision_number"` // UUID of the trunk's parent port diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/constants.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/constants.go new file mode 100644 index 00000000000..1214ce9debf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/constants.go @@ -0,0 +1,8 @@ +package networks + +const ( + StatusActive = "ACTIVE" + StatusBuild = "BUILD" + StatusDown = "DOWN" + StatusError = "ERROR" +) diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/requests.go index 29d14187ab5..d4dd64ff931 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/requests.go @@ -20,22 +20,23 @@ type ListOptsBuilder interface { // by a particular network attribute. SortDir sets the direction, and is either // `asc' or `desc'. Marker and Limit are used for pagination. type ListOpts struct { - Status string `q:"status"` - Name string `q:"name"` - Description string `q:"description"` - AdminStateUp *bool `q:"admin_state_up"` - TenantID string `q:"tenant_id"` - ProjectID string `q:"project_id"` - Shared *bool `q:"shared"` - ID string `q:"id"` - Marker string `q:"marker"` - Limit int `q:"limit"` - SortKey string `q:"sort_key"` - SortDir string `q:"sort_dir"` - Tags string `q:"tags"` - TagsAny string `q:"tags-any"` - NotTags string `q:"not-tags"` - NotTagsAny string `q:"not-tags-any"` + Status string `q:"status"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Shared *bool `q:"shared"` + ID string `q:"id"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` } // ToNetworkListQuery formats a ListOpts into a query string. diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/constants.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/constants.go new file mode 100644 index 00000000000..6275839bf49 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/constants.go @@ -0,0 +1,8 @@ +package ports + +const ( + StatusActive = "ACTIVE" + StatusBuild = "BUILD" + StatusDown = "DOWN" + StatusError = "ERROR" +) diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/requests.go index 218c2897f70..bfff2dffb20 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/requests.go @@ -41,6 +41,7 @@ type ListOpts struct { TagsAny string `q:"tags-any"` NotTags string `q:"not-tags"` NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` SecurityGroups []string `q:"security_groups"` FixedIPs []FixedIPOpts } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/results.go index 74a0fa3b492..db223d48c1d 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/results.go @@ -49,7 +49,7 @@ type DeleteResult struct { // IP is a sub-struct that represents an individual IP. type IP struct { - SubnetID string `json:"subnet_id"` + SubnetID string `json:"subnet_id,omitempty"` IPAddress string `json:"ip_address,omitempty"` } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/requests.go index db597d6864c..85c5d2b402b 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/requests.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/requests.go @@ -42,6 +42,8 @@ type ListOpts struct { TagsAny string `q:"tags-any"` NotTags string `q:"not-tags"` NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` + SegmentID string `q:"segment_id"` } // ToSubnetListQuery formats a ListOpts into a query string. @@ -146,6 +148,10 @@ type CreateOpts struct { // Prefixlen is used when user creates a subnet from the subnetpool. It will // overwrite the "default_prefixlen" value of the referenced subnetpool. Prefixlen int `json:"prefixlen,omitempty"` + + // SegmentID is a network segment the subnet is associated with. It is + // available when segment extension is enabled. + SegmentID string `json:"segment_id,omitempty"` } // ToSubnetCreateMap builds a request body from CreateOpts. @@ -193,9 +199,8 @@ type UpdateOpts struct { // AllocationPools are IP Address pools that will be available for DHCP. AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` - // GatewayIP sets gateway information for the subnet. Setting to nil will - // cause a default gateway to automatically be created. Setting to an empty - // string will cause the subnet to be created with no gateway. Setting to + // GatewayIP sets gateway information for the subnet. Setting to an empty + // string will cause the subnet to not have a gateway. Setting to // an explicit address will set that address as the gateway. GatewayIP *string `json:"gateway_ip,omitempty"` @@ -218,6 +223,10 @@ type UpdateOpts struct { // will set revision_number=%s. If the revision number does not match, the // update will fail. RevisionNumber *int `json:"-" h:"If-Match"` + + // SegmentID is a network segment the subnet is associated with. It is + // available when segment extension is enabled. + SegmentID *string `json:"segment_id,omitempty"` } // ToSubnetUpdateMap builds a request body from UpdateOpts. diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/results.go index 7d5ba13cc59..4f0aa8408dd 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/results.go @@ -1,6 +1,9 @@ package subnets import ( + "encoding/json" + "time" + "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" ) @@ -121,6 +124,54 @@ type Subnet struct { // RevisionNumber optionally set via extensions/standard-attr-revisions RevisionNumber int `json:"revision_number"` + + // SegmentID of a network segment the subnet is associated with. It is + // available when segment extension is enabled. + SegmentID string `json:"segment_id"` + + // Timestamp when the subnet was created + CreatedAt time.Time `json:"-"` + + // Timestamp when the subnet was last updated + UpdatedAt time.Time `json:"-"` +} + +func (r *Subnet) UnmarshalJSON(b []byte) error { + type tmp Subnet + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Subnet(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Subnet(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil } // SubnetPage is the page returned by a pager when traversing over a collection diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/base_endpoint.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/base_endpoint.go index 40080f7af20..f219c0bf4df 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/base_endpoint.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/base_endpoint.go @@ -6,9 +6,7 @@ import ( "strings" ) -// BaseEndpoint will return a URL without the /vX.Y -// portion of the URL. -func BaseEndpoint(endpoint string) (string, error) { +func parseEndpoint(endpoint string, includeVersion bool) (string, error) { u, err := url.Parse(endpoint) if err != nil { return "", err @@ -21,8 +19,23 @@ func BaseEndpoint(endpoint string) (string, error) { if version := versionRe.FindString(path); version != "" { versionIndex := strings.Index(path, version) + if includeVersion { + versionIndex += len(version) + } u.Path = path[:versionIndex] } return u.String(), nil } + +// BaseEndpoint will return a URL without the /vX.Y +// portion of the URL. +func BaseEndpoint(endpoint string) (string, error) { + return parseEndpoint(endpoint, false) +} + +// BaseVersionedEndpoint will return a URL with the /vX.Y portion of the URL, +// if present, but without a project ID or similar +func BaseVersionedEndpoint(endpoint string) (string, error) { + return parseEndpoint(endpoint, true) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/choose_version.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/choose_version.go index 6c720e57ef9..ccc56345a67 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/choose_version.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/choose_version.go @@ -3,7 +3,6 @@ package utils import ( "context" "fmt" - "strconv" "strings" "github.com/gophercloud/gophercloud/v2" @@ -29,6 +28,7 @@ var goodStatus = map[string]bool{ // It returns the highest-Priority Version, OR exact match with client endpoint, // among the alternatives that are provided, as well as its corresponding endpoint. func ChooseVersion(ctx context.Context, client *gophercloud.ProviderClient, recognized []*Version) (*Version, string, error) { + // TODO(stephenfin): This could be removed since we can accomplish this with GetServiceVersions now. type linkResp struct { Href string `json:"href"` Rel string `json:"rel"` @@ -114,123 +114,3 @@ func ChooseVersion(ctx context.Context, client *gophercloud.ProviderClient, reco return highest, endpoint, nil } - -type SupportedMicroversions struct { - MaxMajor int - MaxMinor int - MinMajor int - MinMinor int -} - -// GetSupportedMicroversions returns the minimum and maximum microversion that is supported by the ServiceClient Endpoint. -func GetSupportedMicroversions(ctx context.Context, client *gophercloud.ServiceClient) (SupportedMicroversions, 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"` - Versions []valueResp `json:"versions"` - } - var minVersion, maxVersion string - var supportedMicroversions SupportedMicroversions - var resp response - _, err := client.Get(ctx, client.Endpoint, &resp, &gophercloud.RequestOpts{ - OkCodes: []int{200, 300}, - }) - - if err != nil { - return supportedMicroversions, err - } - - if len(resp.Versions) > 0 { - // We are dealing with an unversioned endpoint - // We only handle the case when there is exactly one, and assume it is the correct one - if len(resp.Versions) > 1 { - return supportedMicroversions, fmt.Errorf("unversioned endpoint with multiple alternatives not supported") - } - minVersion = resp.Versions[0].MinVersion - maxVersion = resp.Versions[0].Version - } else { - minVersion = resp.Version.MinVersion - maxVersion = resp.Version.Version - } - - // Return early if the endpoint does not support microversions - if minVersion == "" && maxVersion == "" { - return supportedMicroversions, fmt.Errorf("microversions not supported by ServiceClient Endpoint") - } - - supportedMicroversions.MinMajor, supportedMicroversions.MinMinor, err = ParseMicroversion(minVersion) - if err != nil { - return supportedMicroversions, err - } - - supportedMicroversions.MaxMajor, supportedMicroversions.MaxMinor, err = ParseMicroversion(maxVersion) - if err != nil { - return supportedMicroversions, err - } - - return supportedMicroversions, nil -} - -// RequireMicroversion checks that the required microversion is supported and -// returns a ServiceClient with the microversion set. -func RequireMicroversion(ctx context.Context, client gophercloud.ServiceClient, required string) (gophercloud.ServiceClient, error) { - supportedMicroversions, err := GetSupportedMicroversions(ctx, &client) - if err != nil { - return client, fmt.Errorf("unable to determine supported microversions: %w", err) - } - supported, err := supportedMicroversions.IsSupported(required) - if err != nil { - return client, err - } - if !supported { - return client, fmt.Errorf("microversion %s not supported. Supported versions: %v", required, supportedMicroversions) - } - client.Microversion = required - return client, nil -} - -// IsSupported checks if a microversion falls in the supported interval. -// It returns true if the version is within the interval and false otherwise. -func (supported SupportedMicroversions) IsSupported(version string) (bool, error) { - // Parse the version X.Y into X and Y integers that are easier to compare. - vMajor, vMinor, err := ParseMicroversion(version) - if err != nil { - return false, err - } - - // Check that the major version number is supported. - if (vMajor < supported.MinMajor) || (vMajor > supported.MaxMajor) { - return false, nil - } - - // Check that the minor version number is supported - if (vMinor <= supported.MaxMinor) && (vMinor >= supported.MinMinor) { - return true, nil - } - - return false, nil -} - -// ParseMicroversion parses the version major.minor into separate integers major and minor. -// For example, "2.53" becomes 2 and 53. -func ParseMicroversion(version string) (major int, minor int, err 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 -} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/discovery.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/discovery.go new file mode 100644 index 00000000000..86d1d14c349 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/discovery.go @@ -0,0 +1,372 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/gophercloud/gophercloud/v2" +) + +type Status string + +const ( + StatusCurrent Status = "CURRENT" + StatusSupported Status = "SUPPORTED" + StatusDeprecated Status = "DEPRECATED" + StatusExperimental Status = "EXPERIMENTAL" + StatusUnknown Status = "" +) + +// SupportedVersion stores a normalized form of the API version data. It handles APIs that +// support microversions as well as those that do not. +type SupportedVersion struct { + // Major is the major version number of the API + Major int + // Minor is the minor version number of the API + Minor int + // Status is the status of the API + Status Status + SupportedMicroversions +} + +// SupportedMicroversions stores a normalized form of the maximum and minimum API microversions +// supported by a given service. +type SupportedMicroversions struct { + // MaxMajor is the major version number of the maximum supported API microversion + MaxMajor int + // MaxMinor is the minor version number of the maximum supported API microversion + MaxMinor int + // MinMajor is the major version number of the minimum supported API microversion + MinMajor int + // MinMinor is the minor version number of the minimum supported API microversion + MinMinor int +} + +type version struct { + ID string `json:"id"` + Status string `json:"status"` + Version string `json:"version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + MinVersion string `json:"min_version"` +} + +type response struct { + Versions []version `json:"-"` +} + +func (r *response) UnmarshalJSON(in []byte) error { + // intermediateResponse is an intermediate struct that allows us to offload the difference + // between a single version document and a multi-version document to the json parser and + // only focus on differences in the latter + type intermediateResponse struct { + ID string `json:"id"` + Version *version `json:"version"` + Versions *json.RawMessage `json:"versions"` + } + + data := intermediateResponse{} + if err := json.Unmarshal(in, &data); err != nil { + return err + } + + // case 1: we have a single enveloped version object + // + // this is the approach used by Manila for single version responses + if data.Version != nil { + r.Versions = []version{*data.Version} + return nil + } + + // case 2: we have an singly enveloped array of version objects + // + // this is the approach used by nova, cinder and glance, among others, for multi-version + // responses + if data.Versions != nil { + var versionArr []version + if err := json.Unmarshal(*data.Versions, &versionArr); err == nil { + r.Versions = versionArr + return nil + } + } + + // case 3: we have an doubly enveloped array of version objects + // + // this is the approach used by keystone and barbican, among others, for multi-version + // responses + if data.Versions != nil { + type values struct { + Values []version `json:"values"` + } + + var valuesObj values + if err := json.Unmarshal(*data.Versions, &valuesObj); err == nil { + r.Versions = valuesObj.Values + return nil + } + } + + // case 4: we have a single unenveloped version object + // + // this is the approach used by most other services for single version responses + if data.ID != "" { + r.Versions = []version{{ID: data.ID}} + return nil + } + + return fmt.Errorf("failed to unmarshal versions document: %s", in) +} + +func extractVersion(endpointURL string) (int, int, error) { + u, err := url.Parse(endpointURL) + if err != nil { + return 0, 0, err + } + + parts := strings.Split(strings.TrimRight(u.Path, "/"), "/") + if len(parts) == 0 { + return 0, 0, fmt.Errorf("expected path with version, got: %s", u.Path) + } + + // first, check the nth path element for a version string + if majorVersion, minorVersion, err := ParseVersion(parts[len(parts)-1]); err == nil { + return majorVersion, minorVersion, nil + } + + // if there are no more parts, quit + if len(parts) == 1 { + // we don't return the error message directly since it might be misleading: at this point + // we might have a *malformed* version identifier rather than *no* version identifier + return 0, 0, fmt.Errorf("failed to infer version from path: %s", u.Path) + } + + // the guidelines say we should use the currently scoped project_id from the token, but we + // don't necessarily have a token yet so we speculatively look at the (n-1)th path element + // (but only that) just as keystoneauth does + // + // https://github.com/openstack/keystoneauth/blob/master/keystoneauth1/discover.py#L1534-L1545 + if majorVersion, minorVersion, err := ParseVersion(parts[len(parts)-1]); err == nil { + return majorVersion, minorVersion, err + } + + // once again, we don't return the error message directly + return 0, 0, fmt.Errorf("failed to infer version from path: %s", u.Path) +} + +// GetServiceVersions returns the versions supported by the ServiceClient Endpoint. +// If the endpoint resolves to an unversioned discovery API, this should return one or more supported versions. +// If the endpoint resolves to a versioned discovery API, this should return exactly one supported version. +func GetServiceVersions(ctx context.Context, client *gophercloud.ProviderClient, endpointURL string, discoverVersions bool) ([]SupportedVersion, error) { + var supportedVersions []SupportedVersion + var endpointVersion *SupportedVersion + + if majorVersion, minorVersion, err := extractVersion(endpointURL); err == nil { + endpointVersion = &SupportedVersion{Major: majorVersion, Minor: minorVersion} + if !discoverVersions { + return append(supportedVersions, *endpointVersion), nil + } + } + + var resp response + _, err := client.Request(ctx, "GET", endpointURL, &gophercloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + if err != nil { + // we weren't able to find a discovery document but we have version information from the URL + if endpointVersion != nil { + return append(supportedVersions, *endpointVersion), nil + } + return supportedVersions, err + } + + versions := resp.Versions + + for _, version := range versions { + majorVersion, minorVersion, err := ParseVersion(version.ID) + if err != nil { + return supportedVersions, err + } + + status, err := ParseStatus(version.Status) + if err != nil { + return supportedVersions, err + } + + supportedVersion := SupportedVersion{ + Major: majorVersion, + Minor: minorVersion, + Status: status, + } + + // Only normalize the microversions if there are microversions to normalize + if (version.Version != "" || version.MaxVersion != "") && version.MinVersion != "" { + supportedVersion.MinMajor, supportedVersion.MinMinor, err = ParseMicroversion(version.MinVersion) + if err != nil { + return supportedVersions, err + } + + maxVersion := version.Version + if maxVersion == "" { + maxVersion = version.MaxVersion + } + supportedVersion.MaxMajor, supportedVersion.MaxMinor, err = ParseMicroversion(maxVersion) + if err != nil { + return supportedVersions, err + } + } + + supportedVersions = append(supportedVersions, supportedVersion) + } + + sort.Slice(supportedVersions, func(i, j int) bool { + return supportedVersions[i].Major > supportedVersions[j].Major || (supportedVersions[i].Major == supportedVersions[j].Major && + supportedVersions[i].Minor > supportedVersions[j].Minor) + }) + + return supportedVersions, nil +} + +// GetSupportedMicroversions returns the minimum and maximum microversion that is supported by the ServiceClient Endpoint. +func GetSupportedMicroversions(ctx context.Context, client *gophercloud.ServiceClient) (SupportedMicroversions, error) { + var supportedMicroversions SupportedMicroversions + + supportedVersions, err := GetServiceVersions(ctx, client.ProviderClient, client.Endpoint, true) + if err != nil { + return supportedMicroversions, err + } + + // If there are multiple versions then we were handed an unversioned endpoint. These don't + // provide microversion information, so we need to fail. Likewise, if there are no versions + // then something has gone wrong and we also need to fail. + if len(supportedVersions) > 1 { + return supportedMicroversions, fmt.Errorf("unversioned endpoint with multiple alternatives not supported") + } else if len(supportedVersions) == 0 { + return supportedMicroversions, fmt.Errorf("microversions not supported by endpoint") + } + + supportedMicroversions = supportedVersions[0].SupportedMicroversions + + if supportedMicroversions.MaxMajor == 0 && + supportedMicroversions.MaxMinor == 0 && + supportedMicroversions.MinMajor == 0 && + supportedMicroversions.MinMinor == 0 { + return supportedMicroversions, fmt.Errorf("microversions not supported by endpoint") + } + + return supportedMicroversions, err +} + +// RequireMicroversion checks that the required microversion is supported and +// returns a ServiceClient with the microversion set. +func RequireMicroversion(ctx context.Context, client gophercloud.ServiceClient, required string) (gophercloud.ServiceClient, error) { + supportedMicroversions, err := GetSupportedMicroversions(ctx, &client) + if err != nil { + return client, fmt.Errorf("unable to determine supported microversions: %w", err) + } + supported, err := supportedMicroversions.IsSupported(required) + if err != nil { + return client, err + } + if !supported { + return client, fmt.Errorf("microversion %s not supported. Supported versions: %v", required, supportedMicroversions) + } + client.Microversion = required + return client, nil +} + +// IsSupported checks if a microversion falls in the supported interval. +// It returns true if the version is within the interval and false otherwise. +func (supported SupportedMicroversions) IsSupported(version string) (bool, error) { + // Parse the version X.Y into X and Y integers that are easier to compare. + vMajor, vMinor, err := ParseMicroversion(version) + if err != nil { + return false, err + } + + // Check that the major version number is supported. + if (vMajor < supported.MinMajor) || (vMajor > supported.MaxMajor) { + return false, nil + } + + // Check that the minor version number is supported + if (vMinor <= supported.MaxMinor) && (vMinor >= supported.MinMinor) { + return true, nil + } + + return false, nil +} + +// ParseVersion parsed the version strings v{MAJOR} and v{MAJOR}.{MINOR} into separate integers +// major and minor. +// For example, "v2.1" becomes 2 and 1, "v3" becomes 3 and 0, and "1" becomes 1 and 0. +func ParseVersion(version string) (major, minor int, err error) { + if version == "" { + return 0, 0, fmt.Errorf("empty version provided") + } + + // We use the regex indicated by the version discovery guidelines. + // + // https://specs.openstack.org/openstack/api-sig/guidelines/consuming-catalog/version-discovery.html#inferring-version + // + // However, we diverge slightly since not all services include the 'v' prefix (glares at zaqar) + versionRe := regexp.MustCompile(`^v?(?P[0-9]+)(\.(?P[0-9]+))?$`) + + match := versionRe.FindStringSubmatch(version) + if len(match) == 0 { + return 0, 0, fmt.Errorf("invalid format: %q", version) + } + + major, err = strconv.Atoi(match[versionRe.SubexpIndex("major")]) + if err != nil { + return 0, 0, err + } + + minor = 0 + if match[versionRe.SubexpIndex("minor")] != "" { + minor, err = strconv.Atoi(match[versionRe.SubexpIndex("minor")]) + if err != nil { + return 0, 0, err + } + } + + return major, minor, nil +} + +// ParseMicroversion parses the version major.minor into separate integers major and minor. +// For example, "2.53" becomes 2 and 53. +func ParseMicroversion(version string) (major int, minor int, err 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 +} + +func ParseStatus(status string) (Status, error) { + switch strings.ToUpper(status) { + case "CURRENT", "STABLE": // keystone uses STABLE instead of CURRENT + return StatusCurrent, nil + case "SUPPORTED": + return StatusSupported, nil + case "DEPRECATED": + return StatusDeprecated, nil + case "": + return StatusUnknown, nil + default: + return StatusUnknown, fmt.Errorf("invalid status: %q", status) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go b/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go index 2d52c733266..61a18ae1981 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go @@ -13,7 +13,7 @@ import ( // DefaultUserAgent is the default User-Agent string set in the request header. const ( - DefaultUserAgent = "gophercloud/v2.5.0" + DefaultUserAgent = "gophercloud/v2.8.0" DefaultMaxBackoffRetries = 60 ) diff --git a/vendor/github.com/gophercloud/gophercloud/v2/service_client.go b/vendor/github.com/gophercloud/gophercloud/v2/service_client.go index 11b80108c3a..015c3f23394 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/service_client.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/service_client.go @@ -115,21 +115,28 @@ func (client *ServiceClient) Head(ctx context.Context, url string, opts *Request } func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { + serviceType := client.Type + switch client.Type { case "compute": opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion - case "sharev2": + case "shared-file-system", "sharev2", "share": opts.MoreHeaders["X-OpenStack-Manila-API-Version"] = client.Microversion - case "volume": + case "block-storage", "block-store", "volume", "volumev3": opts.MoreHeaders["X-OpenStack-Volume-API-Version"] = client.Microversion + // cinder should accept block-storage but (as of Dalmatian) does not + serviceType = "volume" case "baremetal": opts.MoreHeaders["X-OpenStack-Ironic-API-Version"] = client.Microversion case "baremetal-introspection": opts.MoreHeaders["X-OpenStack-Ironic-Inspector-API-Version"] = client.Microversion + case "container-infrastructure-management", "container-infrastructure", "container-infra": + // magnum should accept container-infrastructure-management but (as of Epoxy) does not + serviceType = "container-infra" } if client.Type != "" { - opts.MoreHeaders["OpenStack-API-Version"] = client.Type + " " + client.Microversion + opts.MoreHeaders["OpenStack-API-Version"] = serviceType + " " + client.Microversion } } diff --git a/vendor/github.com/gophercloud/utils/LICENSE b/vendor/github.com/gophercloud/utils/LICENSE new file mode 100644 index 00000000000..8dada3edaf5 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/vendor/github.com/gophercloud/utils/env/env.go b/vendor/github.com/gophercloud/utils/env/env.go new file mode 100644 index 00000000000..23d4516a761 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/env/env.go @@ -0,0 +1,12 @@ +//go:build !windows +// +build !windows + +package env + +import ( + "os" +) + +func Getenv(s string) string { + return os.Getenv(s) +} diff --git a/vendor/github.com/gophercloud/utils/env/env_windows.go b/vendor/github.com/gophercloud/utils/env/env_windows.go new file mode 100644 index 00000000000..7cf80ca1da3 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/env/env_windows.go @@ -0,0 +1,106 @@ +package env + +import ( + "os" + "syscall" + + "golang.org/x/sys/windows" + "golang.org/x/text/encoding/charmap" +) + +func Getenv(s string) string { + var st uint32 + env := os.Getenv(s) + if windows.GetConsoleMode(windows.Handle(syscall.Stdin), &st) == nil || + windows.GetConsoleMode(windows.Handle(syscall.Stdout), &st) == nil || + windows.GetConsoleMode(windows.Handle(syscall.Stderr), &st) == nil { + // detect windows console, should be skipped in cygwin environment + var cm charmap.Charmap + switch windows.GetACP() { + case 37: + cm = *charmap.CodePage037 + case 1047: + cm = *charmap.CodePage1047 + case 1140: + cm = *charmap.CodePage1140 + case 437: + cm = *charmap.CodePage437 + case 850: + cm = *charmap.CodePage850 + case 852: + cm = *charmap.CodePage852 + case 855: + cm = *charmap.CodePage855 + case 858: + cm = *charmap.CodePage858 + case 860: + cm = *charmap.CodePage860 + case 862: + cm = *charmap.CodePage862 + case 863: + cm = *charmap.CodePage863 + case 865: + cm = *charmap.CodePage865 + case 866: + cm = *charmap.CodePage866 + case 28591: + cm = *charmap.ISO8859_1 + case 28592: + cm = *charmap.ISO8859_2 + case 28593: + cm = *charmap.ISO8859_3 + case 28594: + cm = *charmap.ISO8859_4 + case 28595: + cm = *charmap.ISO8859_5 + case 28596: + cm = *charmap.ISO8859_6 + case 28597: + cm = *charmap.ISO8859_7 + case 28598: + cm = *charmap.ISO8859_8 + case 28599: + cm = *charmap.ISO8859_9 + case 28600: + cm = *charmap.ISO8859_10 + case 28603: + cm = *charmap.ISO8859_13 + case 28604: + cm = *charmap.ISO8859_14 + case 28605: + cm = *charmap.ISO8859_15 + case 28606: + cm = *charmap.ISO8859_16 + case 20866: + cm = *charmap.KOI8R + case 21866: + cm = *charmap.KOI8U + case 1250: + cm = *charmap.Windows1250 + case 1251: + cm = *charmap.Windows1251 + case 1252: + cm = *charmap.Windows1252 + case 1253: + cm = *charmap.Windows1253 + case 1254: + cm = *charmap.Windows1254 + case 1255: + cm = *charmap.Windows1255 + case 1256: + cm = *charmap.Windows1256 + case 1257: + cm = *charmap.Windows1257 + case 1258: + cm = *charmap.Windows1258 + case 874: + cm = *charmap.Windows874 + default: + return env + } + if v, err := cm.NewEncoder().String(env); err == nil { + return v + } + } + return env +} diff --git a/vendor/github.com/gophercloud/utils/gnocchi/client.go b/vendor/github.com/gophercloud/utils/gnocchi/client.go new file mode 100644 index 00000000000..1f73d3932bf --- /dev/null +++ b/vendor/github.com/gophercloud/utils/gnocchi/client.go @@ -0,0 +1,25 @@ +package gnocchi + +import ( + "github.com/gophercloud/gophercloud" +) + +func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string) (*gophercloud.ServiceClient, error) { + sc := new(gophercloud.ServiceClient) + eo.ApplyDefaults(clientType) + url, err := client.EndpointLocator(eo) + if err != nil { + return sc, err + } + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +// NewGnocchiV1 creates a ServiceClient that may be used with the v1 Gnocchi package. +func NewGnocchiV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "metric") + sc.ResourceBase = sc.Endpoint + "v1/" + return sc, err +} diff --git a/vendor/github.com/gophercloud/utils/gnocchi/results.go b/vendor/github.com/gophercloud/utils/gnocchi/results.go new file mode 100644 index 00000000000..f08095d3fe8 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/gnocchi/results.go @@ -0,0 +1,37 @@ +package gnocchi + +import ( + "bytes" + "encoding/json" + "time" +) + +// RFC3339NanoTimezone describes a common timestamp format used by Gnocchi API responses. +const RFC3339NanoTimezone = "2006-01-02T15:04:05.999999+00:00" + +// RFC3339NanoNoTimezone describes a common timestamp format that can be used for Gnocchi requests +// with time ranges. +const RFC3339NanoNoTimezone = "2006-01-02T15:04:05.999999" + +// JSONRFC3339NanoTimezone is a type for Gnocchi responses timestamps with a timezone offset. +type JSONRFC3339NanoTimezone time.Time + +// UnmarshalJSON helps to unmarshal timestamps from Gnocchi responses to the +// JSONRFC3339NanoTimezone type. +func (jt *JSONRFC3339NanoTimezone) UnmarshalJSON(data []byte) error { + b := bytes.NewBuffer(data) + dec := json.NewDecoder(b) + var s string + if err := dec.Decode(&s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339NanoTimezone, s) + if err != nil { + return err + } + *jt = JSONRFC3339NanoTimezone(t) + return nil +} diff --git a/vendor/github.com/gophercloud/utils/internal/pkg.go b/vendor/github.com/gophercloud/utils/internal/pkg.go new file mode 100644 index 00000000000..5bf0569ce8c --- /dev/null +++ b/vendor/github.com/gophercloud/utils/internal/pkg.go @@ -0,0 +1 @@ +package internal diff --git a/vendor/github.com/gophercloud/utils/internal/util.go b/vendor/github.com/gophercloud/utils/internal/util.go new file mode 100644 index 00000000000..887bb8f3dcc --- /dev/null +++ b/vendor/github.com/gophercloud/utils/internal/util.go @@ -0,0 +1,111 @@ +package internal + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "os" + "reflect" + "strings" + + "github.com/mitchellh/go-homedir" +) + +// RemainingKeys will inspect a struct and compare it to a map. Any struct +// field that does not have a JSON tag that matches a key in the map or +// a matching lower-case field in the map will be returned as an extra. +// +// This is useful for determining the extra fields returned in response bodies +// for resources that can contain an arbitrary or dynamic number of fields. +func RemainingKeys(s interface{}, m map[string]interface{}) (extras map[string]interface{}) { + extras = make(map[string]interface{}) + for k, v := range m { + extras[k] = v + } + + valueOf := reflect.ValueOf(s) + typeOf := reflect.TypeOf(s) + for i := 0; i < valueOf.NumField(); i++ { + field := typeOf.Field(i) + + lowerField := strings.ToLower(field.Name) + delete(extras, lowerField) + + if tagValue := field.Tag.Get("json"); tagValue != "" && tagValue != "-" { + delete(extras, tagValue) + } + } + + return +} + +// PrepareTLSConfig generates TLS config based on the specifed parameters +func PrepareTLSConfig(caCertFile, clientCertFile, clientKeyFile string, insecure *bool) (*tls.Config, error) { + config := &tls.Config{} + if caCertFile != "" { + caCert, _, err := pathOrContents(caCertFile) + if err != nil { + return nil, fmt.Errorf("Error reading CA Cert: %s", err) + } + + caCertPool := x509.NewCertPool() + if ok := caCertPool.AppendCertsFromPEM(bytes.TrimSpace(caCert)); !ok { + return nil, fmt.Errorf("Error parsing CA Cert from %s", caCertFile) + } + config.RootCAs = caCertPool + } + + if insecure == nil { + config.InsecureSkipVerify = false + } else { + config.InsecureSkipVerify = *insecure + } + + if clientCertFile != "" && clientKeyFile != "" { + clientCert, _, err := pathOrContents(clientCertFile) + if err != nil { + return nil, fmt.Errorf("Error reading Client Cert: %s", err) + } + clientKey, _, err := pathOrContents(clientKeyFile) + if err != nil { + return nil, fmt.Errorf("Error reading Client Key: %s", err) + } + + cert, err := tls.X509KeyPair(clientCert, clientKey) + if err != nil { + return nil, err + } + + config.Certificates = []tls.Certificate{cert} + config.BuildNameToCertificate() + } + + return config, nil +} + +func pathOrContents(poc string) ([]byte, bool, error) { + if len(poc) == 0 { + return nil, false, nil + } + + path := poc + if path[0] == '~' { + var err error + path, err = homedir.Expand(path) + if err != nil { + return []byte(path), true, err + } + } + + if _, err := os.Stat(path); err == nil { + contents, err := ioutil.ReadFile(path) + if err != nil { + return contents, true, err + } + return contents, true, nil + } + + return []byte(poc), false, nil +} diff --git a/vendor/github.com/gophercloud/utils/openstack/clientconfig/doc.go b/vendor/github.com/gophercloud/utils/openstack/clientconfig/doc.go new file mode 100644 index 00000000000..d052d090d82 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/openstack/clientconfig/doc.go @@ -0,0 +1,46 @@ +/* +Package clientconfig provides convienent functions for creating OpenStack +clients. It is based on the Python os-client-config library. + +See https://docs.openstack.org/os-client-config/latest for details. + +Example to Create a Provider Client From clouds.yaml + + opts := &clientconfig.ClientOpts{ + Cloud: "hawaii", + } + + pClient, err := clientconfig.AuthenticatedClient(opts) + if err != nil { + panic(err) + } + +Example to Manually Create a Provider Client + + opts := &clientconfig.ClientOpts{ + AuthInfo: &clientconfig.AuthInfo{ + AuthURL: "https://hi.example.com:5000/v3", + Username: "jdoe", + Password: "password", + ProjectName: "Some Project", + DomainName: "default", + }, + } + + pClient, err := clientconfig.AuthenticatedClient(opts) + if err != nil { + panic(err) + } + +Example to Create a Service Client from clouds.yaml + + opts := &clientconfig.ClientOpts{ + Cloud: "hawaii", + } + + computeClient, err := clientconfig.NewServiceClient("compute", opts) + if err != nil { + panic(err) + } +*/ +package clientconfig diff --git a/vendor/github.com/gophercloud/utils/openstack/clientconfig/requests.go b/vendor/github.com/gophercloud/utils/openstack/clientconfig/requests.go new file mode 100644 index 00000000000..7524dffccda --- /dev/null +++ b/vendor/github.com/gophercloud/utils/openstack/clientconfig/requests.go @@ -0,0 +1,1023 @@ +package clientconfig + +import ( + "errors" + "fmt" + "net/http" + "os" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/gophercloud/utils/env" + "github.com/gophercloud/utils/gnocchi" + "github.com/gophercloud/utils/internal" + + "github.com/hashicorp/go-uuid" + yaml "gopkg.in/yaml.v2" +) + +// AuthType respresents a valid method of authentication. +type AuthType string + +const ( + // AuthPassword defines an unknown version of the password + AuthPassword AuthType = "password" + // AuthToken defined an unknown version of the token + AuthToken AuthType = "token" + + // AuthV2Password defines version 2 of the password + AuthV2Password AuthType = "v2password" + // AuthV2Token defines version 2 of the token + AuthV2Token AuthType = "v2token" + + // AuthV3Password defines version 3 of the password + AuthV3Password AuthType = "v3password" + // AuthV3Token defines version 3 of the token + AuthV3Token AuthType = "v3token" + + // AuthV3ApplicationCredential defines version 3 of the application credential + AuthV3ApplicationCredential AuthType = "v3applicationcredential" +) + +// ClientOpts represents options to customize the way a client is +// configured. +type ClientOpts struct { + // Cloud is the cloud entry in clouds.yaml to use. + Cloud string + + // EnvPrefix allows a custom environment variable prefix to be used. + EnvPrefix string + + // AuthType specifies the type of authentication to use. + // By default, this is "password". + AuthType AuthType + + // AuthInfo defines the authentication information needed to + // authenticate to a cloud when clouds.yaml isn't used. + AuthInfo *AuthInfo + + // RegionName is the region to create a Service Client in. + // This will override a region in clouds.yaml or can be used + // when authenticating directly with AuthInfo. + RegionName string + + // EndpointType specifies whether to use the public, internal, or + // admin endpoint of a service. + EndpointType string + + // HTTPClient provides the ability customize the ProviderClient's + // internal HTTP client. + HTTPClient *http.Client + + // YAMLOpts provides the ability to pass a customized set + // of options and methods for loading the YAML file. + // It takes a YAMLOptsBuilder interface that is defined + // in this file. This is optional and the default behavior + // is to call the local LoadCloudsYAML functions defined + // in this file. + YAMLOpts YAMLOptsBuilder +} + +// YAMLOptsBuilder defines an interface for customization when +// loading a clouds.yaml file. +type YAMLOptsBuilder interface { + LoadCloudsYAML() (map[string]Cloud, error) + LoadSecureCloudsYAML() (map[string]Cloud, error) + LoadPublicCloudsYAML() (map[string]Cloud, error) +} + +// YAMLOpts represents options and methods to load a clouds.yaml file. +type YAMLOpts struct { + // By default, no options are specified. +} + +// LoadCloudsYAML defines how to load a clouds.yaml file. +// By default, this calls the local LoadCloudsYAML function. +func (opts YAMLOpts) LoadCloudsYAML() (map[string]Cloud, error) { + return LoadCloudsYAML() +} + +// LoadSecureCloudsYAML defines how to load a secure.yaml file. +// By default, this calls the local LoadSecureCloudsYAML function. +func (opts YAMLOpts) LoadSecureCloudsYAML() (map[string]Cloud, error) { + return LoadSecureCloudsYAML() +} + +// LoadPublicCloudsYAML defines how to load a public-secure.yaml file. +// By default, this calls the local LoadPublicCloudsYAML function. +func (opts YAMLOpts) LoadPublicCloudsYAML() (map[string]Cloud, error) { + return LoadPublicCloudsYAML() +} + +// LoadCloudsYAML will load a clouds.yaml file and return the full config. +// This is called by the YAMLOpts method. Calling this function directly +// is supported for now but has only been retained for backwards +// compatibility from before YAMLOpts was defined. This may be removed in +// the future. +func LoadCloudsYAML() (map[string]Cloud, error) { + _, content, err := FindAndReadCloudsYAML() + if err != nil { + return nil, err + } + + var clouds Clouds + err = yaml.Unmarshal(content, &clouds) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml: %w", err) + } + + return clouds.Clouds, nil +} + +// LoadSecureCloudsYAML will load a secure.yaml file and return the full config. +// This is called by the YAMLOpts method. Calling this function directly +// is supported for now but has only been retained for backwards +// compatibility from before YAMLOpts was defined. This may be removed in +// the future. +func LoadSecureCloudsYAML() (map[string]Cloud, error) { + var secureClouds Clouds + + _, content, err := FindAndReadSecureCloudsYAML() + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // secure.yaml is optional so just ignore read error + return secureClouds.Clouds, nil + } + return nil, err + } + + err = yaml.Unmarshal(content, &secureClouds) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml: %w", err) + } + + return secureClouds.Clouds, nil +} + +// LoadPublicCloudsYAML will load a public-clouds.yaml file and return the full config. +// This is called by the YAMLOpts method. Calling this function directly +// is supported for now but has only been retained for backwards +// compatibility from before YAMLOpts was defined. This may be removed in +// the future. +func LoadPublicCloudsYAML() (map[string]Cloud, error) { + var publicClouds PublicClouds + + _, content, err := FindAndReadPublicCloudsYAML() + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // clouds-public.yaml is optional so just ignore read error + return publicClouds.Clouds, nil + } + + return nil, err + } + + err = yaml.Unmarshal(content, &publicClouds) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml: %w", err) + } + + return publicClouds.Clouds, nil +} + +// GetCloudFromYAML will return a cloud entry from a clouds.yaml file. +func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) { + if opts == nil { + opts = new(ClientOpts) + } + + if opts.YAMLOpts == nil { + opts.YAMLOpts = new(YAMLOpts) + } + + yamlOpts := opts.YAMLOpts + + clouds, err := yamlOpts.LoadCloudsYAML() + if err != nil { + return nil, fmt.Errorf("unable to load clouds.yaml: %w", err) + } + + // Determine which cloud to use. + // First see if a cloud name was explicitly set in opts. + var cloudName string + if opts.Cloud != "" { + cloudName = opts.Cloud + } else { + // If not, see if a cloud name was specified as an environment variable. + envPrefix := "OS_" + if opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if v := env.Getenv(envPrefix + "CLOUD"); v != "" { + cloudName = v + } + } + + var cloud *Cloud + if cloudName != "" { + v, ok := clouds[cloudName] + if !ok { + return nil, fmt.Errorf("cloud %s does not exist in clouds.yaml", cloudName) + } + cloud = &v + } + + // If a cloud was not specified, and clouds only contains + // a single entry, use that entry. + if cloudName == "" && len(clouds) == 1 { + for _, v := range clouds { + cloud = &v + } + } + + if cloud != nil { + // A profile points to a public cloud entry. + // If one was specified, load a list of public clouds + // and then merge the information with the current cloud data. + profileName := defaultIfEmpty(cloud.Profile, cloud.Cloud) + + if profileName != "" { + publicClouds, err := yamlOpts.LoadPublicCloudsYAML() + if err != nil { + return nil, fmt.Errorf("unable to load clouds-public.yaml: %w", err) + } + + publicCloud, ok := publicClouds[profileName] + if !ok { + return nil, fmt.Errorf("cloud %s does not exist in clouds-public.yaml", profileName) + } + + cloud, err = mergeClouds(cloud, publicCloud) + if err != nil { + return nil, fmt.Errorf("Could not merge information from clouds.yaml and clouds-public.yaml for cloud %s", profileName) + } + } + } + + // Next, load a secure clouds file and see if a cloud entry + // can be found or merged. + secureClouds, err := yamlOpts.LoadSecureCloudsYAML() + if err != nil { + return nil, fmt.Errorf("unable to load secure.yaml: %w", err) + } + + if secureClouds != nil { + // If no entry was found in clouds.yaml, no cloud name was specified, + // and only one secureCloud entry exists, use that as the cloud entry. + if cloud == nil && cloudName == "" && len(secureClouds) == 1 { + for _, v := range secureClouds { + cloud = &v + } + } + + // Otherwise, see if the provided cloud name exists in the secure yaml file. + secureCloud, ok := secureClouds[cloudName] + if !ok && cloud == nil { + // cloud == nil serves two purposes here: + // if no entry in clouds.yaml was found and + // if a single-entry secureCloud wasn't used. + // At this point, no entry could be determined at all. + return nil, fmt.Errorf("Could not find cloud %s", cloudName) + } + + // If secureCloud has content and it differs from the cloud entry, + // merge the two together. + if !reflect.DeepEqual((Cloud{}), secureCloud) && !reflect.DeepEqual(cloud, secureCloud) { + cloud, err = mergeClouds(secureCloud, cloud) + if err != nil { + return nil, fmt.Errorf("unable to merge information from clouds.yaml and secure.yaml") + } + } + } + + // As an extra precaution, do one final check to see if cloud is nil. + // We shouldn't reach this point, though. + if cloud == nil { + return nil, fmt.Errorf("Could not find cloud %s", cloudName) + } + + // Default is to verify SSL API requests + if cloud.Verify == nil { + iTrue := true + cloud.Verify = &iTrue + } + + // merging per-region value overrides + if opts.RegionName != "" { + for _, v := range cloud.Regions { + if opts.RegionName == v.Name { + cloud, err = mergeClouds(v.Values, cloud) + break + } + } + } + + // TODO: this is where reading vendor files should go be considered when not found in + // clouds-public.yml + // https://github.com/openstack/openstacksdk/tree/master/openstack/config/vendors + + // Both Interface and EndpointType are valid settings in clouds.yaml, + // but we want to standardize on EndpointType for simplicity. + // + // If only Interface was set, we copy that to EndpointType to use as the setting. + // But in all other cases, EndpointType is used and Interface is cleared. + if cloud.Interface != "" && cloud.EndpointType == "" { + cloud.EndpointType = cloud.Interface + } + + cloud.Interface = "" + + return cloud, nil +} + +// AuthOptions creates a gophercloud.AuthOptions structure with the +// settings found in a specific cloud entry of a clouds.yaml file or +// based on authentication settings given in ClientOpts. +// +// This attempts to be a single point of entry for all OpenStack authentication. +// +// See http://docs.openstack.org/developer/os-client-config and +// https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py. +func AuthOptions(opts *ClientOpts) (*gophercloud.AuthOptions, error) { + cloud := new(Cloud) + + // If no opts were passed in, create an empty ClientOpts. + if opts == nil { + opts = new(ClientOpts) + } + + // Determine if a clouds.yaml entry should be retrieved. + // Start by figuring out the cloud name. + // First check if one was explicitly specified in opts. + var cloudName string + if opts.Cloud != "" { + cloudName = opts.Cloud + } else { + // If not, see if a cloud name was specified as an environment + // variable. + envPrefix := "OS_" + if opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if v := env.Getenv(envPrefix + "CLOUD"); v != "" { + cloudName = v + } + } + + // If a cloud name was determined, try to look it up in clouds.yaml. + if cloudName != "" { + // Get the requested cloud. + var err error + cloud, err = GetCloudFromYAML(opts) + if err != nil { + return nil, err + } + } + + // If cloud.AuthInfo is nil, then no cloud was specified. + if cloud.AuthInfo == nil { + // If opts.AuthInfo is not nil, then try using the auth settings from it. + if opts.AuthInfo != nil { + cloud.AuthInfo = opts.AuthInfo + } + + // If cloud.AuthInfo is still nil, then set it to an empty Auth struct + // and rely on environment variables to do the authentication. + if cloud.AuthInfo == nil { + cloud.AuthInfo = new(AuthInfo) + } + } + + identityAPI := determineIdentityAPI(cloud, opts) + switch identityAPI { + case "2.0", "2": + return v2auth(cloud, opts) + case "3": + return v3auth(cloud, opts) + } + + return nil, fmt.Errorf("Unable to build AuthOptions") +} + +func determineIdentityAPI(cloud *Cloud, opts *ClientOpts) string { + var identityAPI string + if cloud.IdentityAPIVersion != "" { + identityAPI = cloud.IdentityAPIVersion + } + + envPrefix := "OS_" + if opts != nil && opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if v := env.Getenv(envPrefix + "IDENTITY_API_VERSION"); v != "" { + identityAPI = v + } + + if identityAPI == "" { + if cloud.AuthInfo != nil { + if strings.Contains(cloud.AuthInfo.AuthURL, "v2.0") { + identityAPI = "2.0" + } + + if strings.Contains(cloud.AuthInfo.AuthURL, "v3") { + identityAPI = "3" + } + } + } + + if identityAPI == "" { + switch cloud.AuthType { + case AuthV2Password: + identityAPI = "2.0" + case AuthV2Token: + identityAPI = "2.0" + case AuthV3Password: + identityAPI = "3" + case AuthV3Token: + identityAPI = "3" + case AuthV3ApplicationCredential: + identityAPI = "3" + } + } + + // If an Identity API version could not be determined, + // default to v3. + if identityAPI == "" { + identityAPI = "3" + } + + return identityAPI +} + +// v2auth creates a v2-compatible gophercloud.AuthOptions struct. +func v2auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) { + // Environment variable overrides. + envPrefix := "OS_" + if opts != nil && opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if cloud.AuthInfo.AuthURL == "" { + if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" { + cloud.AuthInfo.AuthURL = v + } + } + + if cloud.AuthInfo.Token == "" { + if v := env.Getenv(envPrefix + "TOKEN"); v != "" { + cloud.AuthInfo.Token = v + } + + if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" { + cloud.AuthInfo.Token = v + } + } + + if cloud.AuthInfo.Username == "" { + if v := env.Getenv(envPrefix + "USERNAME"); v != "" { + cloud.AuthInfo.Username = v + } + } + + if cloud.AuthInfo.Password == "" { + if v := env.Getenv(envPrefix + "PASSWORD"); v != "" { + cloud.AuthInfo.Password = v + } + } + + if cloud.AuthInfo.ProjectID == "" { + if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" { + cloud.AuthInfo.ProjectID = v + } + + if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" { + cloud.AuthInfo.ProjectID = v + } + } + + if cloud.AuthInfo.ProjectName == "" { + if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" { + cloud.AuthInfo.ProjectName = v + } + + if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" { + cloud.AuthInfo.ProjectName = v + } + } + + ao := &gophercloud.AuthOptions{ + IdentityEndpoint: cloud.AuthInfo.AuthURL, + TokenID: cloud.AuthInfo.Token, + Username: cloud.AuthInfo.Username, + Password: cloud.AuthInfo.Password, + TenantID: cloud.AuthInfo.ProjectID, + TenantName: cloud.AuthInfo.ProjectName, + AllowReauth: cloud.AuthInfo.AllowReauth, + } + + return ao, nil +} + +// v3auth creates a v3-compatible gophercloud.AuthOptions struct. +func v3auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) { + // Environment variable overrides. + envPrefix := "OS_" + if opts != nil && opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if cloud.AuthInfo.AuthURL == "" { + if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" { + cloud.AuthInfo.AuthURL = v + } + } + + if cloud.AuthInfo.Token == "" { + if v := env.Getenv(envPrefix + "TOKEN"); v != "" { + cloud.AuthInfo.Token = v + } + + if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" { + cloud.AuthInfo.Token = v + } + } + + if cloud.AuthInfo.Username == "" { + if v := env.Getenv(envPrefix + "USERNAME"); v != "" { + cloud.AuthInfo.Username = v + } + } + + if cloud.AuthInfo.UserID == "" { + if v := env.Getenv(envPrefix + "USER_ID"); v != "" { + cloud.AuthInfo.UserID = v + } + } + + if cloud.AuthInfo.Password == "" { + if v := env.Getenv(envPrefix + "PASSWORD"); v != "" { + cloud.AuthInfo.Password = v + } + } + + if cloud.AuthInfo.ProjectID == "" { + if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" { + cloud.AuthInfo.ProjectID = v + } + + if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" { + cloud.AuthInfo.ProjectID = v + } + } + + if cloud.AuthInfo.ProjectName == "" { + if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" { + cloud.AuthInfo.ProjectName = v + } + + if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" { + cloud.AuthInfo.ProjectName = v + } + } + + if cloud.AuthInfo.DomainID == "" { + if v := env.Getenv(envPrefix + "DOMAIN_ID"); v != "" { + cloud.AuthInfo.DomainID = v + } + } + + if cloud.AuthInfo.DomainName == "" { + if v := env.Getenv(envPrefix + "DOMAIN_NAME"); v != "" { + cloud.AuthInfo.DomainName = v + } + } + + if cloud.AuthInfo.DefaultDomain == "" { + if v := env.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" { + cloud.AuthInfo.DefaultDomain = v + } + } + + if cloud.AuthInfo.ProjectDomainID == "" { + if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" { + cloud.AuthInfo.ProjectDomainID = v + } + } + + if cloud.AuthInfo.ProjectDomainName == "" { + if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" { + cloud.AuthInfo.ProjectDomainName = v + } + } + + if cloud.AuthInfo.UserDomainID == "" { + if v := env.Getenv(envPrefix + "USER_DOMAIN_ID"); v != "" { + cloud.AuthInfo.UserDomainID = v + } + } + + if cloud.AuthInfo.UserDomainName == "" { + if v := env.Getenv(envPrefix + "USER_DOMAIN_NAME"); v != "" { + cloud.AuthInfo.UserDomainName = v + } + } + + if cloud.AuthInfo.ApplicationCredentialID == "" { + if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_ID"); v != "" { + cloud.AuthInfo.ApplicationCredentialID = v + } + } + + if cloud.AuthInfo.ApplicationCredentialName == "" { + if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_NAME"); v != "" { + cloud.AuthInfo.ApplicationCredentialName = v + } + } + + if cloud.AuthInfo.ApplicationCredentialSecret == "" { + if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_SECRET"); v != "" { + cloud.AuthInfo.ApplicationCredentialSecret = v + } + } + + if cloud.AuthInfo.SystemScope == "" { + if v := env.Getenv(envPrefix + "SYSTEM_SCOPE"); v != "" { + cloud.AuthInfo.SystemScope = v + } + } + + // Build a scope and try to do it correctly. + // https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py#L595 + scope := new(gophercloud.AuthScope) + + // Application credentials don't support scope + if isApplicationCredential(cloud.AuthInfo) { + // If Domain* is set, but UserDomain* or ProjectDomain* aren't, + // then use Domain* as the default setting. + cloud = setDomainIfNeeded(cloud) + } else { + if !isProjectScoped(cloud.AuthInfo) { + if cloud.AuthInfo.DomainID != "" { + scope.DomainID = cloud.AuthInfo.DomainID + } else if cloud.AuthInfo.DomainName != "" { + scope.DomainName = cloud.AuthInfo.DomainName + } + if cloud.AuthInfo.SystemScope != "" { + scope.System = true + } + } else { + // If Domain* is set, but UserDomain* or ProjectDomain* aren't, + // then use Domain* as the default setting. + cloud = setDomainIfNeeded(cloud) + + if cloud.AuthInfo.ProjectID != "" { + scope.ProjectID = cloud.AuthInfo.ProjectID + } else { + scope.ProjectName = cloud.AuthInfo.ProjectName + scope.DomainID = cloud.AuthInfo.ProjectDomainID + scope.DomainName = cloud.AuthInfo.ProjectDomainName + } + } + } + + ao := &gophercloud.AuthOptions{ + Scope: scope, + IdentityEndpoint: cloud.AuthInfo.AuthURL, + TokenID: cloud.AuthInfo.Token, + Username: cloud.AuthInfo.Username, + UserID: cloud.AuthInfo.UserID, + Password: cloud.AuthInfo.Password, + TenantID: cloud.AuthInfo.ProjectID, + TenantName: cloud.AuthInfo.ProjectName, + DomainID: cloud.AuthInfo.UserDomainID, + DomainName: cloud.AuthInfo.UserDomainName, + ApplicationCredentialID: cloud.AuthInfo.ApplicationCredentialID, + ApplicationCredentialName: cloud.AuthInfo.ApplicationCredentialName, + ApplicationCredentialSecret: cloud.AuthInfo.ApplicationCredentialSecret, + AllowReauth: cloud.AuthInfo.AllowReauth, + } + + // If an auth_type of "token" was specified, then make sure + // Gophercloud properly authenticates with a token. This involves + // unsetting a few other auth options. The reason this is done + // here is to wait until all auth settings (both in clouds.yaml + // and via environment variables) are set and then unset them. + if strings.Contains(string(cloud.AuthType), "token") || ao.TokenID != "" { + ao.Username = "" + ao.Password = "" + ao.UserID = "" + ao.DomainID = "" + ao.DomainName = "" + } + + // Check for absolute minimum requirements. + if ao.IdentityEndpoint == "" { + err := gophercloud.ErrMissingInput{Argument: "auth_url"} + return nil, err + } + + return ao, nil +} + +// AuthenticatedClient is a convenience function to get a new provider client +// based on a clouds.yaml entry. +func AuthenticatedClient(opts *ClientOpts) (*gophercloud.ProviderClient, error) { + ao, err := AuthOptions(opts) + if err != nil { + return nil, err + } + + return openstack.AuthenticatedClient(*ao) +} + +// NewServiceClient is a convenience function to get a new service client. +func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceClient, error) { + cloud := new(Cloud) + + // If no opts were passed in, create an empty ClientOpts. + if opts == nil { + opts = new(ClientOpts) + } + + // Determine if a clouds.yaml entry should be retrieved. + // Start by figuring out the cloud name. + // First check if one was explicitly specified in opts. + var cloudName string + if opts.Cloud != "" { + cloudName = opts.Cloud + } + + // Next see if a cloud name was specified as an environment variable. + envPrefix := "OS_" + if opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if v := env.Getenv(envPrefix + "CLOUD"); v != "" { + cloudName = v + } + + // If a cloud name was determined, try to look it up in clouds.yaml. + if cloudName != "" { + // Get the requested cloud. + var err error + cloud, err = GetCloudFromYAML(opts) + if err != nil { + return nil, err + } + } + + // Check if a custom CA cert was provided. + // First, check if the CACERT environment variable is set. + var caCertPath string + if v := env.Getenv(envPrefix + "CACERT"); v != "" { + caCertPath = v + } + // Next, check if the cloud entry sets a CA cert. + if v := cloud.CACertFile; v != "" { + caCertPath = v + } + + // Check if a custom client cert was provided. + // First, check if the CERT environment variable is set. + var clientCertPath string + if v := env.Getenv(envPrefix + "CERT"); v != "" { + clientCertPath = v + } + // Next, check if the cloud entry sets a client cert. + if v := cloud.ClientCertFile; v != "" { + clientCertPath = v + } + + // Check if a custom client key was provided. + // First, check if the KEY environment variable is set. + var clientKeyPath string + if v := env.Getenv(envPrefix + "KEY"); v != "" { + clientKeyPath = v + } + // Next, check if the cloud entry sets a client key. + if v := cloud.ClientKeyFile; v != "" { + clientKeyPath = v + } + + // Define whether or not SSL API requests should be verified. + var insecurePtr *bool + if cloud.Verify != nil { + // Here we take the boolean pointer negation. + insecure := !*cloud.Verify + insecurePtr = &insecure + } + + tlsConfig, err := internal.PrepareTLSConfig(caCertPath, clientCertPath, clientKeyPath, insecurePtr) + if err != nil { + return nil, err + } + + // Get a Provider Client + ao, err := AuthOptions(opts) + if err != nil { + return nil, err + } + pClient, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + return nil, err + } + + // If an HTTPClient was specified, use it. + if opts.HTTPClient != nil { + pClient.HTTPClient = *opts.HTTPClient + } else { + // Otherwise create a new HTTP client with the generated TLS config. + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = tlsConfig + pClient.HTTPClient = http.Client{Transport: transport} + } + + err = openstack.Authenticate(pClient, *ao) + if err != nil { + return nil, err + } + + // Determine the region to use. + // First, check if the REGION_NAME environment variable is set. + var region string + if v := env.Getenv(envPrefix + "REGION_NAME"); v != "" { + region = v + } + + // Next, check if the cloud entry sets a region. + if v := cloud.RegionName; v != "" { + region = v + } + + // Finally, see if one was specified in the ClientOpts. + // If so, this takes precedence. + if v := opts.RegionName; v != "" { + region = v + } + + // Determine the endpoint type to use. + // First, check if the OS_INTERFACE environment variable is set. + var endpointType string + if v := env.Getenv(envPrefix + "INTERFACE"); v != "" { + endpointType = v + } + + // Next, check if the cloud entry sets an endpoint type. + if v := cloud.EndpointType; v != "" { + endpointType = v + } + + // Finally, see if one was specified in the ClientOpts. + // If so, this takes precedence. + if v := opts.EndpointType; v != "" { + endpointType = v + } + + eo := gophercloud.EndpointOpts{ + Region: region, + Availability: GetEndpointType(endpointType), + } + + switch service { + case "baremetal": + return openstack.NewBareMetalV1(pClient, eo) + case "baremetal-introspection": + return openstack.NewBareMetalIntrospectionV1(pClient, eo) + case "clustering": + return openstack.NewClusteringV1(pClient, eo) + case "compute": + return openstack.NewComputeV2(pClient, eo) + case "container": + return openstack.NewContainerV1(pClient, eo) + case "container-infra": + return openstack.NewContainerInfraV1(pClient, eo) + case "database": + return openstack.NewDBV1(pClient, eo) + case "dns": + return openstack.NewDNSV2(pClient, eo) + case "gnocchi": + return gnocchi.NewGnocchiV1(pClient, eo) + case "identity": + identityVersion := "3" + if v := cloud.IdentityAPIVersion; v != "" { + identityVersion = v + } + + switch identityVersion { + case "v2", "2", "2.0": + return openstack.NewIdentityV2(pClient, eo) + case "v3", "3": + return openstack.NewIdentityV3(pClient, eo) + default: + return nil, fmt.Errorf("invalid identity API version") + } + case "image": + return openstack.NewImageServiceV2(pClient, eo) + case "key-manager": + return openstack.NewKeyManagerV1(pClient, eo) + case "load-balancer": + return openstack.NewLoadBalancerV2(pClient, eo) + case "messaging": + clientID, err := uuid.GenerateUUID() + if err != nil { + return nil, fmt.Errorf("failed to generate UUID: %w", err) + } + return openstack.NewMessagingV2(pClient, clientID, eo) + case "network": + return openstack.NewNetworkV2(pClient, eo) + case "object-store": + return openstack.NewObjectStorageV1(pClient, eo) + case "orchestration": + return openstack.NewOrchestrationV1(pClient, eo) + case "placement": + return openstack.NewPlacementV1(pClient, eo) + case "sharev2": + return openstack.NewSharedFileSystemV2(pClient, eo) + case "volume": + volumeVersion := "3" + if v := cloud.VolumeAPIVersion; v != "" { + volumeVersion = v + } + + switch volumeVersion { + case "v1", "1": + return openstack.NewBlockStorageV1(pClient, eo) + case "v2", "2": + return openstack.NewBlockStorageV2(pClient, eo) + case "v3", "3": + return openstack.NewBlockStorageV3(pClient, eo) + default: + return nil, fmt.Errorf("invalid volume API version") + } + case "workflowv2": + return openstack.NewWorkflowV2(pClient, eo) + } + + return nil, fmt.Errorf("unable to create a service client for %s", service) +} + +// isProjectScoped determines if an auth struct is project scoped. +func isProjectScoped(authInfo *AuthInfo) bool { + if authInfo.ProjectID == "" && authInfo.ProjectName == "" { + return false + } + + return true +} + +// setDomainIfNeeded will set a DomainID and DomainName +// to ProjectDomain* and UserDomain* if not already set. +func setDomainIfNeeded(cloud *Cloud) *Cloud { + if cloud.AuthInfo.DomainID != "" { + if cloud.AuthInfo.UserDomainID == "" { + cloud.AuthInfo.UserDomainID = cloud.AuthInfo.DomainID + } + + if cloud.AuthInfo.ProjectDomainID == "" { + cloud.AuthInfo.ProjectDomainID = cloud.AuthInfo.DomainID + } + + cloud.AuthInfo.DomainID = "" + } + + if cloud.AuthInfo.DomainName != "" { + if cloud.AuthInfo.UserDomainName == "" { + cloud.AuthInfo.UserDomainName = cloud.AuthInfo.DomainName + } + + if cloud.AuthInfo.ProjectDomainName == "" { + cloud.AuthInfo.ProjectDomainName = cloud.AuthInfo.DomainName + } + + cloud.AuthInfo.DomainName = "" + } + + // If Domain fields are still not set, and if DefaultDomain has a value, + // set UserDomainID and ProjectDomainID to DefaultDomain. + // https://github.com/openstack/osc-lib/blob/86129e6f88289ef14bfaa3f7c9cdfbea8d9fc944/osc_lib/cli/client_config.py#L117-L146 + if cloud.AuthInfo.DefaultDomain != "" { + if cloud.AuthInfo.UserDomainName == "" && cloud.AuthInfo.UserDomainID == "" { + cloud.AuthInfo.UserDomainID = cloud.AuthInfo.DefaultDomain + } + + if cloud.AuthInfo.ProjectDomainName == "" && cloud.AuthInfo.ProjectDomainID == "" { + cloud.AuthInfo.ProjectDomainID = cloud.AuthInfo.DefaultDomain + } + } + + return cloud +} + +// isApplicationCredential determines if an application credential is used to auth. +func isApplicationCredential(authInfo *AuthInfo) bool { + if authInfo.ApplicationCredentialID == "" && authInfo.ApplicationCredentialName == "" && authInfo.ApplicationCredentialSecret == "" { + return false + } + return true +} diff --git a/vendor/github.com/gophercloud/utils/openstack/clientconfig/results.go b/vendor/github.com/gophercloud/utils/openstack/clientconfig/results.go new file mode 100644 index 00000000000..6f3bd11662c --- /dev/null +++ b/vendor/github.com/gophercloud/utils/openstack/clientconfig/results.go @@ -0,0 +1,187 @@ +package clientconfig + +import "encoding/json" + +// PublicClouds represents a collection of PublicCloud entries in clouds-public.yaml file. +// The format of the clouds-public.yml is documented at +// https://docs.openstack.org/python-openstackclient/latest/configuration/ +type PublicClouds struct { + Clouds map[string]Cloud `yaml:"public-clouds" json:"public-clouds"` +} + +// Clouds represents a collection of Cloud entries in a clouds.yaml file. +// The format of clouds.yaml is documented at +// https://docs.openstack.org/os-client-config/latest/user/configuration.html. +type Clouds struct { + Clouds map[string]Cloud `yaml:"clouds" json:"clouds"` +} + +// Cloud represents an entry in a clouds.yaml/public-clouds.yaml/secure.yaml file. +type Cloud struct { + Cloud string `yaml:"cloud,omitempty" json:"cloud,omitempty"` + Profile string `yaml:"profile,omitempty" json:"profile,omitempty"` + AuthInfo *AuthInfo `yaml:"auth,omitempty" json:"auth,omitempty"` + AuthType AuthType `yaml:"auth_type,omitempty" json:"auth_type,omitempty"` + RegionName string `yaml:"region_name,omitempty" json:"region_name,omitempty"` + Regions []Region `yaml:"regions,omitempty" json:"regions,omitempty"` + + // EndpointType and Interface both specify whether to use the public, internal, + // or admin interface of a service. They should be considered synonymous, but + // EndpointType will take precedence when both are specified. + EndpointType string `yaml:"endpoint_type,omitempty" json:"endpoint_type,omitempty"` + Interface string `yaml:"interface,omitempty" json:"interface,omitempty"` + + // API Version overrides. + IdentityAPIVersion string `yaml:"identity_api_version,omitempty" json:"identity_api_version,omitempty"` + VolumeAPIVersion string `yaml:"volume_api_version,omitempty" json:"volume_api_version,omitempty"` + + // Verify whether or not SSL API requests should be verified. + Verify *bool `yaml:"verify,omitempty" json:"verify,omitempty"` + + // CACertFile a path to a CA Cert bundle that can be used as part of + // verifying SSL API requests. + CACertFile string `yaml:"cacert,omitempty" json:"cacert,omitempty"` + + // ClientCertFile a path to a client certificate to use as part of the SSL + // transaction. + ClientCertFile string `yaml:"cert,omitempty" json:"cert,omitempty"` + + // ClientKeyFile a path to a client key to use as part of the SSL + // transaction. + ClientKeyFile string `yaml:"key,omitempty" json:"key,omitempty"` +} + +// AuthInfo represents the auth section of a cloud entry or +// auth options entered explicitly in ClientOpts. +type AuthInfo struct { + // AuthURL is the keystone/identity endpoint URL. + AuthURL string `yaml:"auth_url,omitempty" json:"auth_url,omitempty"` + + // Token is a pre-generated authentication token. + Token string `yaml:"token,omitempty" json:"token,omitempty"` + + // Username is the username of the user. + Username string `yaml:"username,omitempty" json:"username,omitempty"` + + // UserID is the unique ID of a user. + UserID string `yaml:"user_id,omitempty" json:"user_id,omitempty"` + + // Password is the password of the user. + Password string `yaml:"password,omitempty" json:"password,omitempty"` + + // Application Credential ID to login with. + ApplicationCredentialID string `yaml:"application_credential_id,omitempty" json:"application_credential_id,omitempty"` + + // Application Credential name to login with. + ApplicationCredentialName string `yaml:"application_credential_name,omitempty" json:"application_credential_name,omitempty"` + + // Application Credential secret to login with. + ApplicationCredentialSecret string `yaml:"application_credential_secret,omitempty" json:"application_credential_secret,omitempty"` + + // SystemScope is a system information to scope to. + SystemScope string `yaml:"system_scope,omitempty" json:"system_scope,omitempty"` + + // ProjectName is the common/human-readable name of a project. + // Users can be scoped to a project. + // ProjectName on its own is not enough to ensure a unique scope. It must + // also be combined with either a ProjectDomainName or ProjectDomainID. + // ProjectName cannot be combined with ProjectID in a scope. + ProjectName string `yaml:"project_name,omitempty" json:"project_name,omitempty"` + + // ProjectID is the unique ID of a project. + // It can be used to scope a user to a specific project. + ProjectID string `yaml:"project_id,omitempty" json:"project_id,omitempty"` + + // UserDomainName is the name of the domain where a user resides. + // It is used to identify the source domain of a user. + UserDomainName string `yaml:"user_domain_name,omitempty" json:"user_domain_name,omitempty"` + + // UserDomainID is the unique ID of the domain where a user resides. + // It is used to identify the source domain of a user. + UserDomainID string `yaml:"user_domain_id,omitempty" json:"user_domain_id,omitempty"` + + // ProjectDomainName is the name of the domain where a project resides. + // It is used to identify the source domain of a project. + // ProjectDomainName can be used in addition to a ProjectName when scoping + // a user to a specific project. + ProjectDomainName string `yaml:"project_domain_name,omitempty" json:"project_domain_name,omitempty"` + + // ProjectDomainID is the name of the domain where a project resides. + // It is used to identify the source domain of a project. + // ProjectDomainID can be used in addition to a ProjectName when scoping + // a user to a specific project. + ProjectDomainID string `yaml:"project_domain_id,omitempty" json:"project_domain_id,omitempty"` + + // DomainName is the name of a domain which can be used to identify the + // source domain of either a user or a project. + // If UserDomainName and ProjectDomainName are not specified, then DomainName + // is used as a default choice. + // It can also be used be used to specify a domain-only scope. + DomainName string `yaml:"domain_name,omitempty" json:"domain_name,omitempty"` + + // DomainID is the unique ID of a domain which can be used to identify the + // source domain of eitehr a user or a project. + // If UserDomainID and ProjectDomainID are not specified, then DomainID is + // used as a default choice. + // It can also be used be used to specify a domain-only scope. + DomainID string `yaml:"domain_id,omitempty" json:"domain_id,omitempty"` + + // DefaultDomain is the domain ID to fall back on if no other domain has + // been specified and a domain is required for scope. + DefaultDomain string `yaml:"default_domain,omitempty" json:"default_domain,omitempty"` + + // AllowReauth should be set to true if you grant permission for Gophercloud to + // cache your credentials in memory, and to allow Gophercloud to attempt to + // re-authenticate automatically if/when your token expires. If you set it to + // false, it will not cache these settings, but re-authentication will not be + // possible. This setting defaults to false. + AllowReauth bool `yaml:"allow_reauth,omitempty" json:"allow_reauth,omitempty"` +} + +// Region represents a region included as part of cloud in clouds.yaml +// According to Python-based openstacksdk, this can be either a struct (as defined) +// or a plain string. Custom unmarshallers handle both cases. +type Region struct { + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Values Cloud `yaml:"values,omitempty" json:"values,omitempty"` +} + +// UnmarshalJSON handles either a plain string acting as the Name property or +// a struct, mimicking the Python-based openstacksdk. +func (r *Region) UnmarshalJSON(data []byte) error { + var name string + if err := json.Unmarshal(data, &name); err == nil { + r.Name = name + return nil + } + + type region Region + var tmp region + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + r.Name = tmp.Name + r.Values = tmp.Values + + return nil +} + +// UnmarshalYAML handles either a plain string acting as the Name property or +// a struct, mimicking the Python-based openstacksdk. +func (r *Region) UnmarshalYAML(unmarshal func(interface{}) error) error { + var name string + if err := unmarshal(&name); err == nil { + r.Name = name + return nil + } + + type region Region + var tmp region + if err := unmarshal(&tmp); err != nil { + return err + } + r.Name = tmp.Name + r.Values = tmp.Values + + return nil +} diff --git a/vendor/github.com/gophercloud/utils/openstack/clientconfig/utils.go b/vendor/github.com/gophercloud/utils/openstack/clientconfig/utils.go new file mode 100644 index 00000000000..3082073ab0e --- /dev/null +++ b/vendor/github.com/gophercloud/utils/openstack/clientconfig/utils.go @@ -0,0 +1,190 @@ +package clientconfig + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/user" + "path/filepath" + "reflect" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/utils/env" +) + +// defaultIfEmpty is a helper function to make it cleaner to set default value +// for strings. +func defaultIfEmpty(value string, defaultValue string) string { + if value == "" { + return defaultValue + } + return value +} + +// mergeCLouds merges two Clouds recursively (the AuthInfo also gets merged). +// In case both Clouds define a value, the value in the 'override' cloud takes precedence +func mergeClouds(override, cloud interface{}) (*Cloud, error) { + overrideJson, err := json.Marshal(override) + if err != nil { + return nil, err + } + cloudJson, err := json.Marshal(cloud) + if err != nil { + return nil, err + } + var overrideInterface interface{} + err = json.Unmarshal(overrideJson, &overrideInterface) + if err != nil { + return nil, err + } + var cloudInterface interface{} + err = json.Unmarshal(cloudJson, &cloudInterface) + if err != nil { + return nil, err + } + var mergedCloud Cloud + mergedInterface := mergeInterfaces(overrideInterface, cloudInterface) + mergedJson, err := json.Marshal(mergedInterface) + err = json.Unmarshal(mergedJson, &mergedCloud) + if err != nil { + return nil, err + } + return &mergedCloud, nil +} + +// merges two interfaces. In cases where a value is defined for both 'overridingInterface' and +// 'inferiorInterface' the value in 'overridingInterface' will take precedence. +func mergeInterfaces(overridingInterface, inferiorInterface interface{}) interface{} { + switch overriding := overridingInterface.(type) { + case map[string]interface{}: + interfaceMap, ok := inferiorInterface.(map[string]interface{}) + if !ok { + return overriding + } + for k, v := range interfaceMap { + if overridingValue, ok := overriding[k]; ok { + overriding[k] = mergeInterfaces(overridingValue, v) + } else { + overriding[k] = v + } + } + case []interface{}: + list, ok := inferiorInterface.([]interface{}) + if !ok { + return overriding + } + for i := range list { + overriding = append(overriding, list[i]) + } + return overriding + case nil: + // mergeClouds(nil, map[string]interface{...}) -> map[string]interface{...} + v, ok := inferiorInterface.(map[string]interface{}) + if ok { + return v + } + } + // We don't want to override with empty values + if reflect.DeepEqual(overridingInterface, nil) || reflect.DeepEqual(reflect.Zero(reflect.TypeOf(overridingInterface)).Interface(), overridingInterface) { + return inferiorInterface + } else { + return overridingInterface + } +} + +// FindAndReadCloudsYAML attempts to locate a clouds.yaml file in the following +// locations: +// +// 1. OS_CLIENT_CONFIG_FILE +// 2. Current directory. +// 3. unix-specific user_config_dir (~/.config/openstack/clouds.yaml) +// 4. unix-specific site_config_dir (/etc/openstack/clouds.yaml) +// +// If found, the contents of the file is returned. +func FindAndReadCloudsYAML() (string, []byte, error) { + // OS_CLIENT_CONFIG_FILE + if v := env.Getenv("OS_CLIENT_CONFIG_FILE"); v != "" { + if ok := fileExists(v); ok { + content, err := ioutil.ReadFile(v) + return v, content, err + } + } + + s, b, err := FindAndReadYAML("clouds.yaml") + if s == "" { + return FindAndReadYAML("clouds.yml") + } + return s, b, err +} + +func FindAndReadPublicCloudsYAML() (string, []byte, error) { + s, b, err := FindAndReadYAML("clouds-public.yaml") + if s == "" { + return FindAndReadYAML("clouds-public.yml") + } + return s, b, err +} + +func FindAndReadSecureCloudsYAML() (string, []byte, error) { + s, b, err := FindAndReadYAML("secure.yaml") + if s == "" { + return FindAndReadYAML("secure.yml") + } + return s, b, err +} + +func FindAndReadYAML(yamlFile string) (string, []byte, error) { + // current directory + cwd, err := os.Getwd() + if err != nil { + return "", nil, fmt.Errorf("unable to determine working directory: %w", err) + } + + filename := filepath.Join(cwd, yamlFile) + if ok := fileExists(filename); ok { + content, err := ioutil.ReadFile(filename) + return filename, content, err + } + + // unix user config directory: ~/.config/openstack. + if currentUser, err := user.Current(); err == nil { + homeDir := currentUser.HomeDir + if homeDir != "" { + filename := filepath.Join(homeDir, ".config/openstack/"+yamlFile) + if ok := fileExists(filename); ok { + content, err := ioutil.ReadFile(filename) + return filename, content, err + } + } + } + + // unix-specific site config directory: /etc/openstack. + filename = "/etc/openstack/" + yamlFile + if ok := fileExists(filename); ok { + content, err := ioutil.ReadFile(filename) + return filename, content, err + } + + return "", nil, fmt.Errorf("no %s file found: %w", yamlFile, os.ErrNotExist) +} + +// fileExists checks for the existence of a file at a given location. +func fileExists(filename string) bool { + if _, err := os.Stat(filename); err == nil { + return true + } + return false +} + +// GetEndpointType is a helper method to determine the endpoint type +// requested by the user. +func GetEndpointType(endpointType string) gophercloud.Availability { + if endpointType == "internal" || endpointType == "internalURL" { + return gophercloud.AvailabilityInternal + } + if endpointType == "admin" || endpointType == "adminURL" { + return gophercloud.AvailabilityAdmin + } + return gophercloud.AvailabilityPublic +} diff --git a/vendor/github.com/hashicorp/go-uuid/.travis.yml b/vendor/github.com/hashicorp/go-uuid/.travis.yml new file mode 100644 index 00000000000..769849071ed --- /dev/null +++ b/vendor/github.com/hashicorp/go-uuid/.travis.yml @@ -0,0 +1,12 @@ +language: go + +sudo: false + +go: + - 1.4 + - 1.5 + - 1.6 + - tip + +script: + - go test -bench . -benchmem -v ./... diff --git a/vendor/github.com/hashicorp/go-uuid/LICENSE b/vendor/github.com/hashicorp/go-uuid/LICENSE new file mode 100644 index 00000000000..a320b309c44 --- /dev/null +++ b/vendor/github.com/hashicorp/go-uuid/LICENSE @@ -0,0 +1,365 @@ +Copyright © 2015-2022 HashiCorp, Inc. + +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. "Contributor" + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. "Contributor Version" + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the terms of + a Secondary License. + +1.6. "Executable Form" + + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + + means a work that combines Covered Software with other material, in a + separate file or files, that is not Covered Software. + +1.8. "License" + + means this document. + +1.9. "Licensable" + + means having the right to grant, to the maximum extent possible, whether + at the time of the initial grant or subsequently, any and all of the + rights conveyed by this License. + +1.10. "Modifications" + + means any of the following: + + a. any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. "Patent Claims" of a Contributor + + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the License, + by the making, using, selling, offering for sale, having made, import, + or transfer of either its Contributions or its Contributor Version. + +1.12. "Secondary License" + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. "Source Code Form" + + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, "control" means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution + become effective for each Contribution on the date the Contributor first + distributes such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under + this License. No additional rights or licenses will be implied from the + distribution or licensing of Covered Software under this License. + Notwithstanding Section 2.1(b) above, no patent license is granted by a + Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of + its Contributions. + + This License does not grant any rights in the trademarks, service marks, + or logos of any Contributor (except as may be necessary to comply with + the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this + License (see Section 10.2) or under the terms of a Secondary License (if + permitted under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its + Contributions are its original creation(s) or it has sufficient rights to + grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under + applicable copyright doctrines of fair use, fair dealing, or other + equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under + the terms of this License. You must inform recipients that the Source + Code Form of the Covered Software is governed by the terms of this + License, and how they can obtain a copy of this License. You may not + attempt to alter or restrict the recipients' rights in the Source Code + Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter the + recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for + the Covered Software. If the Larger Work is a combination of Covered + Software with a work governed by one or more Secondary Licenses, and the + Covered Software is not Incompatible With Secondary Licenses, this + License permits You to additionally distribute such Covered Software + under the terms of such Secondary License(s), so that the recipient of + the Larger Work may, at their option, further distribute the Covered + Software under the terms of either this License or such Secondary + License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices + (including copyright notices, patent notices, disclaimers of warranty, or + limitations of liability) contained within the Source Code Form of the + Covered Software, except that You may alter any license notices to the + extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on + behalf of any Contributor. You must make it absolutely clear that any + such warranty, support, indemnity, or liability obligation is offered by + You alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, + judicial order, or regulation then You must: (a) comply with the terms of + this License to the maximum extent possible; and (b) describe the + limitations and the code they affect. Such description must be placed in a + text file included with all distributions of the Covered Software under + this License. Except to the extent prohibited by statute or regulation, + such description must be sufficiently detailed for a recipient of ordinary + skill to be able to understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing + basis, if such Contributor fails to notify You of the non-compliance by + some reasonable means prior to 60 days after You have come back into + compliance. Moreover, Your grants from a particular Contributor are + reinstated on an ongoing basis if such Contributor notifies You of the + non-compliance by some reasonable means, this is the first time You have + received notice of non-compliance with this License from such + Contributor, and You become compliant prior to 30 days after Your receipt + of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, + counter-claims, and cross-claims) alleging that a Contributor Version + directly or indirectly infringes any patent, then the rights granted to + You by any and all Contributors for the Covered Software under Section + 2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an "as is" basis, + without warranty of any kind, either expressed, implied, or statutory, + including, without limitation, warranties that the Covered Software is free + of defects, merchantable, fit for a particular purpose or non-infringing. + The entire risk as to the quality and performance of the Covered Software + is with You. Should any Covered Software prove defective in any respect, + You (not any Contributor) assume the cost of any necessary servicing, + repair, or correction. This disclaimer of warranty constitutes an essential + part of this License. No use of any Covered Software is authorized under + this License except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from + such party's negligence to the extent applicable law prohibits such + limitation. Some jurisdictions do not allow the exclusion or limitation of + incidental or consequential damages, so this exclusion and limitation may + not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts + of a jurisdiction where the defendant maintains its principal place of + business and such litigation shall be governed by laws of that + jurisdiction, without reference to its conflict-of-law provisions. Nothing + in this Section shall prevent a party's ability to bring cross-claims or + counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject + matter hereof. If any provision of this License is held to be + unenforceable, such provision shall be reformed only to the extent + necessary to make it enforceable. Any law or regulation which provides that + the language of a contract shall be construed against the drafter shall not + be used to construe this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version + of the License under which You originally received the Covered Software, + or under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a + modified version of this License if you rename the license and remove + any references to the name of the license steward (except to note that + such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary + Licenses If You choose to distribute Source Code Form that is + Incompatible With Secondary Licenses under the terms of this version of + the License, the notice described in Exhibit B of this License must be + attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, +then You may include the notice in a location (such as a LICENSE file in a +relevant directory) where a recipient would be likely to look for such a +notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice + + This Source Code Form is "Incompatible + With Secondary Licenses", as defined by + the Mozilla Public License, v. 2.0. + diff --git a/vendor/github.com/hashicorp/go-uuid/README.md b/vendor/github.com/hashicorp/go-uuid/README.md new file mode 100644 index 00000000000..fbde8b9aef6 --- /dev/null +++ b/vendor/github.com/hashicorp/go-uuid/README.md @@ -0,0 +1,8 @@ +# uuid [![Build Status](https://travis-ci.org/hashicorp/go-uuid.svg?branch=master)](https://travis-ci.org/hashicorp/go-uuid) + +Generates UUID-format strings using high quality, _purely random_ bytes. It is **not** intended to be RFC compliant, merely to use a well-understood string representation of a 128-bit value. It can also parse UUID-format strings into their component bytes. + +Documentation +============= + +The full documentation is available on [Godoc](http://godoc.org/github.com/hashicorp/go-uuid). diff --git a/vendor/github.com/hashicorp/go-uuid/uuid.go b/vendor/github.com/hashicorp/go-uuid/uuid.go new file mode 100644 index 00000000000..0c10c4e9f5f --- /dev/null +++ b/vendor/github.com/hashicorp/go-uuid/uuid.go @@ -0,0 +1,83 @@ +package uuid + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "io" +) + +// GenerateRandomBytes is used to generate random bytes of given size. +func GenerateRandomBytes(size int) ([]byte, error) { + return GenerateRandomBytesWithReader(size, rand.Reader) +} + +// GenerateRandomBytesWithReader is used to generate random bytes of given size read from a given reader. +func GenerateRandomBytesWithReader(size int, reader io.Reader) ([]byte, error) { + if reader == nil { + return nil, fmt.Errorf("provided reader is nil") + } + buf := make([]byte, size) + if _, err := io.ReadFull(reader, buf); err != nil { + return nil, fmt.Errorf("failed to read random bytes: %v", err) + } + return buf, nil +} + + +const uuidLen = 16 + +// GenerateUUID is used to generate a random UUID +func GenerateUUID() (string, error) { + return GenerateUUIDWithReader(rand.Reader) +} + +// GenerateUUIDWithReader is used to generate a random UUID with a given Reader +func GenerateUUIDWithReader(reader io.Reader) (string, error) { + if reader == nil { + return "", fmt.Errorf("provided reader is nil") + } + buf, err := GenerateRandomBytesWithReader(uuidLen, reader) + if err != nil { + return "", err + } + return FormatUUID(buf) +} + +func FormatUUID(buf []byte) (string, error) { + if buflen := len(buf); buflen != uuidLen { + return "", fmt.Errorf("wrong length byte slice (%d)", buflen) + } + + return fmt.Sprintf("%x-%x-%x-%x-%x", + buf[0:4], + buf[4:6], + buf[6:8], + buf[8:10], + buf[10:16]), nil +} + +func ParseUUID(uuid string) ([]byte, error) { + if len(uuid) != 2 * uuidLen + 4 { + return nil, fmt.Errorf("uuid string is wrong length") + } + + if uuid[8] != '-' || + uuid[13] != '-' || + uuid[18] != '-' || + uuid[23] != '-' { + return nil, fmt.Errorf("uuid is improperly formatted") + } + + hexStr := uuid[0:8] + uuid[9:13] + uuid[14:18] + uuid[19:23] + uuid[24:36] + + ret, err := hex.DecodeString(hexStr) + if err != nil { + return nil, err + } + if len(ret) != uuidLen { + return nil, fmt.Errorf("decoded hex is the wrong length") + } + + return ret, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 907028ffe1e..c03d1a10550 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1233,14 +1233,11 @@ github.com/googleapis/gax-go/v2/internal github.com/googleapis/gax-go/v2/internallog github.com/googleapis/gax-go/v2/internallog/internal github.com/googleapis/gax-go/v2/iterator -# github.com/gophercloud/gophercloud v1.7.0 +# github.com/gophercloud/gophercloud v1.14.1 ## explicit; go 1.14 github.com/gophercloud/gophercloud github.com/gophercloud/gophercloud/openstack -github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/secgroups -github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/usage -github.com/gophercloud/gophercloud/openstack/compute/v2/flavors github.com/gophercloud/gophercloud/openstack/compute/v2/servers github.com/gophercloud/gophercloud/openstack/identity/v2/tenants github.com/gophercloud/gophercloud/openstack/identity/v2/tokens @@ -1252,7 +1249,7 @@ github.com/gophercloud/gophercloud/openstack/networking/v2/networks github.com/gophercloud/gophercloud/openstack/networking/v2/ports github.com/gophercloud/gophercloud/openstack/utils github.com/gophercloud/gophercloud/pagination -# github.com/gophercloud/gophercloud/v2 v2.5.0 +# github.com/gophercloud/gophercloud/v2 v2.8.0 ## explicit; go 1.22 github.com/gophercloud/gophercloud/v2 github.com/gophercloud/gophercloud/v2/openstack @@ -1299,6 +1296,12 @@ github.com/gophercloud/gophercloud/v2/openstack/sharedfilesystems/v2/shares github.com/gophercloud/gophercloud/v2/openstack/sharedfilesystems/v2/snapshots github.com/gophercloud/gophercloud/v2/openstack/utils github.com/gophercloud/gophercloud/v2/pagination +# github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 +## explicit; go 1.15 +github.com/gophercloud/utils/env +github.com/gophercloud/utils/gnocchi +github.com/gophercloud/utils/internal +github.com/gophercloud/utils/openstack/clientconfig # github.com/gophercloud/utils/v2 v2.0.0-20250212084022-725b94822eeb ## explicit; go 1.22 github.com/gophercloud/utils/v2/env @@ -1348,6 +1351,9 @@ github.com/hashicorp/go-immutable-radix/v2 # github.com/hashicorp/go-retryablehttp v0.7.7 ## explicit; go 1.19 github.com/hashicorp/go-retryablehttp +# github.com/hashicorp/go-uuid v1.0.3 +## explicit +github.com/hashicorp/go-uuid # github.com/hashicorp/go-version v1.7.0 ## explicit github.com/hashicorp/go-version From 07287719c6310a9b5f2dba1f5fd6bb2771e2a60d Mon Sep 17 00:00:00 2001 From: daturece Date: Tue, 4 Nov 2025 15:57:11 +0100 Subject: [PATCH 7/8] address idempotency, aggregate errors, filter using tags, handle 404/409 errors from api --- docs/hibernating-clusters.md | 2 +- .../hibernation/openstack_actuator.go | 1200 ++++++++--------- .../hibernation/openstack_actuator_test.go | 146 +- 3 files changed, 635 insertions(+), 713 deletions(-) diff --git a/docs/hibernating-clusters.md b/docs/hibernating-clusters.md index fb53a5d6170..d2317c5d2b2 100644 --- a/docs/hibernating-clusters.md +++ b/docs/hibernating-clusters.md @@ -346,7 +346,7 @@ The unreachable controller tracks the reachability of a cluster and is responsib Once a cluster hibernates and stops responding, the unreachable controller sets the `Unreachable` condition's `Status` to `True`. -Once the cluster resumes, the unreachable controller set the condition `False` and the hive controllers are free to resume syncing. +Once the cluster resumes, the unreachable controller sets the condition `False` and the hive controllers are free to resume syncing. ## OpenStack Hibernation OpenStack hibernation deletes cluster instances while preserving the ability to restore them. Unlike other cloud providers, OpenStack lacks native hibernation, therefore an implementation based on snapshots is necessary. diff --git a/pkg/controller/hibernation/openstack_actuator.go b/pkg/controller/hibernation/openstack_actuator.go index 7bdc4a7b889..2ac7e85775e 100644 --- a/pkg/controller/hibernation/openstack_actuator.go +++ b/pkg/controller/hibernation/openstack_actuator.go @@ -11,6 +11,7 @@ import ( "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images" hivev1 "github.com/openshift/hive/apis/hive/v1" "github.com/openshift/hive/pkg/openstackclient" + "github.com/pkg/errors" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" @@ -33,24 +34,37 @@ type openstackActuator struct { var _ HibernationActuator = &openstackActuator{} +// OpenStackInstanceConfig stores instance configuration for restoration +type OpenStackInstanceConfig struct { + Name string `json:"name"` + Flavor string `json:"flavor"` + PortID string `json:"portID"` + SnapshotID string `json:"snapshotID"` + SnapshotName string `json:"snapshotName"` + SecurityGroups []string `json:"securityGroups"` + ClusterID string `json:"clusterID"` + NetworkID string `json:"networkID"` + OpenshiftClusterID string `json:"openshiftClusterID"` + Metadata map[string]string `json:"metadata"` + Tags []string `json:"tags"` +} + // Create API client func getOpenStackClient(cd *hivev1.ClusterDeployment, c client.Client, logger log.FieldLogger) (openstackclient.Client, error) { - ctx := context.Background() - if cd.Spec.Platform.OpenStack == nil || cd.Spec.Platform.OpenStack.CredentialsSecretRef.Name == "" { - return nil, fmt.Errorf("no OpenStack credentials secret reference found in ClusterDeployment") + return nil, errors.New("no OpenStack credentials secret reference found in ClusterDeployment") } secretName := cd.Spec.Platform.OpenStack.CredentialsSecretRef.Name secretNamespace := cd.Namespace secret := &corev1.Secret{} - err := c.Get(ctx, types.NamespacedName{ + err := c.Get(context.Background(), types.NamespacedName{ Name: secretName, Namespace: secretNamespace, }, secret) if err != nil { - return nil, fmt.Errorf("failed to get credentials secret %s/%s: %v", secretNamespace, secretName, err) + return nil, errors.Wrapf(err, "failed to get credentials secret %s/%s", secretNamespace, secretName) } return openstackclient.NewClientFromSecret(secret) @@ -61,21 +75,22 @@ func (a *openstackActuator) CanHandle(cd *hivev1.ClusterDeployment) bool { return cd.Spec.Platform.OpenStack != nil } +// StopMachines func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { logger = logger.WithField("cloud", "openstack") - logger.Info("stopping machines and creating snapshots") + logger.Info("stopping machines") openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) if err != nil { - return fmt.Errorf("failed to create OpenStack client: %v", err) + return errors.Wrap(err, "failed to create OpenStack client") } infraID := cd.Spec.ClusterMetadata.InfraID - // 1. Find instances + // Step 1: Find instances matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) if err != nil { - return fmt.Errorf("error finding instances: %v", err) + return errors.Wrap(err, "error finding instances") } if len(matchingServers) == 0 { @@ -85,350 +100,447 @@ func (a *openstackActuator) StopMachines(cd *hivev1.ClusterDeployment, hiveClien logger.WithField("count", len(matchingServers)).Info("found instances to hibernate") - // 2. Validate instance states - if err := a.validateInstanceStates(openstackClient, matchingServers, logger); err != nil { + // Step 2: Validate instance states + if err := a.validateInstanceStates(matchingServers, logger); err != nil { return err } - // 3. Pause all instances for data consistency + // Step 3: Pause all instances if err := a.pauseInstances(openstackClient, matchingServers, logger); err != nil { - return fmt.Errorf("failed to pause instances: %v", err) + return err } - // 4. Create snapshots - snapshotIDs, snapshotNames, err := a.createSnapshots(openstackClient, matchingServers, logger) + // Step 4: Create snapshots + snapshotMapping, err := a.createSnapshots(openstackClient, matchingServers, logger) if err != nil { - // If snapshot creation fails, unpause - logger.Warn("snapshot creation failed - unpausing instances") - a.unpauseInstances(openstackClient, matchingServers, logger) return err } - // 5. Wait for snapshots to complete - if err := a.waitForSnapshots(openstackClient, snapshotIDs, snapshotNames, matchingServers, logger); err != nil { - // If snapshot wait fails, unpause - logger.Warn("snapshot wait failed - unpausing instances") - a.unpauseInstances(openstackClient, matchingServers, logger) + // Step 5: Wait for snapshots to complete + if err := a.waitForSnapshots(openstackClient, snapshotMapping, logger); err != nil { return err } - // 6. Save instance configuration - if err := a.saveInstanceConfigurationToSecret(cd, hiveClient, openstackClient, matchingServers, snapshotIDs, snapshotNames, logger); err != nil { - // If config save fails, unpause instances before returning - logger.Warn("config save failed - unpausing instances") - a.unpauseInstances(openstackClient, matchingServers, logger) - return fmt.Errorf("error saving configuration: %v", err) + // Step 6: Save instance configuration + if err := a.saveInstanceConfiguration(cd, hiveClient, openstackClient, matchingServers, snapshotMapping, logger); err != nil { + return errors.Wrap(err, "error saving configuration") } - // 7. Delete instances + // Step 7: Delete instances if err := a.deleteInstances(openstackClient, matchingServers, logger); err != nil { return err } - // 8. Wait for instance cleanup + // Step 8: Wait for cleanup if err := a.waitForInstanceCleanup(openstackClient, infraID, logger); err != nil { return err } - // 9. Cleanup old snapshots - logger.Info("cleaning up old hibernation snapshots after instance deletion") - err = a.cleanupOldSnapshotsAfterDeletion(cd, hiveClient, openstackClient, infraID, snapshotIDs, logger) - if err != nil { - logger.WithField("error", err).Warn("some old snapshots couldn't be cleaned up") + // Step 9: Best-effort cleanup of old snapshots + if err := a.cleanupOldSnapshots(cd, hiveClient, openstackClient, infraID, snapshotMapping, logger); err != nil { + logger.Warnf("failed to cleanup old snapshots: %v", err) } logger.Info("hibernation completed successfully") return nil } -// Find which instances are missing -func (a *openstackActuator) findMissingInstances(expectedInstances []OpenStackInstanceConfig, existingServers []*servers.Server, logger log.FieldLogger) []OpenStackInstanceConfig { - var missingInstances []OpenStackInstanceConfig +// StartMachines +func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { + logger = logger.WithField("cloud", "openstack") + logger.Info("starting machines from snapshots") - // Create a map of existing instance names for quick lookup - existingNames := make(map[string]bool) - for _, server := range existingServers { - existingNames[server.Name] = true + // Only proceed if PowerState is Running + if cd.Spec.PowerState != hivev1.ClusterPowerStateRunning { + logger.WithField("power_state", cd.Spec.PowerState).Info("PowerState is not Running - refusing to start machines") + return nil } - // Check which expected instances are missing - for _, expected := range expectedInstances { - instanceLogger := logger.WithField("instance", expected.Name) - if !existingNames[expected.Name] { - instanceLogger.Info("instance is missing - needs to be created") - missingInstances = append(missingInstances, expected) + openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) + if err != nil { + return errors.Wrap(err, "failed to create OpenStack client") + } + + // Load hibernation config + instances, err := a.loadHibernationConfig(cd, hiveClient, logger) + if err != nil { + logger.Warnf("no hibernation config found: %v", err) + + // Check if we have existing instances + infraID := cd.Spec.ClusterMetadata.InfraID + existingServers, checkErr := a.findInstancesByInfraID(openstackClient, infraID) + if checkErr != nil { + logger.Warnf("could not check existing instances: %v", checkErr) + } else if len(existingServers) > 0 { + logger.Info("instances exist but no hibernation config - clearing any state") + _ = a.deleteHibernationConfig(cd, hiveClient, logger) } else { - instanceLogger.Info("instance already exists") + logger.Warn("cannot recreate instances without hibernation snapshots") } + return nil } - return missingInstances + // Check for existing instances + infraID := cd.Spec.ClusterMetadata.InfraID + existingServers, err := a.findInstancesByInfraID(openstackClient, infraID) + if err != nil { + logger.Warnf("could not check existing instances: %v", err) + existingServers = []*servers.Server{} + } + + // Check if we already have all instances + if len(existingServers) >= len(instances) { + logger.Info("sufficient instances already exist - clearing hibernation config") + _ = a.deleteHibernationConfig(cd, hiveClient, logger) + return nil + } + + logger.Infof("restoring %d instances from hibernation (currently have %d)", len(instances), len(existingServers)) + + // Figure out what needs to be created + instancesToCreate := a.findMissingInstances(instances, existingServers, logger) + + if len(instancesToCreate) > 0 { + logger.WithField("count", len(instancesToCreate)).Info("creating missing instances") + + // Create missing instances + if err := a.createMissingInstances(openstackClient, instancesToCreate, logger); err != nil { + logger.Errorf("some instance creation failed: %v", err) + } + } + + // Wait for all instances to be active + if err := a.waitForAllInstancesToBeActive(openstackClient, infraID, len(instances), logger); err != nil { + return errors.Wrap(err, "not all instances are active yet") + } + + // Restore tags after instances are active + if err := a.restoreInstanceTags(openstackClient, instances, logger); err != nil { + logger.Warnf("failed to restore some tags: %v", err) + } + + // Cleanup hibernation snapshots + logger.Info("cleaning up hibernation snapshots after successful restoration") + if err := a.cleanupRestorationSnapshots(openstackClient, instances, logger); err != nil { + logger.Warnf("failed to cleanup some snapshots: %v", err) + } + + // Clear hibernation config + logger.Info("all instances confirmed active - clearing hibernation configuration") + return a.deleteHibernationConfig(cd, hiveClient, logger) } -// Check if instances are in valid states for hibernation -func (a *openstackActuator) validateInstanceStates(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) error { +// MachinesRunning checks if machines are running +func (a *openstackActuator) MachinesRunning(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) (bool, []string, error) { + logger = logger.WithField("cloud", "openstack") + logger.Info("checking if machines are running") + + openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) + if err != nil { + return false, nil, errors.Wrap(err, "failed to create OpenStack client") + } + + infraID := cd.Spec.ClusterMetadata.InfraID + matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) + if err != nil { + return false, nil, errors.Wrap(err, "error finding instances") + } + + logger.Infof("found %d instances with infraID '%s'", len(matchingServers), infraID) + + if len(matchingServers) == 0 { + logger.Info("no instances found - machines are not running") + return false, []string{"no instances found"}, nil + } + + // Check actual instance states + var notRunningInstances []string + for _, server := range matchingServers { + status := strings.ToLower(server.Status) + if status != "active" && status != "paused" { + notRunningInstances = append(notRunningInstances, server.Name) + } + } + + if len(notRunningInstances) > 0 { + logger.Infof("found non-running instances: %v", notRunningInstances) + return false, notRunningInstances, nil + } + + return true, []string{}, nil +} + +// MachinesStopped checks if machines are stopped +func (a *openstackActuator) MachinesStopped(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) (bool, []string, error) { + logger = logger.WithField("cloud", "openstack") + logger.Info("checking if machines are stopped") + + openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) + if err != nil { + return false, nil, errors.Wrap(err, "failed to create OpenStack client") + } + + infraID := cd.Spec.ClusterMetadata.InfraID + matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) + if err != nil { + return false, nil, errors.Wrap(err, "error finding instances") + } + + if len(matchingServers) == 0 { + logger.Info("no instances found - machines are stopped") + return true, nil, nil + } + + // Instances still exist + var notStopped []string + for _, server := range matchingServers { + notStopped = append(notStopped, server.Name) + } + + logger.Infof("found %d instances still existing", len(notStopped)) + return false, notStopped, nil +} + +// Validate all instances +func (a *openstackActuator) validateInstanceStates(servers []*servers.Server, logger log.FieldLogger) error { + var errs []error + for _, server := range servers { serverLogger := logger.WithFields(log.Fields{ "instance": server.Name, "status": server.Status, }) - serverLogger.Info("instance status") + serverLogger.Info("validating instance status") // Check for deleting states that would cause conflicts if strings.Contains(strings.ToLower(server.Status), "delet") { - return fmt.Errorf("cannot hibernate: instance %s is being deleted by another process", server.Name) + errs = append(errs, errors.Errorf("instance %s is being deleted by another process", server.Name)) } - if server.Status != "ACTIVE" { - serverLogger.Warn("instance status is not ACTIVE - snapshot may fail") + // Warn about non-ACTIVE states + if server.Status != "ACTIVE" && server.Status != "PAUSED" { + serverLogger.Warn("instance status is not ACTIVE - operation may proceed but could fail") } } - return nil -} -// Helper function to generate hibernation snapshot names -func (a *openstackActuator) generateHibernationSnapshotName(serverName string, timestamp string) string { - return fmt.Sprintf("%s-hibernation-%s", serverName, timestamp) + return utilerrors.NewAggregate(errs) } -// Create snapshots for all instances -func (a *openstackActuator) createSnapshots(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) ([]string, []string, error) { - ctx := context.Background() - snapshotIDs := make([]string, 0, len(servers)) - snapshotNames := make([]string, 0, len(servers)) - - timestamp := time.Now().UTC().Format("20060102-150405") - for i, server := range servers { - progressLogger := logger.WithFields(log.Fields{ - "current": i + 1, - "total": len(servers), - "instance": server.Name, - }) - - progressLogger.Info("creating snapshot") +// Pause instances +func (a *openstackActuator) pauseInstances(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) error { + var errs []error - snapshotName := a.generateHibernationSnapshotName(server.Name, timestamp) + for _, server := range servers { + serverLogger := logger.WithField("instance", server.Name) - snapshotID, err := openstackClient.CreateServerSnapshot(ctx, server.ID, snapshotName) + // Check current status first + current, err := openstackClient.GetServer(context.Background(), server.ID) if err != nil { - if strings.Contains(err.Error(), "task_state deleting") || strings.Contains(err.Error(), "409") { - return nil, nil, fmt.Errorf("hibernation conflict: instance %s is being modified by another process", server.Name) + if isNotFoundError(err) { + serverLogger.Info("instance not found, skipping pause") + continue } - return nil, nil, fmt.Errorf("failed to create snapshot for %s: %v", server.Name, err) + errs = append(errs, errors.Wrapf(err, "failed to get status of %s", server.Name)) + continue } - snapshotIDs = append(snapshotIDs, snapshotID) - snapshotNames = append(snapshotNames, snapshotName) - progressLogger.WithFields(log.Fields{ - "snapshot_id": snapshotID, - "snapshot_name": snapshotName, - }).Info("snapshot created") - } - return snapshotIDs, snapshotNames, nil -} - -// Wait for all snapshots to complete -func (a *openstackActuator) waitForSnapshots(openstackClient openstackclient.Client, snapshotIDs []string, snapshotNames []string, servers []*servers.Server, logger log.FieldLogger) error { - for i, snapshotID := range snapshotIDs { - serverName := servers[i].Name - snapshotName := snapshotNames[i] - logger.WithFields(log.Fields{ - "snapshot_id": snapshotID, - "snapshot_name": snapshotName, - "server": serverName, - }).Info("waiting for snapshot to complete") + if current.Status == "PAUSED" { + serverLogger.Info("instance already paused") + continue + } - err := a.waitForSnapshotCompletion(openstackClient, snapshotID, serverName, logger) - if err != nil { - return fmt.Errorf("failed to wait for snapshot %s: %v", snapshotID, err) + serverLogger.Info("pausing instance") + if err := openstackClient.PauseServer(context.Background(), server.ID); err != nil { + // Check if it's a conflict (already paused) + if strings.Contains(err.Error(), "409") { + serverLogger.Info("instance already paused (409)") + continue + } + errs = append(errs, errors.Wrapf(err, "failed to pause %s", server.Name)) } } - return nil + + return utilerrors.NewAggregate(errs) } -// Delete all instances -func (a *openstackActuator) deleteInstances(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) error { - ctx := context.Background() +// Snapshot creation +func (a *openstackActuator) createSnapshots(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) (map[string]string, error) { + snapshotMapping := make(map[string]string) + timestamp := time.Now().UTC().Format("20060102-150405") + var errs []error - for i, server := range servers { - logger.WithFields(log.Fields{ - "current": i + 1, - "total": len(servers), - "instance": server.Name, - }).Info("deleting instance") + for _, server := range servers { + snapshotName := fmt.Sprintf("%s-hibernation-%s", server.Name, timestamp) + serverLogger := logger.WithFields(log.Fields{ + "instance": server.Name, + "snapshot_name": snapshotName, + }) + + // Check if snapshot already exists + existing, err := a.findSnapshotByName(openstackClient, snapshotName) + if err == nil && existing != nil { + serverLogger.WithField("snapshot_id", existing.ID).Info("snapshot already exists") + snapshotMapping[server.ID] = existing.ID + continue + } - err := openstackClient.DeleteServer(ctx, server.ID) + serverLogger.Info("creating snapshot") + snapshotID, err := openstackClient.CreateServerSnapshot(context.Background(), server.ID, snapshotName) if err != nil { - return fmt.Errorf("failed to delete %s: %v", server.Name, err) + if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "task_state deleting") { + errs = append(errs, errors.Errorf("conflict creating snapshot for %s - instance being modified", server.Name)) + } else { + errs = append(errs, errors.Wrapf(err, "failed to create snapshot for %s", server.Name)) + } + continue } + + snapshotMapping[server.ID] = snapshotID + serverLogger.WithField("snapshot_id", snapshotID).Info("snapshot created") } - return nil + + if len(errs) > 0 { + return nil, utilerrors.NewAggregate(errs) + } + return snapshotMapping, nil } -// Wait for snapshot to reach ACTIVE state -func (a *openstackActuator) waitForSnapshotCompletion(openstackClient openstackclient.Client, snapshotID, serverName string, logger log.FieldLogger) error { - ctx := context.Background() +// Wait for snapshots to complete +func (a *openstackActuator) waitForSnapshots(openstackClient openstackclient.Client, snapshotMapping map[string]string, logger log.FieldLogger) error { timeout := time.After(30 * time.Minute) ticker := time.NewTicker(45 * time.Second) defer ticker.Stop() - for { + pendingSnapshots := make(map[string]string) + for k, v := range snapshotMapping { + pendingSnapshots[k] = v + } + + for len(pendingSnapshots) > 0 { select { case <-timeout: - return fmt.Errorf("timeout waiting for snapshot %s to complete after %v", snapshotID, timeout) + return errors.New("timeout waiting for snapshots to complete after 30 minutes") case <-ticker.C: - image, err := openstackClient.GetImage(ctx, snapshotID) - if err != nil { - logger.Warnf("error checking snapshot %s status: %v", snapshotID, err) - continue - } + var errs []error + for instanceID, snapshotID := range pendingSnapshots { + image, err := openstackClient.GetImage(context.Background(), snapshotID) + if err != nil { + errs = append(errs, errors.Wrapf(err, "error checking snapshot %s", snapshotID)) + continue + } - logger.Infof("snapshot %s for %s status: %s", snapshotID, serverName, image.Status) + logger.Debugf("snapshot %s status: %s", snapshotID, image.Status) + + switch image.Status { + case "active": + delete(pendingSnapshots, instanceID) + logger.Infof("snapshot %s completed", snapshotID) + case "queued", "saving": + // Still in progress + case "killed", "deleted", "deactivated": + errs = append(errs, errors.Errorf("snapshot %s failed with status: %s", snapshotID, image.Status)) + delete(pendingSnapshots, instanceID) + default: + logger.Warnf("unknown snapshot status %s for %s", image.Status, snapshotID) + } + } - switch image.Status { - case "active": - return nil - case "queued", "saving": - continue - case "killed", "deleted", "deactivated": - return fmt.Errorf("snapshot %s failed with status: %s", snapshotID, image.Status) - default: - logger.Warnf("unknown snapshot status %s for %s, continuing to wait", image.Status, snapshotID) - continue + if len(errs) > 0 { + return utilerrors.NewAggregate(errs) } } } -} - -// Recreate instances from snapshots using saved configuration -func (a *openstackActuator) StartMachines(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { - logger = logger.WithField("cloud", "openstack") - logger.Info("starting machines from snapshots") - - // Only proceed if PowerState is Running - if cd.Spec.PowerState != hivev1.ClusterPowerStateRunning { - logger.WithField("power_state", cd.Spec.PowerState).Info("PowerState is not Running - refusing to start machines") - return nil - } - openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) - if err != nil { - return fmt.Errorf("failed to create OpenStack client: %v", err) - } + logger.Info("all snapshots completed successfully") + return nil +} - // Load hibernation config first to know how many instances we should have - instances, err := a.loadHibernationConfigFromSecret(cd, hiveClient, logger) - if err != nil { - logger.Warnf("no hibernation config found: %v", err) +// Delete instances +func (a *openstackActuator) deleteInstances(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) error { + var errs []error - // Check if we have existing instances but no hibernation config - infraID := cd.Spec.ClusterMetadata.InfraID - existingServers, checkErr := a.findInstancesByInfraID(openstackClient, infraID) - if checkErr != nil { - logger.Warnf("could not check existing instances: %v", checkErr) - } else if len(existingServers) > 0 { - logger.Info("instances exist but no hibernation config - clearing any hibernation state") - _ = a.deleteHibernationConfigSecret(cd, hiveClient, logger) - } else { - logger.Warn("cannot recreate instances without hibernation snapshots") + for _, server := range servers { + logger.WithField("instance", server.Name).Info("deleting instance") + err := openstackClient.DeleteServer(context.Background(), server.ID) + if err != nil { + if isNotFoundError(err) { + logger.WithField("instance", server.Name).Info("instance already deleted (404)") + continue + } + errs = append(errs, errors.Wrapf(err, "failed to delete instance %s", server.Name)) } - return nil - } - - // Check for existing instances - infraID := cd.Spec.ClusterMetadata.InfraID - existingServers, err := a.findInstancesByInfraID(openstackClient, infraID) - if err != nil { - logger.Warnf("could not check existing instances: %v", err) - } - - // Check if we already have all the instances we need and clear hibernation config - if len(existingServers) >= len(instances) { - logger.Info("sufficient instances already exist - clearing hibernation config") - _ = a.deleteHibernationConfigSecret(cd, hiveClient, logger) - return nil } - logger.Infof("restoring %d instances from hibernation snapshots (currently have %d)", len(instances), len(existingServers)) - - return a.restoreFromHibernationConfig(cd, hiveClient, openstackClient, instances, logger) + return utilerrors.NewAggregate(errs) } -// Recreate instances from hibernation configuration -func (a *openstackActuator) restoreFromHibernationConfig(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { - infraID := cd.Spec.ClusterMetadata.InfraID - - logger.WithField("count", len(instances)).Info("restoring instances from hibernation") +// Wait for instance cleanup +func (a *openstackActuator) waitForInstanceCleanup(openstackClient openstackclient.Client, infraID string, logger log.FieldLogger) error { + timeout := time.After(5 * time.Minute) + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() - // Check what instances already exist - existingServers, err := a.findInstancesByInfraID(openstackClient, infraID) - if err != nil { - logger.Warnf("could not check existing instances: %v", err) - existingServers = []*servers.Server{} // Assume none exist - } + logger.Info("waiting for OpenStack to clean up deleted instances...") - // Figure out what we need to create - instancesToCreate := a.findMissingInstances(instances, existingServers, logger) + for { + select { + case <-timeout: + return errors.New("timeout waiting for instance cleanup after 5 minutes") + case <-ticker.C: + matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) + if err != nil { + logger.Warnf("error checking for remaining instances: %v", err) + continue // Continue polling despite errors + } - if len(instancesToCreate) == 0 { - logger.Info("all instances already exist - continuing to verify they're active") - } else { - logger.WithField("count", len(instancesToCreate)).Info("need to create missing instances") + if len(matchingServers) == 0 { + logger.Info("all instances have been cleaned up") + return nil + } - // Create missing instances - err = a.createMissingInstances(openstackClient, instancesToCreate, logger) - if err != nil { - logger.Errorf("some instance creation failed: %v", err) + var instanceNames []string + for _, server := range matchingServers { + instanceNames = append(instanceNames, server.Name) + } + logger.Debugf("still waiting for %d instances to be cleaned up: %v", len(matchingServers), instanceNames) } } +} - // ALWAYS wait for instances to be active (regardless of whether we created new ones) - err = a.waitForAllInstancesToBeActive(openstackClient, infraID, len(instances), logger) - if err != nil { - return fmt.Errorf("not all instances are active yet: %v", err) +// Find missing instances +func (a *openstackActuator) findMissingInstances(expectedInstances []OpenStackInstanceConfig, existingServers []*servers.Server, logger log.FieldLogger) []OpenStackInstanceConfig { + existingNames := make(map[string]bool) + for _, server := range existingServers { + existingNames[server.Name] = true } - // Tags have to be added separately AFTER the instances are ACTIVE - a.restoreInstanceTags(openstackClient, instances, logger) - - // ALWAYS clean up hibernation snapshots after successful restoration - logger.Info("cleaning up hibernation snapshots after successful restoration") - err = a.cleanupRestorationSnapshots(openstackClient, instances, logger) - if err != nil { - // Log but don't fail - restoration succeeded, cleanup is best-effort - logger.Warnf("failed to cleanup some snapshots: %v", err) + var missingInstances []OpenStackInstanceConfig + for _, expected := range expectedInstances { + if !existingNames[expected.Name] { + logger.WithField("instance", expected.Name).Info("instance is missing") + missingInstances = append(missingInstances, expected) + } } - // ALWAYS clear hibernation config when we have confirmed instances are running - logger.Info("all instances confirmed active - clearing hibernation configuration") - return a.deleteHibernationConfigSecret(cd, hiveClient, logger) + return missingInstances } -// Create missing instances during restoration +// Create missing instances func (a *openstackActuator) createMissingInstances(openstackClient openstackclient.Client, instancesToCreate []OpenStackInstanceConfig, logger log.FieldLogger) error { - ctx := context.Background() var errs []error - for i, instance := range instancesToCreate { - instanceLogger := logger.WithFields(log.Fields{ - "current": i + 1, - "total": len(instancesToCreate), - "instance": instance.Name, - }) - - instanceLogger.Info("creating missing instance") + for _, instance := range instancesToCreate { + instanceLogger := logger.WithField("instance", instance.Name) - // Validate snapshot still exists - _, err := openstackClient.GetImage(ctx, instance.SnapshotID) + // Verify snapshot still exists + _, err := openstackClient.GetImage(context.Background(), instance.SnapshotID) if err != nil { - err := fmt.Errorf("snapshot %s not found for %s: %w", instance.SnapshotID, instance.Name, err) - errs = append(errs, err) - logger.WithField("error", err).Error("snapshot validation failed") + errs = append(errs, errors.Wrapf(err, "snapshot %s not found for instance %s", instance.SnapshotID, instance.Name)) continue } - // Build server creation options with complete metadata + // Build server creation options createOpts := &servers.CreateOpts{ Name: instance.Name, ImageRef: instance.SnapshotID, @@ -443,22 +555,24 @@ func (a *openstackActuator) createMissingInstances(openstackClient openstackclie Metadata: instance.Metadata, } - newServer, err := openstackClient.CreateServerFromOpts(ctx, createOpts) + newServer, err := openstackClient.CreateServerFromOpts(context.Background(), createOpts) if err != nil { - err := fmt.Errorf("failed to create instance %s: %w", instance.Name, err) - errs = append(errs, err) - logger.WithField("error", err).Error("instance creation failed") + // Check if instance already exists + if strings.Contains(err.Error(), "already exists") || strings.Contains(err.Error(), "409") { + instanceLogger.Info("instance already exists") + continue + } + errs = append(errs, errors.Wrapf(err, "failed to create instance %s", instance.Name)) continue } instanceLogger.WithField("server_id", newServer.ID).Info("created instance") - } return utilerrors.NewAggregate(errs) } -// Wait for ALL instances to be active +// Wait for all instances to be active func (a *openstackActuator) waitForAllInstancesToBeActive(openstackClient openstackclient.Client, infraID string, expectedCount int, logger log.FieldLogger) error { timeout := time.After(30 * time.Minute) ticker := time.NewTicker(45 * time.Second) @@ -469,24 +583,21 @@ func (a *openstackActuator) waitForAllInstancesToBeActive(openstackClient openst for { select { case <-timeout: - return fmt.Errorf("timeout waiting for all instances to become active after %v", timeout) + return errors.New("timeout waiting for instances to become active") case <-ticker.C: - // Get current instances - currentServers, err := a.findInstancesByInfraID(openstackClient, infraID) + currentServers, err := a.findInstancesByNamePrefix(openstackClient, infraID) if err != nil { - logger.Warnf("error checking instance status: %v", err) + logger.Warnf("error checking instances: %v", err) continue } - if len(currentServers) != expectedCount { - logger.Infof("have %d instances, expecting %d - still waiting", len(currentServers), expectedCount) + if len(currentServers) < expectedCount { + logger.Debugf("have %d/%d instances", len(currentServers), expectedCount) continue } - // Check if all instances are ACTIVE activeCount := 0 var nonActiveInstances []string - for _, server := range currentServers { if server.Status == "ACTIVE" { activeCount++ @@ -496,188 +607,101 @@ func (a *openstackActuator) waitForAllInstancesToBeActive(openstackClient openst } if activeCount == expectedCount { - logger.Info("all instances are active!") + logger.Info("all instances are active") return nil } - logger.Infof("%d/%d instances active, waiting for: %v", activeCount, expectedCount, nonActiveInstances) + logger.Debugf("%d/%d instances active, waiting for: %v", activeCount, expectedCount, nonActiveInstances) } } } -// MachinesRunning checks if machines are running -func (a *openstackActuator) MachinesRunning(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) (bool, []string, error) { - logger = logger.WithField("cloud", "openstack") - logger.Info("checking if machines are running") - - openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) - if err != nil { - return false, nil, fmt.Errorf("failed to create OpenStack client: %v", err) - } - - infraID := cd.Spec.ClusterMetadata.InfraID - matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) - if err != nil { - return false, nil, fmt.Errorf("error finding instances: %v", err) - } - - logger.Infof("found %d instances with prefix '%s'", len(matchingServers), infraID) - - if len(matchingServers) == 0 { - logger.Info("no instances found - machines are not running") - return false, []string{"no instances found"}, nil - } - - // Check actual instance states - runningCount, deletingInstances := a.categorizeInstanceStates(openstackClient, matchingServers, logger) - - // If instances are being deleted, hibernation is in progress - if len(deletingInstances) > 0 && runningCount == 0 { - logger.Infof("all instances are being deleted (%v) - hibernation in progress", deletingInstances) - return false, []string{"instances-being-deleted"}, nil - } - - return runningCount > 0, []string{}, nil -} - -// Check the actual state of instances in OpenStack -func (a *openstackActuator) categorizeInstanceStates(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) (int, []string) { - runningCount := 0 - var deletingInstances []string +// Restore instance tags +func (a *openstackActuator) restoreInstanceTags(openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { + var errs []error - for _, server := range servers { - status := strings.ToLower(server.Status) - if strings.Contains(status, "delet") || status == "shutoff" || status == "error" { - logger.Infof("instance %s is being deleted/stopped (status: %s)", server.Name, server.Status) - deletingInstances = append(deletingInstances, server.Name) - } else { - logger.Infof("instance %s is running (status: %s)", server.Name, server.Status) - runningCount++ + for _, instance := range instances { + if len(instance.Tags) == 0 { + continue } - } - - return runningCount, deletingInstances -} - -// Check if machines are stopped -func (a *openstackActuator) MachinesStopped(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) (bool, []string, error) { - logger = logger.WithField("cloud", "openstack") - logger.Info("checking if machines are stopped") - - openstackClient, err := a.openstackClientFn(cd, hiveClient, logger) - if err != nil { - return false, nil, fmt.Errorf("failed to create OpenStack client: %v", err) - } - - infraID := cd.Spec.ClusterMetadata.InfraID - matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) - if err != nil { - return false, nil, fmt.Errorf("error finding instances: %v", err) - } - - if len(matchingServers) == 0 { - logger.Info("no instances found - machines are stopped") - return true, nil, nil - } - - var notStopped []string - for _, server := range matchingServers { - notStopped = append(notStopped, server.Name) - } - - logger.Infof("found %d instances still running", len(notStopped)) - return false, notStopped, nil -} -type OpenStackInstanceConfig struct { - Name string `json:"name"` - Flavor string `json:"flavor"` - PortID string `json:"portID"` - SnapshotID string `json:"snapshotID"` - SnapshotName string `json:"snapshotName"` - SecurityGroups []string `json:"securityGroups"` - ClusterID string `json:"clusterID"` - NetworkID string `json:"networkID"` - OpenshiftClusterID string `json:"openshiftClusterID"` - Metadata map[string]string `json:"metadata"` - Tags []string `json:"tags"` -} + servers, err := a.findInstancesByNamePrefix(openstackClient, instance.ClusterID) + if err != nil { + errs = append(errs, errors.Wrapf(err, "failed to find instances for tag restoration")) + continue + } -// Return servers that match the infraID prefix -func (a *openstackActuator) findInstancesByInfraID(openstackClient openstackclient.Client, prefix string) ([]*servers.Server, error) { - ctx := context.Background() + found := false + for _, server := range servers { + if server.Name == instance.Name { + found = true - allServers, err := openstackClient.ListServers(ctx, nil) - if err != nil { - return nil, fmt.Errorf("error listing servers: %v", err) - } + err = openstackClient.SetServerTags(context.Background(), server.ID, instance.Tags) + if err != nil { + errs = append(errs, errors.Wrapf(err, "failed to restore tags for %s", instance.Name)) + } else { + logger.WithField("instance", instance.Name).Info("restored server tags") + } + break + } + } - var matchingServers []*servers.Server - for _, server := range allServers { - if strings.HasPrefix(server.Name, prefix) { - matchingServers = append(matchingServers, &server) + if !found { + errs = append(errs, errors.Errorf("instance %s not found", instance.Name)) } } - return matchingServers, nil + return utilerrors.NewAggregate(errs) } -// Configuration persistence methods -func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, servers []*servers.Server, snapshotIDs []string, snapshotNames []string, logger log.FieldLogger) error { - ctx := context.Background() - +// Save instance configuration +func (a *openstackActuator) saveInstanceConfiguration(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, servers []*servers.Server, snapshotMapping map[string]string, logger log.FieldLogger) error { if len(servers) == 0 { return nil } - if len(snapshotIDs) != len(servers) { - return fmt.Errorf("mismatch between servers (%d) and snapshot IDs (%d)", len(servers), len(snapshotIDs)) - } - - if len(snapshotNames) != len(servers) { - return fmt.Errorf("mismatch between servers (%d) and snapshot names (%d)", len(servers), len(snapshotNames)) - } - - // Get InfraID infraID := cd.Spec.ClusterMetadata.InfraID // Get shared configuration networkID, err := a.getNetworkIDForCluster(openstackClient, infraID) if err != nil { - return fmt.Errorf("error getting network ID: %v", err) + return errors.Wrap(err, "error getting network ID") } // Get openshiftClusterID from first instance - server := servers[0] var openshiftClusterID string - if server.Metadata != nil { - if id, exists := server.Metadata["openshiftClusterID"]; exists { + if len(servers) > 0 && servers[0].Metadata != nil { + if id, exists := servers[0].Metadata["openshiftClusterID"]; exists { openshiftClusterID = id } } // Get all ports - allPorts, err := openstackClient.ListPorts(ctx) + allPorts, err := openstackClient.ListPorts(context.Background()) if err != nil { - return fmt.Errorf("error listing ports: %v", err) + return errors.Wrap(err, "error listing ports") } // Build configuration for each instance var instanceConfigs []OpenStackInstanceConfig - for i, server := range servers { + var errs []error + + for _, server := range servers { // Get flavor ID var flavorID string if server.Flavor != nil { if id, ok := server.Flavor["id"].(string); ok { flavorID = id } else { - return fmt.Errorf("could not extract flavor ID for %s", server.Name) + errs = append(errs, errors.Errorf("could not extract flavor ID for %s", server.Name)) + continue } } else { - return fmt.Errorf("no flavor information found for %s", server.Name) + errs = append(errs, errors.Errorf("no flavor information found for %s", server.Name)) + continue } - // Find maching port + // Find matching port var portID string for _, port := range allPorts { if port.Name == server.Name || port.Name == server.Name+"-0" { @@ -687,31 +711,44 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster } if portID == "" { - return fmt.Errorf("no port found for instance %s", server.Name) + errs = append(errs, errors.Errorf("no port found for instance %s", server.Name)) + continue } // Get security groups - secGroups, err := openstackClient.GetServerSecurityGroupNames(ctx, server.ID) + secGroups, err := openstackClient.GetServerSecurityGroupNames(context.Background(), server.ID) if err != nil { - return fmt.Errorf("error getting security groups for %s: %v", server.Name, err) + errs = append(errs, errors.Wrapf(err, "error getting security groups for %s", server.Name)) + continue } // Get tags - serverTags, err := openstackClient.GetServerTags(ctx, server.ID) + serverTags, err := openstackClient.GetServerTags(context.Background(), server.ID) if err != nil { logger.Warnf("failed to get tags for %s: %v", server.Name, err) serverTags = []string{} } - // Use the snapshot name directly - snapshotName := snapshotNames[i] + // Get snapshot info + snapshotID, exists := snapshotMapping[server.ID] + if !exists { + errs = append(errs, errors.Errorf("no snapshot found for instance %s", server.Name)) + continue + } + + // Get snapshot name + snapshot, err := openstackClient.GetImage(context.Background(), snapshotID) + if err != nil { + errs = append(errs, errors.Wrapf(err, "failed to get snapshot details for %s", snapshotID)) + continue + } instanceConfigs = append(instanceConfigs, OpenStackInstanceConfig{ Name: server.Name, Flavor: flavorID, PortID: portID, - SnapshotID: snapshotIDs[i], - SnapshotName: snapshotName, + SnapshotID: snapshotID, + SnapshotName: snapshot.Name, SecurityGroups: secGroups, ClusterID: infraID, NetworkID: networkID, @@ -721,22 +758,23 @@ func (a *openstackActuator) saveInstanceConfigurationToSecret(cd *hivev1.Cluster }) logger.WithFields(log.Fields{ - "instance": server.Name, - "snapshot_id": snapshotIDs[i], - "snapshot_name": snapshotName, + "instance": server.Name, + "snapshot_id": snapshotID, }).Info("saved snapshot info") } - return a.saveHibernationConfigToSecret(cd, hiveClient, instanceConfigs, logger) -} + if len(errs) > 0 { + return utilerrors.NewAggregate(errs) + } -// Store hibernation information to secrets -func (a *openstackActuator) saveHibernationConfigToSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, instanceConfigs []OpenStackInstanceConfig, logger log.FieldLogger) error { - ctx := context.Background() + return a.saveHibernationConfig(cd, hiveClient, instanceConfigs, logger) +} +// Save hibernation config to secret +func (a *openstackActuator) saveHibernationConfig(cd *hivev1.ClusterDeployment, hiveClient client.Client, instanceConfigs []OpenStackInstanceConfig, logger log.FieldLogger) error { configData, err := json.Marshal(instanceConfigs) if err != nil { - return fmt.Errorf("failed to marshal hibernation config: %v", err) + return errors.Wrap(err, "failed to marshal hibernation config") } secretName := fmt.Sprintf("%s-hibernation-config", cd.Name) @@ -763,23 +801,26 @@ func (a *openstackActuator) saveHibernationConfigToSecret(cd *hivev1.ClusterDepl }, } - err = hiveClient.Create(ctx, secret) + err = hiveClient.Create(context.Background(), secret) if err != nil { if !apierrors.IsAlreadyExists(err) { - return fmt.Errorf("failed to create hibernation config secret: %v", err) + return errors.Wrap(err, "failed to create hibernation config secret") } // Update existing secret existingSecret := &corev1.Secret{} - err = hiveClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: cd.Namespace}, existingSecret) + err = hiveClient.Get(context.Background(), types.NamespacedName{ + Name: secretName, + Namespace: cd.Namespace, + }, existingSecret) if err != nil { - return fmt.Errorf("failed to get existing hibernation config secret: %v", err) + return errors.Wrap(err, "failed to get existing hibernation config secret") } existingSecret.Data = secret.Data - err = hiveClient.Update(ctx, existingSecret) + err = hiveClient.Update(context.Background(), existingSecret) if err != nil { - return fmt.Errorf("failed to update hibernation config secret: %v", err) + return errors.Wrap(err, "failed to update hibernation config secret") } } @@ -787,305 +828,226 @@ func (a *openstackActuator) saveHibernationConfigToSecret(cd *hivev1.ClusterDepl return nil } -// Waits for OpenStack to fully remove an instance -func (a *openstackActuator) waitForInstanceCleanup(openstackClient openstackclient.Client, infraID string, logger log.FieldLogger) error { - timeout := time.After(5 * time.Minute) - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - logger.Info("waiting for OpenStack to clean up deleted instances...") - - for { - select { - case <-timeout: - return fmt.Errorf("timeout waiting for instance cleanup after %v", timeout) - case <-ticker.C: - matchingServers, err := a.findInstancesByInfraID(openstackClient, infraID) - if err != nil { - logger.Warnf("error checking for remaining instances: %v", err) - continue // Continue polling despite errors - } - - if len(matchingServers) == 0 { - logger.Info("all instances have been cleaned up") - return nil - } - - // Log remaining instances - var instanceNames []string - for _, server := range matchingServers { - instanceNames = append(instanceNames, server.Name) - } - logger.Infof("still waiting for %d instances to be cleaned up: %v", len(matchingServers), instanceNames) - } - } -} - -func (a *openstackActuator) restoreInstanceTags(openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) { - ctx := context.Background() - - for _, instance := range instances { - if len(instance.Tags) > 0 { - // Find the active instance by name - servers, err := a.findInstancesByInfraID(openstackClient, instance.ClusterID) - if err != nil { - continue - } - - for _, server := range servers { - if server.Name == instance.Name { - err = openstackClient.SetServerTags(ctx, server.ID, instance.Tags) - if err != nil { - logger.WithFields(log.Fields{ - "instance": instance.Name, - "error": err, - "tags": instance.Tags, - }).Warn("failed to restore server tags") - } else { - logger.WithFields(log.Fields{ - "instance": instance.Name, - "tag_count": len(instance.Tags), - }).Info("restored server tags") - } - break - } - } - } - } -} - -// Get stored hibernation information from secret -func (a *openstackActuator) loadHibernationConfigFromSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) ([]OpenStackInstanceConfig, error) { - ctx := context.Background() +// Load hibernation config from secret +func (a *openstackActuator) loadHibernationConfig(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) ([]OpenStackInstanceConfig, error) { secretName := fmt.Sprintf("%s-hibernation-config", cd.Name) secret := &corev1.Secret{} - err := hiveClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: cd.Namespace}, secret) + err := hiveClient.Get(context.Background(), types.NamespacedName{ + Name: secretName, + Namespace: cd.Namespace, + }, secret) if err != nil { if apierrors.IsNotFound(err) { - return nil, fmt.Errorf("hibernation config secret not found") + return nil, errors.New("hibernation config secret not found") } - return nil, fmt.Errorf("failed to get hibernation config secret: %v", err) + return nil, errors.Wrap(err, "failed to get hibernation config secret") } configData, exists := secret.Data["hibernation-config"] if !exists { - return nil, fmt.Errorf("hibernation config not found in secret") + return nil, errors.New("hibernation config not found in secret") } var instanceConfigs []OpenStackInstanceConfig err = json.Unmarshal(configData, &instanceConfigs) if err != nil { - return nil, fmt.Errorf("failed to unmarshal hibernation config: %v", err) + return nil, errors.Wrap(err, "failed to unmarshal hibernation config") } logger.Infof("loaded hibernation configuration from secret %s (%d instances)", secretName, len(instanceConfigs)) return instanceConfigs, nil } -// Delete stored hibernation information from secret -func (a *openstackActuator) deleteHibernationConfigSecret(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { - ctx := context.Background() +// Delete hibernation config secret +func (a *openstackActuator) deleteHibernationConfig(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) error { secretName := fmt.Sprintf("%s-hibernation-config", cd.Name) secret := &corev1.Secret{} - err := hiveClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: cd.Namespace}, secret) + err := hiveClient.Get(context.Background(), types.NamespacedName{ + Name: secretName, + Namespace: cd.Namespace, + }, secret) if err != nil { if apierrors.IsNotFound(err) { - logger.Info("hibernation config secret already deleted") + logger.Debug("hibernation config secret already deleted") return nil } - return fmt.Errorf("failed to get hibernation config secret: %v", err) + return errors.Wrap(err, "failed to get hibernation config secret") } - err = hiveClient.Delete(ctx, secret) + err = hiveClient.Delete(context.Background(), secret) if err != nil { - return fmt.Errorf("failed to delete hibernation config secret: %v", err) + return errors.Wrap(err, "failed to delete hibernation config secret") } logger.Infof("deleted hibernation config secret %s", secretName) return nil } -// Find the network ID for a specific cluster using infraID -func (a *openstackActuator) getNetworkIDForCluster(openstackClient openstackclient.Client, infraID string) (string, error) { - ctx := context.Background() - networkName := fmt.Sprintf("%s-openshift", infraID) - - network, err := openstackClient.GetNetworkByName(ctx, networkName) - if err != nil { - return "", fmt.Errorf("failed to find network '%s': %w", networkName, err) - } - - return network.ID, nil -} - -func (a *openstackActuator) cleanupRestorationSnapshots(openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { - ctx := context.Background() - - logger.Info("attempting best-effort cleanup of restoration snapshots") - - successCount := 0 - for _, instance := range instances { - if instance.SnapshotID != "" { - logger.Infof("attempting to delete restoration snapshot %s for instance %s", instance.SnapshotID, instance.Name) - err := openstackClient.DeleteImage(ctx, instance.SnapshotID) - if err != nil { - if strings.Contains(err.Error(), "in use") || strings.Contains(err.Error(), "409") { - logger.Infof("snapshot %s still in use (will be cleaned up next hibernation cycle)", instance.SnapshotID) - } else { - logger.Warnf("failed to delete snapshot %s: %v", instance.SnapshotID, err) - } - } else { - logger.Infof("successfully deleted restoration snapshot %s", instance.SnapshotID) - successCount++ - } - } - } - - logger.Infof("restoration snapshot cleanup: %d/%d snapshots deleted (remaining will be cleaned up next hibernation)", successCount, len(instances)) - return nil // Always succeed -} - -func (a *openstackActuator) cleanupOldSnapshotsAfterDeletion(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, infraID string, currentSnapshotIDs []string, logger log.FieldLogger) error { +// Cleanup old snapshots +func (a *openstackActuator) cleanupOldSnapshots(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, infraID string, currentSnapshots map[string]string, logger log.FieldLogger) error { // Get all hibernation snapshots for this cluster - allSnapshots, err := a.findHibernationSnapshotsByExactNames(cd, hiveClient, openstackClient, infraID, logger) + allSnapshots, err := a.findHibernationSnapshots(openstackClient, infraID, logger) if err != nil { - return err + return errors.Wrap(err, "failed to find hibernation snapshots") } if len(allSnapshots) == 0 { - logger.Info("no hibernation snapshots found") + logger.Debug("no hibernation snapshots found for cleanup") return nil } - // Create map of current snapshot IDs for exclusion - currentIDs := sets.NewString(currentSnapshotIDs...) + // Create set of current snapshot IDs + currentIDs := sets.NewString() + for _, snapshotID := range currentSnapshots { + currentIDs.Insert(snapshotID) + } - // Filter out current snapshots - only delete OLD ones - var oldSnapshots []images.Image + // Delete old snapshots + var errs []error + deleted := 0 for _, snapshot := range allSnapshots { - if !currentIDs.Has(snapshot.ID) { - oldSnapshots = append(oldSnapshots, snapshot) - logger.Infof("found OLD snapshot to delete: %s (ID: %s)", snapshot.Name, snapshot.ID) + if currentIDs.Has(snapshot.ID) { + logger.Debugf("keeping current snapshot %s", snapshot.Name) + continue + } + + logger.WithField("snapshot", snapshot.Name).Info("deleting old hibernation snapshot") + err := openstackClient.DeleteImage(context.Background(), snapshot.ID) + if err != nil { + if !isNotFoundError(err) { + errs = append(errs, errors.Wrapf(err, "failed to delete old snapshot %s", snapshot.Name)) + } } else { - logger.Infof("keeping NEW snapshot: %s (ID: %s)", snapshot.Name, snapshot.ID) + deleted++ } } - if len(oldSnapshots) == 0 { - logger.Info("no old snapshots to clean up") - return nil + if deleted > 0 { + logger.Infof("deleted %d old hibernation snapshots", deleted) } - logger.Infof("deleting %d old hibernation snapshots", len(oldSnapshots)) + return utilerrors.NewAggregate(errs) +} + +// Cleanup restoration snapshots +func (a *openstackActuator) cleanupRestorationSnapshots(openstackClient openstackclient.Client, instances []OpenStackInstanceConfig, logger log.FieldLogger) error { + logger.Info("attempting best-effort cleanup of restoration snapshots") + var errs []error successCount := 0 - for _, snapshot := range oldSnapshots { - logger.Infof("deleting old hibernation snapshot: %s", snapshot.Name) - err := openstackClient.DeleteImage(context.Background(), snapshot.ID) + for _, instance := range instances { + if instance.SnapshotID == "" { + continue + } + + logger.Debugf("attempting to delete restoration snapshot %s for instance %s", instance.SnapshotID, instance.Name) + err := openstackClient.DeleteImage(context.Background(), instance.SnapshotID) if err != nil { - logger.Warnf("failed to delete old snapshot %s: %v", snapshot.Name, err) + if strings.Contains(err.Error(), "in use") || strings.Contains(err.Error(), "409") { + logger.Debugf("snapshot %s still in use (will be cleaned up next hibernation cycle)", instance.SnapshotID) + } else if !isNotFoundError(err) { + errs = append(errs, errors.Wrapf(err, "failed to delete snapshot %s", instance.SnapshotID)) + } } else { - logger.Infof("successfully deleted old snapshot: %s", snapshot.Name) successCount++ } } - logger.Infof("old snapshot cleanup completed: %d/%d snapshots deleted", successCount, len(oldSnapshots)) - return nil + if successCount > 0 { + logger.Infof("deleted %d restoration snapshots", successCount) + } + + return utilerrors.NewAggregate(errs) } -func (a *openstackActuator) getExpectedSnapshotNames(cd *hivev1.ClusterDeployment, hiveClient client.Client, logger log.FieldLogger) ([]string, error) { - instances, err := a.loadHibernationConfigFromSecret(cd, hiveClient, logger) +// Using tags to filter the instances +func (a *openstackActuator) findInstancesByInfraID(openstackClient openstackclient.Client, infraID string) ([]*servers.Server, error) { + taggedServers, err := openstackClient.ListServers(context.Background(), &servers.ListOpts{ + Tags: fmt.Sprintf("openshiftClusterID=%s", infraID), + }) + if err != nil { - return nil, fmt.Errorf("failed to load hibernation config: %v", err) + return nil, errors.Wrap(err, "error listing servers by tag") } - var snapshotNames []string - for _, instance := range instances { - if instance.SnapshotName != "" { - snapshotNames = append(snapshotNames, instance.SnapshotName) - } + var matchingServers []*servers.Server + for i := range taggedServers { + matchingServers = append(matchingServers, &taggedServers[i]) } - - return snapshotNames, nil + return matchingServers, nil } -func (a *openstackActuator) findHibernationSnapshotsByExactNames(cd *hivev1.ClusterDeployment, hiveClient client.Client, openstackClient openstackclient.Client, infraID string, logger log.FieldLogger) ([]images.Image, error) { - ctx := context.Background() - - // Get the exact snapshot names from hibernation config - expectedSnapshotNames, err := a.getExpectedSnapshotNames(cd, hiveClient, logger) - +// Filter instances by the InfraID prefix +func (a *openstackActuator) findInstancesByNamePrefix(openstackClient openstackclient.Client, infraID string) ([]*servers.Server, error) { + allServers, err := openstackClient.ListServers(context.Background(), nil) if err != nil { - return nil, fmt.Errorf("failed to get expected snapshot names: %v", err) + return nil, errors.Wrap(err, "error listing servers") } - if len(expectedSnapshotNames) == 0 { - return []images.Image{}, nil + var matchingServers []*servers.Server + for i := range allServers { + if strings.HasPrefix(allServers[i].Name, infraID) { + matchingServers = append(matchingServers, &allServers[i]) + } } - nameFilter := fmt.Sprintf("in:%s", strings.Join(expectedSnapshotNames, ",")) - listOpts := &images.ListOpts{ - Name: nameFilter, - } + return matchingServers, nil +} + +// Get network ID for cluster +func (a *openstackActuator) getNetworkIDForCluster(openstackClient openstackclient.Client, infraID string) (string, error) { + networkName := fmt.Sprintf("%s-openshift", infraID) - hibernationSnapshots, err := openstackClient.ListImages(ctx, listOpts) + network, err := openstackClient.GetNetworkByName(context.Background(), networkName) if err != nil { - return nil, fmt.Errorf("failed to query snapshots by exact names: %v", err) + return "", errors.Wrapf(err, "failed to find network '%s'", networkName) } - logger.WithFields(log.Fields{ - "expected_count": len(expectedSnapshotNames), - "found_count": len(hibernationSnapshots), - }).Info("found snapshots by exact names") - - return hibernationSnapshots, nil + return network.ID, nil } -func (a *openstackActuator) pauseInstances(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) error { - ctx := context.Background() - - for i, server := range servers { - serverLogger := logger.WithFields(log.Fields{ - "current": i + 1, - "total": len(servers), - "instance": server.Name, - }) - - serverLogger.Info("pausing instance for snapshot consistency") +// Find hibernation snapshots by pattern +func (a *openstackActuator) findHibernationSnapshots(openstackClient openstackclient.Client, infraID string, logger log.FieldLogger) ([]images.Image, error) { + // List all images and filter by pattern + allImages, err := openstackClient.ListImages(context.Background(), nil) + if err != nil { + return nil, errors.Wrap(err, "failed to list images") + } - err := openstackClient.PauseServer(ctx, server.ID) - if err != nil { - // If pause fails, try to unpause any previously paused instances - if i > 0 { - logger.Warn("pause failed - attempting to unpause previously paused instances") - a.unpauseInstances(openstackClient, servers[:i], logger) - } - return fmt.Errorf("failed to pause instance %s: %w", server.Name, err) + var hibernationSnapshots []images.Image + for _, image := range allImages { + if strings.Contains(image.Name, "-hibernation-") && strings.HasPrefix(image.Name, infraID) { + hibernationSnapshots = append(hibernationSnapshots, image) } - - serverLogger.Info("instance paused successfully") } - logger.WithField("count", len(servers)).Info("all instances paused successfully") - return nil + logger.Debugf("found %d hibernation snapshots for cluster %s", len(hibernationSnapshots), infraID) + return hibernationSnapshots, nil } -// Unpause instances -func (a *openstackActuator) unpauseInstances(openstackClient openstackclient.Client, servers []*servers.Server, logger log.FieldLogger) { - ctx := context.Background() +// Find snapshot by name +func (a *openstackActuator) findSnapshotByName(openstackClient openstackclient.Client, snapshotName string) (*images.Image, error) { + listOpts := &images.ListOpts{ + Name: snapshotName, + } - for _, server := range servers { - serverLogger := logger.WithField("instance", server.Name) - serverLogger.Info("unpausing instance") + snapshots, err := openstackClient.ListImages(context.Background(), listOpts) + if err != nil { + return nil, err + } - err := openstackClient.UnpauseServer(ctx, server.ID) - if err != nil { - serverLogger.WithField("error", err).Error("failed to unpause instance") - } else { - serverLogger.Info("instance unpaused successfully") + for _, snapshot := range snapshots { + if snapshot.Name == snapshotName { + return &snapshot, nil } } + + return nil, errors.New("snapshot not found") +} + +// Helper utilities +func isNotFoundError(err error) bool { + return err != nil && (strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found")) } diff --git a/pkg/controller/hibernation/openstack_actuator_test.go b/pkg/controller/hibernation/openstack_actuator_test.go index 36e3ecd1f7c..0e18ab51ea9 100644 --- a/pkg/controller/hibernation/openstack_actuator_test.go +++ b/pkg/controller/hibernation/openstack_actuator_test.go @@ -55,7 +55,7 @@ func TestOpenStackStopMachines(t *testing.T) { }, }, { - name: "stop running instances with pause/unpause flow", + name: "stop running instances with pause flow", instances: map[string]int{"ACTIVE": 2}, setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { // Initial server discovery @@ -64,16 +64,23 @@ func TestOpenStackStopMachines(t *testing.T) { {ID: "server-2", Name: "testinfra-worker-0", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, }, nil).Times(1) + // Check status before pause (idempotency) + c.EXPECT().GetServer(gomock.Any(), "server-1").Return(&servers.Server{ID: "server-1", Status: "ACTIVE"}, nil).Times(1) + c.EXPECT().GetServer(gomock.Any(), "server-2").Return(&servers.Server{ID: "server-2", Status: "ACTIVE"}, nil).Times(1) + // Pause all instances c.EXPECT().PauseServer(gomock.Any(), "server-1").Return(nil).Times(1) c.EXPECT().PauseServer(gomock.Any(), "server-2").Return(nil).Times(1) + // Check for existing snapshots (idempotency) + c.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return([]images.Image{}, nil).Times(2) + // Create snapshots (timestamp will be consistent) c.EXPECT().CreateServerSnapshot(gomock.Any(), "server-1", gomock.Any()).Return("snapshot-1", nil).Times(1) c.EXPECT().CreateServerSnapshot(gomock.Any(), "server-2", gomock.Any()).Return("snapshot-2", nil).Times(1) // Wait for snapshots to complete - activeImg := &images.Image{Status: "active"} + activeImg := &images.Image{Status: "active", Name: "test-snapshot"} c.EXPECT().GetImage(gomock.Any(), "snapshot-1").Return(activeImg, nil).AnyTimes() c.EXPECT().GetImage(gomock.Any(), "snapshot-2").Return(activeImg, nil).AnyTimes() @@ -101,7 +108,7 @@ func TestOpenStackStopMachines(t *testing.T) { }, }, { - name: "pause failure should unpause previous instances", + name: "pause failure - no rollback", instances: map[string]int{"ACTIVE": 2}, setupClient: func(t *testing.T, c *mockopenstackclient.MockClient) { // Initial server discovery @@ -110,13 +117,16 @@ func TestOpenStackStopMachines(t *testing.T) { {ID: "server-2", Name: "testinfra-worker-0", Status: "ACTIVE"}, }, nil).Times(1) + // Check status before pause (idempotency) + c.EXPECT().GetServer(gomock.Any(), "server-1").Return(&servers.Server{ID: "server-1", Status: "ACTIVE"}, nil).Times(1) + c.EXPECT().GetServer(gomock.Any(), "server-2").Return(&servers.Server{ID: "server-2", Status: "ACTIVE"}, nil).Times(1) + // First pause succeeds c.EXPECT().PauseServer(gomock.Any(), "server-1").Return(nil).Times(1) // Second pause fails c.EXPECT().PauseServer(gomock.Any(), "server-2").Return(errors.New("pause failed")).Times(1) - // Should unpause the first server due to failure - c.EXPECT().UnpauseServer(gomock.Any(), "server-1").Return(nil).Times(1) + // NO ROLLBACK - we removed unpause logic }, expectErr: true, }, @@ -129,14 +139,20 @@ func TestOpenStackStopMachines(t *testing.T) { {ID: "server-1", Name: "testinfra-master-0", Status: "ACTIVE", Flavor: map[string]interface{}{"id": "flavor-1"}}, }, nil).Times(1) + // Check status before pause + c.EXPECT().GetServer(gomock.Any(), "server-1").Return(&servers.Server{ID: "server-1", Status: "ACTIVE"}, nil).Times(1) + // Pause c.EXPECT().PauseServer(gomock.Any(), "server-1").Return(nil).Times(1) + // Check for existing snapshot + c.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return([]images.Image{}, nil).Times(1) + // Snapshot creation c.EXPECT().CreateServerSnapshot(gomock.Any(), "server-1", gomock.Any()).Return("snapshot-new", nil).Times(1) // Wait for snapshot - activeImg := &images.Image{Status: "active"} + activeImg := &images.Image{Status: "active", Name: "test-snapshot"} c.EXPECT().GetImage(gomock.Any(), "snapshot-new").Return(activeImg, nil).AnyTimes() // Network operations @@ -408,7 +424,7 @@ func TestOpenStackMachinesRunning(t *testing.T) { {ID: "2", Name: "testinfra-worker-0", Status: "DELETING"}, }, expectedRunning: false, - expectedNames: []string{"instances-being-deleted"}, + expectedNames: []string{"testinfra-master-0", "testinfra-worker-0"}, }, { name: "mixed states", @@ -416,8 +432,8 @@ func TestOpenStackMachinesRunning(t *testing.T) { {ID: "1", Name: "testinfra-master-0", Status: "ACTIVE"}, {ID: "2", Name: "testinfra-worker-0", Status: "SHUTOFF"}, }, - expectedRunning: true, // At least one is running - expectedNames: []string{}, + expectedRunning: false, + expectedNames: []string{"testinfra-worker-0"}, }, } @@ -487,6 +503,33 @@ func TestOpenStackMachinesStopped(t *testing.T) { // Helper functions +func TestIdempotentPause(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + openstackClient := mockopenstackclient.NewMockClient(ctrl) + + testServers := []*servers.Server{ + {ID: "server-1", Name: "testinfra-master-0", Status: "ACTIVE"}, + {ID: "server-2", Name: "testinfra-worker-0", Status: "PAUSED"}, + } + + // First server - active, needs pause + openstackClient.EXPECT().GetServer(gomock.Any(), "server-1").Return(&servers.Server{ + ID: "server-1", Status: "ACTIVE", + }, nil).Times(1) + openstackClient.EXPECT().PauseServer(gomock.Any(), "server-1").Return(nil).Times(1) + + // Second server - already paused, no pause call + openstackClient.EXPECT().GetServer(gomock.Any(), "server-2").Return(&servers.Server{ + ID: "server-2", Status: "PAUSED", + }, nil).Times(1) + + actuator := testOpenStackActuator(openstackClient) + err := actuator.pauseInstances(openstackClient, testServers, log.New()) + assert.Nil(t, err) +} + func testOpenStackActuator(openstackClient openstackclient.Client) *openstackActuator { return &openstackActuator{ openstackClientFn: func(*hivev1.ClusterDeployment, client.Client, log.FieldLogger) (openstackclient.Client, error) { @@ -572,63 +615,6 @@ func testHibernationSecretWithMetadata() *corev1.Secret { } } -func TestExactNameFiltering(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - openstackClient := mockopenstackclient.NewMockClient(ctrl) - - // Mock the exact name filtering call - expectedSnapshots := []images.Image{ - {ID: "snapshot-1", Name: "testinfra-master-0-hibernation-20240101-120000"}, - {ID: "snapshot-2", Name: "testinfra-worker-0-hibernation-20240101-120000"}, - } - - openstackClient.EXPECT().ListImages(gomock.Any(), gomock.Any()).DoAndReturn( - func(ctx context.Context, opts *images.ListOpts) ([]images.Image, error) { - // Verify the "in:" filter is being used - expectedFilter := "in:testinfra-master-0-hibernation-20240101-120000,testinfra-worker-0-hibernation-20240101-120000" - assert.Equal(t, expectedFilter, opts.Name) - return expectedSnapshots, nil - }).Times(1) - - actuator := testOpenStackActuator(openstackClient) - c := testfake.NewFakeClientBuilder().WithRuntimeObjects(testHibernationSecretWithMetadata()).Build() - - cd := testOpenStackClusterDeployment() - snapshots, err := actuator.findHibernationSnapshotsByExactNames(cd, c, openstackClient, "testinfra", log.New()) - - assert.Nil(t, err) - assert.Equal(t, 2, len(snapshots)) - assert.Equal(t, "snapshot-1", snapshots[0].ID) -} - -func TestSnapshotCleanupWithExactFiltering(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - openstackClient := mockopenstackclient.NewMockClient(ctrl) - - // Mock exact name filtering returning both old and new snapshots - allSnapshots := []images.Image{ - {ID: "snapshot-new", Name: "testinfra-master-0-hibernation-20240201-120000"}, // Current - {ID: "snapshot-old", Name: "testinfra-master-0-hibernation-20240101-120000"}, // Old - } - - openstackClient.EXPECT().ListImages(gomock.Any(), gomock.Any()).Return(allSnapshots, nil).Times(1) - - // Should only delete the old snapshot - openstackClient.EXPECT().DeleteImage(gomock.Any(), "snapshot-old").Return(nil).Times(1) - - actuator := testOpenStackActuator(openstackClient) - c := testfake.NewFakeClientBuilder().WithRuntimeObjects(testHibernationSecretWithMetadata()).Build() - - cd := testOpenStackClusterDeployment() - err := actuator.cleanupOldSnapshotsAfterDeletion(cd, c, openstackClient, "testinfra", []string{"snapshot-new"}, log.New()) - - assert.Nil(t, err) -} - func TestStartMachinesWithTagRestoration(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -672,30 +658,4 @@ func TestStartMachinesWithTagRestoration(t *testing.T) { err := actuator.StartMachines(testOpenStackClusterDeployment(), c, log.New()) assert.Nil(t, err) -} - -func TestPauseUnpauseFlow(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - openstackClient := mockopenstackclient.NewMockClient(ctrl) - - servers := []*servers.Server{ - {ID: "server-1", Name: "testinfra-master-0", Status: "ACTIVE"}, - {ID: "server-2", Name: "testinfra-worker-0", Status: "ACTIVE"}, - } - - // Test successful pause - openstackClient.EXPECT().PauseServer(gomock.Any(), "server-1").Return(nil).Times(1) - openstackClient.EXPECT().PauseServer(gomock.Any(), "server-2").Return(nil).Times(1) - - actuator := testOpenStackActuator(openstackClient) - err := actuator.pauseInstances(openstackClient, servers, log.New()) - assert.Nil(t, err) - - // Test unpause - openstackClient.EXPECT().UnpauseServer(gomock.Any(), "server-1").Return(nil).Times(1) - openstackClient.EXPECT().UnpauseServer(gomock.Any(), "server-2").Return(nil).Times(1) - - actuator.unpauseInstances(openstackClient, servers, log.New()) -} +} \ No newline at end of file From 4e7ce1518fd6dd5cce5913a5ccbad7829dd5e5d7 Mon Sep 17 00:00:00 2001 From: daturece Date: Wed, 12 Nov 2025 09:50:35 +0100 Subject: [PATCH 8/8] run update-gofmt --- pkg/controller/hibernation/openstack_actuator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/hibernation/openstack_actuator_test.go b/pkg/controller/hibernation/openstack_actuator_test.go index 0e18ab51ea9..4fbc5643392 100644 --- a/pkg/controller/hibernation/openstack_actuator_test.go +++ b/pkg/controller/hibernation/openstack_actuator_test.go @@ -658,4 +658,4 @@ func TestStartMachinesWithTagRestoration(t *testing.T) { err := actuator.StartMachines(testOpenStackClusterDeployment(), c, log.New()) assert.Nil(t, err) -} \ No newline at end of file +}