Skip to content

Commit cd941d8

Browse files
authored
Merge pull request gophercloud#2 from jtopjian/clientconfig
clientconfig package
2 parents 5ed9d6b + 8d2c1bd commit cd941d8

File tree

8 files changed

+591
-0
lines changed

8 files changed

+591
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// +build acceptance clientconfig
2+
3+
package clientconfig
4+
5+
import (
6+
"testing"
7+
8+
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
9+
10+
acc_compute "github.com/gophercloud/gophercloud/acceptance/openstack/compute/v2"
11+
acc_tools "github.com/gophercloud/gophercloud/acceptance/tools"
12+
13+
cc "github.com/gophercloud/utils/openstack/clientconfig"
14+
)
15+
16+
func TestServerCreateDestroy(t *testing.T) {
17+
clientOpts := &cc.ClientOpts{
18+
Cloud: "acctest",
19+
EnvPrefix: "FOO",
20+
}
21+
22+
client, err := cc.NewServiceClient("compute", clientOpts)
23+
if err != nil {
24+
t.Fatalf("Unable to create client: %v", err)
25+
}
26+
27+
server, err := acc_compute.CreateServer(t, client)
28+
if err != nil {
29+
t.Fatalf("Unable to create server: %v", err)
30+
}
31+
defer acc_compute.DeleteServer(t, client, server)
32+
33+
newServer, err := servers.Get(client, server.ID).Extract()
34+
if err != nil {
35+
t.Fatalf("Unable to get server %s: %v", server.ID, err)
36+
}
37+
38+
acc_tools.PrintResource(t, newServer)
39+
}

openstack/clientconfig/doc.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
Package clientconfig provides convienent functions for creating OpenStack
3+
clients. It is based on the Python os-client-config library.
4+
5+
See https://docs.openstack.org/os-client-config/latest for details.
6+
7+
Example to Create a Provider Client From clouds.yaml
8+
9+
opts := &clientconfig.ClientOpts{
10+
Name: "hawaii",
11+
}
12+
13+
pClient, err := clientconfig.AuthenticatedClient(opts)
14+
if err != nil {
15+
panic(err)
16+
}
17+
18+
19+
Example to Create a Service Client from clouds.yaml
20+
21+
opts := &clientconfig.ClientOpts{
22+
Name: "hawaii",
23+
}
24+
25+
computeClient, err := clientconfig.NewServiceClient("compute", opts)
26+
if err != nil {
27+
panic(err)
28+
}
29+
30+
*/
31+
package clientconfig

