forked from sardinasystems/fleeting-plugin-openstack
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
248 additions
and
157 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,190 @@ | ||
package openstackclient | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"fmt" | ||
"os" | ||
|
||
"github.com/gophercloud/gophercloud/v2" | ||
"github.com/gophercloud/gophercloud/v2/openstack" | ||
"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" | ||
"github.com/gophercloud/gophercloud/v2/openstack/config" | ||
"github.com/gophercloud/gophercloud/v2/openstack/config/clouds" | ||
"github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" | ||
"github.com/gophercloud/gophercloud/v2/openstack/utils" | ||
"github.com/mitchellh/mapstructure" | ||
) | ||
|
||
type AuthConfig struct { | ||
// AuthFromEnv specifies whether to use environment variables for auth | ||
AuthFromEnv bool | ||
|
||
// Cloud is the name of the cloud config from clouds.yaml to use | ||
Cloud string | ||
|
||
// CloudsConfig is the path to the clouds.yaml file | ||
CloudsConfig string | ||
|
||
// NovaMicroversion is the microversion of the OpenStack Nova client. Default 2.79 (which should be ok for Train+) | ||
NovaMicroversion string | ||
} | ||
|
||
// Some good known properties useful for setting up ConnectInfo | ||
// | ||
// See also: https://docs.openstack.org/glance/latest/admin/useful-image-properties.html | ||
type ImageProperties struct { | ||
// Architecture that must be supported by the hypervisor. | ||
Architecture string `json:"architecture,omitempty" mapstructure:"architecture,omitempty"` | ||
|
||
// OSType is the operating system installed on the image. | ||
OSType string `json:"os_type,omitempty" mapstructure:"os_type,omitempty"` | ||
|
||
// OSDistro is the common name of the operating system distribution in lowercase | ||
OSDistro string `json:"os_distro,omitempty" mapstructure:"os_distro,omitempty"` | ||
|
||
// OSVersion is the operating system version as specified by the distributor. | ||
OSVersion string `json:"os_version,omitempty" mapstructure:"os_version,omitempty"` | ||
|
||
// OSAdminUser is the default admin user name for the operating system | ||
OSAdminUser string `json:"os_admin_user,omitempty" mapstructure:"os_admin_user,omitempty"` | ||
} | ||
|
||
type Client interface { | ||
GetImageProperties(ctx context.Context, imageRef string) (*ImageProperties, error) | ||
ShowServerConsoleOutput(ctx context.Context, serverId string) (string, error) | ||
GetServer(ctx context.Context, serverId string) (*servers.Server, error) | ||
ListServers(ctx context.Context) ([]servers.Server, error) | ||
CreateServer(ctx context.Context, spec servers.CreateOptsBuilder, hintOpts servers.SchedulerHintOptsBuilder) (*servers.Server, error) | ||
DeleteServer(ctx context.Context, serverId string) error | ||
} | ||
|
||
var _ Client = (*client)(nil) | ||
|
||
type client struct { | ||
compute *gophercloud.ServiceClient | ||
image *gophercloud.ServiceClient | ||
} | ||
|
||
func New(authConfig AuthConfig) (Client, error) { | ||
if authConfig.NovaMicroversion == "" { | ||
authConfig.NovaMicroversion = "2.79" // Train+ | ||
} | ||
|
||
providerClient, endpointOps, err := newProviderClient(authConfig) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
computeClient, err := openstack.NewComputeV2(providerClient, endpointOps) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
_computeClient, err := utils.RequireMicroversion(context.TODO(), *computeClient, authConfig.NovaMicroversion) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to request microversion %s for OpenStack Nova: %w", authConfig.NovaMicroversion, err) | ||
} | ||
|
||
computeClient = &_computeClient | ||
|
||
imageClient, err := openstack.NewImageV2(providerClient, endpointOps) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &client{ | ||
compute: computeClient, | ||
image: imageClient, | ||
}, nil | ||
} | ||
|
||
func newProviderClient(authConfig AuthConfig) (*gophercloud.ProviderClient, gophercloud.EndpointOpts, error) { | ||
var endpointOps gophercloud.EndpointOpts | ||
var authOptions gophercloud.AuthOptions | ||
var providerClient *gophercloud.ProviderClient | ||
|
||
if authConfig.AuthFromEnv { | ||
var err error | ||
endpointOps = gophercloud.EndpointOpts{Region: os.Getenv("OS_REGION_NAME")} | ||
authOptions, err = openstack.AuthOptionsFromEnv() | ||
if err != nil { | ||
return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to get auth options from environment: %w", err) | ||
} | ||
authOptions.AllowReauth = true | ||
|
||
providerClient, err = openstack.AuthenticatedClient(context.Background(), authOptions) | ||
if err != nil { | ||
return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) | ||
} | ||
} else { | ||
var err error | ||
var tlsCfg *tls.Config | ||
cloudOpts := []clouds.ParseOption{clouds.WithCloudName(authConfig.Cloud)} | ||
if authConfig.CloudsConfig != "" { | ||
cloudOpts = append(cloudOpts, clouds.WithLocations(authConfig.CloudsConfig)) | ||
} | ||
|
||
authOptions, endpointOps, tlsCfg, err = clouds.Parse(cloudOpts...) | ||
if err != nil { | ||
return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to parse clouds.yaml: %w", err) | ||
} | ||
|
||
// plugin is a long running process. force allow reauth | ||
authOptions.AllowReauth = true | ||
|
||
providerClient, err = config.NewProviderClient(context.TODO(), authOptions, config.WithTLSConfig(tlsCfg)) | ||
if err != nil { | ||
return nil, gophercloud.EndpointOpts{}, fmt.Errorf("failed to connect to OpenStack Keystone: %w", err) | ||
} | ||
} | ||
|
||
return providerClient, endpointOps, nil | ||
} | ||
|
||
func (c *client) GetImageProperties(ctx context.Context, imageRef string) (*ImageProperties, error) { | ||
image, err := images.Get(ctx, c.image, imageRef).Extract() | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get image %s: %w", imageRef, err) | ||
} | ||
|
||
out := new(ImageProperties) | ||
err = mapstructure.Decode(image.Properties, out) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to parse properties: %w", err) | ||
} | ||
|
||
return out, nil | ||
} | ||
|
||
func (c *client) ShowServerConsoleOutput(ctx context.Context, serverId string) (string, error) { | ||
return servers.ShowConsoleOutput(ctx, c.compute, serverId, servers.ShowConsoleOutputOpts{ | ||
Length: 100, | ||
}).Extract() | ||
} | ||
|
||
func (c *client) GetServer(ctx context.Context, serverId string) (*servers.Server, error) { | ||
return servers.Get(ctx, c.compute, serverId).Extract() | ||
} | ||
|
||
func (c *client) ListServers(ctx context.Context) ([]servers.Server, error) { | ||
page, err := servers.List(c.compute, nil).AllPages(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("server listing error: %w", err) | ||
} | ||
|
||
allServers, err := servers.ExtractServers(page) | ||
if err != nil { | ||
return nil, fmt.Errorf("server listing extract error: %w", err) | ||
} | ||
|
||
return allServers, nil | ||
} | ||
|
||
func (c *client) CreateServer(ctx context.Context, spec servers.CreateOptsBuilder, hintOpts servers.SchedulerHintOptsBuilder) (*servers.Server, error) { | ||
return servers.Create(ctx, c.compute, spec, hintOpts).Extract() | ||
} | ||
|
||
func (c *client) DeleteServer(ctx context.Context, serverId string) error { | ||
return servers.Delete(ctx, c.compute, serverId).ExtractErr() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
package openstackclient | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"testing" | ||
|
||
"github.com/gophercloud/gophercloud/v2/testhelper" | ||
thclient "github.com/gophercloud/gophercloud/v2/testhelper/client" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestGetImageProperties(t *testing.T) { | ||
assert := assert.New(t) | ||
|
||
img, err := os.ReadFile("../../testdata/image_get.json") | ||
require.NoError(t, err) | ||
|
||
testhelper.SetupHTTP() | ||
defer testhelper.TeardownHTTP() | ||
|
||
testhelper.ServeFile(t, "", "", "application/json", string(img)) | ||
|
||
client := &client{ | ||
compute: thclient.ServiceClient(), | ||
image: thclient.ServiceClient(), | ||
} | ||
|
||
ctx := context.TODO() | ||
props, err := client.GetImageProperties(ctx, "1da9661c-953e-424d-a1e5-834a8174b198") | ||
assert.NoError(err) | ||
if assert.NotNil(props) { | ||
assert.Equal("core", props.OSAdminUser) | ||
} | ||
|
||
t.Log(props) | ||
} |
Oops, something went wrong.