openstack/clientconfig/requests.go

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
package clientconfig
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/gophercloud/gophercloud"
8+
"github.com/gophercloud/gophercloud/openstack"
9+
10+
"gopkg.in/yaml.v2"
11+
)
12+
13+
// ClientOpts represents options to customize the way a client is
14+
// configured.
15+
type ClientOpts struct {
16+
// Cloud is the cloud entry in clouds.yaml to use.
17+
Cloud string
18+
19+
// EnvPrefix allows a custom environment variable prefix to be used.
20+
EnvPrefix string
21+
}
22+
23+
// LoadYAML will load a clouds.yaml file and return the full config.
24+
func LoadYAML() (map[string]Cloud, error) {
25+
content, err := findAndReadYAML()
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
var clouds Clouds
31+
err = yaml.Unmarshal(content, &clouds)
32+
if err != nil {
33+
return nil, fmt.Errorf("failed to unmarshal yaml: %v", err)
34+
}
35+
36+
return clouds.Clouds, nil
37+
}
38+
39+
// GetCloudFromYAML will return a cloud entry from a clouds.yaml file.
40+
func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
41+
clouds, err := LoadYAML()
42+
if err != nil {
43+
return nil, fmt.Errorf("unable to load clouds.yaml: %s", err)
44+
}
45+
46+
// Determine which cloud to use.
47+
var cloudName string
48+
if opts != nil && opts.Cloud != "" {
49+
cloudName = opts.Cloud
50+
}
51+
52+
if v := os.Getenv("OS_CLOUD"); v != "" {
53+
cloudName = v
54+
}
55+
56+
var cloud *Cloud
57+
if cloudName != "" {
58+
v, ok := clouds[cloudName]
59+
if !ok {
60+
return nil, fmt.Errorf("cloud %s does not exist in clouds.yaml", cloudName)
61+
}
62+
cloud = &v
63+
}
64+
65+
// If a cloud was not specified, and clouds only contains
66+
// a single entry, use that entry.
67+
if cloudName == "" && len(clouds) == 1 {
68+
for _, v := range clouds {
69+
cloud = &v
70+
}
71+
}
72+
73+
if cloud == nil {
74+
return nil, fmt.Errorf("Unable to determine a valid entry in clouds.yaml")
75+
}
76+
77+
return cloud, nil
78+
}
79+
80+
// AuthOptions creates a gophercloud identity.AuthOptions structure with the
81+
// settings found in a specific cloud entry of a clouds.yaml file.
82+
// See http://docs.openstack.org/developer/os-client-config and
83+
// https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py.
84+
func AuthOptions(opts *ClientOpts) (*gophercloud.AuthOptions, error) {
85+
// Get the requested cloud.
86+
cloud, err := GetCloudFromYAML(opts)
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
auth := cloud.Auth
92+
93+
// Create a gophercloud.AuthOptions struct based on the clouds.yaml entry.
94+
ao := &gophercloud.AuthOptions{
95+
IdentityEndpoint: auth.AuthURL,
96+
Username: auth.Username,
97+
Password: auth.Password,
98+
TenantID: auth.ProjectID,
99+
TenantName: auth.ProjectName,
100+
DomainID: auth.DomainID,
101+
DomainName: auth.DomainName,
102+
}
103+
104+
// Domain scope overrides.
105+
if auth.ProjectDomainID != "" {
106+
ao.DomainID = auth.ProjectDomainID
107+
}
108+
109+
if auth.ProjectDomainName != "" {
110+
ao.DomainName = auth.ProjectDomainName
111+
}
112+
113+
// Environment variable overrides.
114+
envPrefix := "OS_"
115+
if opts != nil && opts.EnvPrefix != "" {
116+
envPrefix = opts.EnvPrefix
117+
}
118+
119+
if v := os.Getenv(envPrefix + "AUTH_URL"); v != "" {
120+
ao.IdentityEndpoint = v
121+
}
122+
123+
if v := os.Getenv(envPrefix + "USERNAME"); v != "" {
124+
ao.Username = v
125+
}
126+
127+
if v := os.Getenv(envPrefix + "PASSWORD"); v != "" {
128+
ao.Password = v
129+
}
130+
131+
if v := os.Getenv(envPrefix + "TENANT_ID"); v != "" {
132+
ao.TenantID = v
133+
}
134+
135+
if v := os.Getenv(envPrefix + "PROJECT_ID"); v != "" {
136+
ao.TenantID = v
137+
}
138+
139+
if v := os.Getenv(envPrefix + "TENANT_NAME"); v != "" {
140+
ao.TenantName = v
141+
}
142+
143+
if v := os.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
144+
ao.TenantName = v
145+
}
146+
147+
if v := os.Getenv(envPrefix + "DOMAIN_ID"); v != "" {
148+
ao.DomainID = v
149+
}
150+
151+
if v := os.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" {
152+
ao.DomainID = v
153+
}
154+
155+
if v := os.Getenv(envPrefix + "DOMAIN_NAME"); v != "" {
156+
ao.DomainName = v
157+
}
158+
159+
if v := os.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" {
160+
ao.DomainName = v
161+
}
162+
163+
// Check for absolute minimum requirements.
164+
if ao.IdentityEndpoint == "" {
165+
err := gophercloud.ErrMissingInput{Argument: "authURL"}
166+
return nil, err
167+
}
168+
169+
if ao.Username == "" {
170+
err := gophercloud.ErrMissingInput{Argument: "username"}
171+
return nil, err
172+
}
173+
174+
if ao.Password == "" {
175+
err := gophercloud.ErrMissingInput{Argument: "password"}
176+
return nil, err
177+
}
178+
179+
return ao, nil
180+
}
181+
182+
// AuthenticatedClient is a convenience function to get a new provider client
183+
// based on a clouds.yaml entry.
184+
func AuthenticatedClient(opts *ClientOpts) (*gophercloud.ProviderClient, error) {
185+
ao, err := AuthOptions(opts)
186+
if err != nil {
187+
return nil, err
188+
}
189+
190+
return openstack.AuthenticatedClient(*ao)
191+
}
192+
193+
// NewServiceClient is a convenience function to get a new service client.
194+
func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceClient, error) {
195+
cloud, err := GetCloudFromYAML(opts)
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
// Environment variable overrides.
201+
envPrefix := "OS_"
202+
if opts != nil && opts.EnvPrefix != "" {
203+
envPrefix = opts.EnvPrefix
204+
}
205+
206+
// Get a Provider Client
207+
pClient, err := AuthenticatedClient(opts)
208+
if err != nil {
209+
return nil, err
210+
}
211+
212+
// Determine the region to use.
213+
var region string
214+
if v := cloud.RegionName; v != "" {
215+
region = cloud.RegionName
216+
}
217+
218+
if v := os.Getenv(envPrefix + "REGION_NAME"); v != "" {
219+
region = v
220+
}
221+
222+
eo := gophercloud.EndpointOpts{
223+
Region: region,
224+
}
225+
226+
switch service {
227+
case "compute":
228+
return openstack.NewComputeV2(pClient, eo)
229+
case "database":
230+
return openstack.NewDBV1(pClient, eo)
231+
case "dns":
232+
return openstack.NewDNSV2(pClient, eo)
233+
case "identity":
234+
identityVersion := "3"
235+
if v := cloud.IdentityAPIVersion; v != "" {
236+
identityVersion = v
237+
}
238+
239+
switch identityVersion {
240+
case "v2", "2", "2.0":
241+
return openstack.NewIdentityV2(pClient, eo)
242+
case "v3", "3":
243+
return openstack.NewIdentityV3(pClient, eo)
244+
default:
245+
return nil, fmt.Errorf("invalid identity API version")
246+
}
247+
case "image":
248+
return openstack.NewImageServiceV2(pClient, eo)
249+
case "network":
250+
return openstack.NewNetworkV2(pClient, eo)
251+
case "object-store":
252+
return openstack.NewObjectStorageV1(pClient, eo)
253+
case "orchestration":
254+
return openstack.NewOrchestrationV1(pClient, eo)
255+
case "sharev2":
256+
return openstack.NewSharedFileSystemV2(pClient, eo)
257+
case "volume":
258+
volumeVersion := "2"
259+
if v := cloud.VolumeAPIVersion; v != "" {
260+
volumeVersion = v
261+
}
262+
263+
switch volumeVersion {
264+
case "v1", "1":
265+
return openstack.NewBlockStorageV1(pClient, eo)
266+
case "v2", "2":
267+
return openstack.NewBlockStorageV2(pClient, eo)
268+
default:
269+
return nil, fmt.Errorf("invalid volume API version")
270+
}
271+
}
272+
273+
return nil, fmt.Errorf("unable to create a service client for %s", service)
274+
}

openstack/clientconfig/results.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package clientconfig
2+
3+
// Clouds represents a collection of Cloud entries in a clouds.yaml file.
4+
// The format of clouds.yaml is documented at
5+
// https://docs.openstack.org/os-client-config/latest/user/configuration.html.
6+
type Clouds struct {
7+
Clouds map[string]Cloud `yaml:"clouds"`
8+
}
9+
10+
// Cloud represents an entry in a clouds.yaml file.
11+
type Cloud struct {
12+
Auth *CloudAuth `yaml:"auth"`
13+
RegionName string `yaml:"region_name"`
14+
Regions []interface{} `yaml:"regions"`
15+
16+
// API Version overrides.
17+
IdentityAPIVersion string `yaml:"identity_api_version"`
18+
VolumeAPIVersion string `yaml:"volume_api_version"`
19+
}
20+
21+
// CloudAuth represents the auth section of a cloud entry.
22+
type CloudAuth struct {
23+
AuthURL string `yaml:"auth_url"`
24+
Username string `yaml:"username"`
25+
Password string `yaml:"password"`
26+
ProjectName string `yaml:"project_name"`
27+
ProjectID string `yaml:"project_id"`
28+
DomainName string `yaml:"domain_name"`
29+
DomainID string `yaml:"domain_id"`
30+
ProjectDomainName string `yaml:"project_domain_name"`
31+
ProjectDomainID string `yaml:"project_domain_id"`
32+
}

0 commit comments

Comments
 (0)