blob: f3638bec5af4f03fad2d2ffd85e2c408c12b8939 [file] [log] [blame]
Ash Wilson8ba82242014-08-28 15:38:55 -04001package openstack
2
3import (
Ash Wilsona87ee062014-09-03 11:26:06 -04004 "fmt"
Ash Wilson09694b92014-09-09 14:08:27 -04005 "net/url"
Ash Wilsoned6a1d82014-09-03 12:01:00 -04006 "strings"
Ash Wilson4dee1b82014-08-29 14:56:45 -04007
Ash Wilson8ba82242014-08-28 15:38:55 -04008 "github.com/rackspace/gophercloud"
Ash Wilson52fbd182014-10-03 13:48:06 -04009 tokens2 "github.com/rackspace/gophercloud/openstack/identity/v2/tokens"
Ash Wilsonb8401a72014-09-08 17:07:49 -040010 endpoints3 "github.com/rackspace/gophercloud/openstack/identity/v3/endpoints"
11 services3 "github.com/rackspace/gophercloud/openstack/identity/v3/services"
Ash Wilsona87ee062014-09-03 11:26:06 -040012 tokens3 "github.com/rackspace/gophercloud/openstack/identity/v3/tokens"
Ash Wilson4dee1b82014-08-29 14:56:45 -040013 "github.com/rackspace/gophercloud/openstack/utils"
Ash Wilson3c8cc772014-09-16 11:40:49 -040014 "github.com/rackspace/gophercloud/pagination"
Ash Wilson8ba82242014-08-28 15:38:55 -040015)
16
Ash Wilson4dee1b82014-08-29 14:56:45 -040017const (
18 v20 = "v2.0"
19 v30 = "v3.0"
20)
Ash Wilson8ba82242014-08-28 15:38:55 -040021
Ash Wilsona87ee062014-09-03 11:26:06 -040022// NewClient prepares an unauthenticated ProviderClient instance.
23// Most users will probably prefer using the AuthenticatedClient function instead.
24// This is useful if you wish to explicitly control the version of the identity service that's used for authentication explicitly,
25// for example.
26func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
Ash Wilson09694b92014-09-09 14:08:27 -040027 u, err := url.Parse(endpoint)
28 if err != nil {
29 return nil, err
30 }
31 hadPath := u.Path != ""
32 u.Path, u.RawQuery, u.Fragment = "", "", ""
33 base := u.String()
34
Ash Wilsonaca58d82014-09-10 17:00:35 -040035 endpoint = normalizeURL(endpoint)
36 base = normalizeURL(base)
Ash Wilsone7da01c2014-09-09 12:31:06 -040037
Ash Wilson09694b92014-09-09 14:08:27 -040038 if hadPath {
39 return &gophercloud.ProviderClient{
40 IdentityBase: base,
41 IdentityEndpoint: endpoint,
42 }, nil
43 }
44
45 return &gophercloud.ProviderClient{
46 IdentityBase: base,
47 IdentityEndpoint: "",
48 }, nil
Ash Wilsona87ee062014-09-03 11:26:06 -040049}
50
51// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint specified by options, acquires a token, and
Ash Wilsonccd020b2014-09-02 10:40:54 -040052// returns a Client instance that's ready to operate.
Ash Wilson8ba82242014-08-28 15:38:55 -040053// It first queries the root identity endpoint to determine which versions of the identity service are supported, then chooses
54// the most recent identity service available to proceed.
Ash Wilsona87ee062014-09-03 11:26:06 -040055func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
56 client, err := NewClient(options.IdentityEndpoint)
57 if err != nil {
58 return nil, err
59 }
60
61 err = Authenticate(client, options)
Ash Wilsonccd020b2014-09-02 10:40:54 -040062 if err != nil {
63 return nil, err
64 }
65 return client, nil
66}
67
Ash Wilsonccd020b2014-09-02 10:40:54 -040068// Authenticate or re-authenticate against the most recent identity service supported at the provided endpoint.
Ash Wilsona87ee062014-09-03 11:26:06 -040069func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
Ash Wilson4dee1b82014-08-29 14:56:45 -040070 versions := []*utils.Version{
Ash Wilson09694b92014-09-09 14:08:27 -040071 &utils.Version{ID: v20, Priority: 20, Suffix: "/v2.0/"},
72 &utils.Version{ID: v30, Priority: 30, Suffix: "/v3/"},
Ash Wilson4dee1b82014-08-29 14:56:45 -040073 }
74
Ash Wilson09694b92014-09-09 14:08:27 -040075 chosen, endpoint, err := utils.ChooseVersion(client.IdentityBase, client.IdentityEndpoint, versions)
Ash Wilson4dee1b82014-08-29 14:56:45 -040076 if err != nil {
Ash Wilsonccd020b2014-09-02 10:40:54 -040077 return err
Ash Wilson4dee1b82014-08-29 14:56:45 -040078 }
79
80 switch chosen.ID {
81 case v20:
Ash Wilson09694b92014-09-09 14:08:27 -040082 return v2auth(client, endpoint, options)
Ash Wilson4dee1b82014-08-29 14:56:45 -040083 case v30:
Ash Wilson09694b92014-09-09 14:08:27 -040084 return v3auth(client, endpoint, options)
Ash Wilson4dee1b82014-08-29 14:56:45 -040085 default:
Ash Wilsonccd020b2014-09-02 10:40:54 -040086 // The switch statement must be out of date from the versions list.
Ash Wilsona87ee062014-09-03 11:26:06 -040087 return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
Ash Wilsonccd020b2014-09-02 10:40:54 -040088 }
89}
90
Ash Wilson09694b92014-09-09 14:08:27 -040091// AuthenticateV2 explicitly authenticates against the identity v2 endpoint.
92func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
93 return v2auth(client, "", options)
94}
95
96func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
97 v2Client := NewIdentityV2(client)
98 if endpoint != "" {
99 v2Client.Endpoint = endpoint
100 }
101
Ash Wilson6cc00702014-10-03 14:00:26 -0400102 result := tokens2.Create(v2Client, options)
Ash Wilson52fbd182014-10-03 13:48:06 -0400103
104 token, err := result.ExtractToken()
Ash Wilson09694b92014-09-09 14:08:27 -0400105 if err != nil {
106 return err
107 }
108
Ash Wilson52fbd182014-10-03 13:48:06 -0400109 catalog, err := result.ExtractServiceCatalog()
Ash Wilson09694b92014-09-09 14:08:27 -0400110 if err != nil {
111 return err
112 }
113
114 client.TokenID = token.ID
115 client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
Ash Wilson52fbd182014-10-03 13:48:06 -0400116 return v2endpointLocator(catalog, opts)
Ash Wilson09694b92014-09-09 14:08:27 -0400117 }
118
119 return nil
120}
121
Ash Wilson6cc00702014-10-03 14:00:26 -0400122func v2endpointLocator(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) {
Ash Wilson9d9876b2014-09-09 09:28:00 -0400123 // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided.
Ash Wilson6cc00702014-10-03 14:00:26 -0400124 var endpoints = make([]tokens2.Endpoint, 0, 1)
125 for _, entry := range catalog.Entries {
Ash Wilsone7da01c2014-09-09 12:31:06 -0400126 if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) {
Ash Wilson9d9876b2014-09-09 09:28:00 -0400127 for _, endpoint := range entry.Endpoints {
128 if opts.Region == "" || endpoint.Region == opts.Region {
129 endpoints = append(endpoints, endpoint)
130 }
131 }
132 }
133 }
134
135 // Report an error if the options were ambiguous.
136 if len(endpoints) == 0 {
137 return "", gophercloud.ErrEndpointNotFound
138 }
139 if len(endpoints) > 1 {
140 return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
141 }
142
143 // Extract the appropriate URL from the matching Endpoint.
144 for _, endpoint := range endpoints {
Ash Wilsonefac18b2014-09-10 14:44:42 -0400145 switch opts.Availability {
146 case gophercloud.AvailabilityPublic:
Ash Wilsonaca58d82014-09-10 17:00:35 -0400147 return normalizeURL(endpoint.PublicURL), nil
Ash Wilsonefac18b2014-09-10 14:44:42 -0400148 case gophercloud.AvailabilityInternal:
Ash Wilsonaca58d82014-09-10 17:00:35 -0400149 return normalizeURL(endpoint.InternalURL), nil
Ash Wilson6cc00702014-10-03 14:00:26 -0400150 case gophercloud.AvailabilityAdmin:
151 return normalizeURL(endpoint.AdminURL), nil
Ash Wilson9d9876b2014-09-09 09:28:00 -0400152 default:
Ash Wilsonefac18b2014-09-10 14:44:42 -0400153 return "", fmt.Errorf("Unexpected availability in endpoint query: %s", opts.Availability)
Ash Wilson9d9876b2014-09-09 09:28:00 -0400154 }
155 }
156
Ash Wilsonb8401a72014-09-08 17:07:49 -0400157 return "", gophercloud.ErrEndpointNotFound
158}
159
Ash Wilson09694b92014-09-09 14:08:27 -0400160// AuthenticateV3 explicitly authenticates against the identity v3 service.
161func AuthenticateV3(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
162 return v3auth(client, "", options)
163}
164
165func v3auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions) error {
166 // Override the generated service endpoint with the one returned by the version endpoint.
167 v3Client := NewIdentityV3(client)
168 if endpoint != "" {
169 v3Client.Endpoint = endpoint
170 }
171
Ash Wilson63b2a292014-10-02 09:29:06 -0400172 token, err := tokens3.Create(v3Client, options, nil).Extract()
Ash Wilson09694b92014-09-09 14:08:27 -0400173 if err != nil {
174 return err
175 }
Ash Wilson63b2a292014-10-02 09:29:06 -0400176 client.TokenID = token.ID
Ash Wilson09694b92014-09-09 14:08:27 -0400177
Ash Wilson09694b92014-09-09 14:08:27 -0400178 client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
179 return v3endpointLocator(v3Client, opts)
180 }
181
182 return nil
183}
184
Ash Wilsonb8401a72014-09-08 17:07:49 -0400185func v3endpointLocator(v3Client *gophercloud.ServiceClient, opts gophercloud.EndpointOpts) (string, error) {
Ash Wilsonb8401a72014-09-08 17:07:49 -0400186 // Discover the service we're interested in.
Ash Wilson566613e2014-09-12 14:51:46 -0400187 var services = make([]services3.Service, 0, 1)
Ash Wilson6b35e502014-09-12 15:15:23 -0400188 servicePager := services3.List(v3Client, services3.ListOpts{ServiceType: opts.Type})
Ash Wilson3c8cc772014-09-16 11:40:49 -0400189 err := servicePager.EachPage(func(page pagination.Page) (bool, error) {
Ash Wilson566613e2014-09-12 14:51:46 -0400190 part, err := services3.ExtractServices(page)
191 if err != nil {
Ash Wilson6b35e502014-09-12 15:15:23 -0400192 return false, err
Ash Wilson566613e2014-09-12 14:51:46 -0400193 }
Ash Wilsonb8401a72014-09-08 17:07:49 -0400194
Ash Wilson566613e2014-09-12 14:51:46 -0400195 for _, service := range part {
Ash Wilsonb8401a72014-09-08 17:07:49 -0400196 if service.Name == opts.Name {
Ash Wilson566613e2014-09-12 14:51:46 -0400197 services = append(services, service)
Ash Wilsonb8401a72014-09-08 17:07:49 -0400198 }
199 }
Ash Wilsonb8401a72014-09-08 17:07:49 -0400200
Ash Wilson6b35e502014-09-12 15:15:23 -0400201 return true, nil
Ash Wilsonb8401a72014-09-08 17:07:49 -0400202 })
203 if err != nil {
204 return "", err
205 }
Ash Wilsonb8401a72014-09-08 17:07:49 -0400206
Ash Wilson566613e2014-09-12 14:51:46 -0400207 if len(services) == 0 {
208 return "", gophercloud.ErrServiceNotFound
209 }
210 if len(services) > 1 {
211 return "", fmt.Errorf("Discovered %d matching services: %#v", len(services), services)
212 }
213 service := services[0]
214
215 // Enumerate the endpoints available for this service.
216 var endpoints []endpoints3.Endpoint
Ash Wilson6b35e502014-09-12 15:15:23 -0400217 endpointPager := endpoints3.List(v3Client, endpoints3.ListOpts{
Ash Wilson566613e2014-09-12 14:51:46 -0400218 Availability: opts.Availability,
219 ServiceID: service.ID,
Ash Wilson6b35e502014-09-12 15:15:23 -0400220 })
Ash Wilson3c8cc772014-09-16 11:40:49 -0400221 err = endpointPager.EachPage(func(page pagination.Page) (bool, error) {
Ash Wilson566613e2014-09-12 14:51:46 -0400222 part, err := endpoints3.ExtractEndpoints(page)
223 if err != nil {
Ash Wilson6b35e502014-09-12 15:15:23 -0400224 return false, err
Ash Wilson566613e2014-09-12 14:51:46 -0400225 }
226
227 for _, endpoint := range part {
Ash Wilson1cd3e692014-09-09 11:01:47 -0400228 if opts.Region == "" || endpoint.Region == opts.Region {
Ash Wilson566613e2014-09-12 14:51:46 -0400229 endpoints = append(endpoints, endpoint)
Ash Wilsonb8401a72014-09-08 17:07:49 -0400230 }
231 }
Ash Wilson566613e2014-09-12 14:51:46 -0400232
Ash Wilson6b35e502014-09-12 15:15:23 -0400233 return true, nil
Ash Wilson566613e2014-09-12 14:51:46 -0400234 })
235 if err != nil {
236 return "", err
Ash Wilsonb8401a72014-09-08 17:07:49 -0400237 }
238
239 if len(endpoints) == 0 {
240 return "", gophercloud.ErrEndpointNotFound
241 }
242 if len(endpoints) > 1 {
243 return "", fmt.Errorf("Discovered %d matching endpoints: %#v", len(endpoints), endpoints)
244 }
Ash Wilsonb8401a72014-09-08 17:07:49 -0400245 endpoint := endpoints[0]
246
Ash Wilsonaca58d82014-09-10 17:00:35 -0400247 return normalizeURL(endpoint.URL), nil
248}
249
250// normalizeURL ensures that each endpoint URL has a closing `/`, as expected by ServiceClient.
251func normalizeURL(url string) string {
252 if !strings.HasSuffix(url, "/") {
253 return url + "/"
254 }
255 return url
Ash Wilsonb8401a72014-09-08 17:07:49 -0400256}
257
Ash Wilsona87ee062014-09-03 11:26:06 -0400258// NewIdentityV2 creates a ServiceClient that may be used to interact with the v2 identity service.
259func NewIdentityV2(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
Ash Wilson09694b92014-09-09 14:08:27 -0400260 v2Endpoint := client.IdentityBase + "v2.0/"
Ash Wilsonccd020b2014-09-02 10:40:54 -0400261
Ash Wilsona87ee062014-09-03 11:26:06 -0400262 return &gophercloud.ServiceClient{
263 Provider: client,
264 Endpoint: v2Endpoint,
Ash Wilson4dee1b82014-08-29 14:56:45 -0400265 }
Ash Wilson8ba82242014-08-28 15:38:55 -0400266}
267
Ash Wilsona87ee062014-09-03 11:26:06 -0400268// NewIdentityV3 creates a ServiceClient that may be used to access the v3 identity service.
269func NewIdentityV3(client *gophercloud.ProviderClient) *gophercloud.ServiceClient {
Ash Wilson09694b92014-09-09 14:08:27 -0400270 v3Endpoint := client.IdentityBase + "v3/"
Ash Wilsona87ee062014-09-03 11:26:06 -0400271
272 return &gophercloud.ServiceClient{
273 Provider: client,
274 Endpoint: v3Endpoint,
275 }
Ash Wilson8ba82242014-08-28 15:38:55 -0400276}
Ash Wilson1cd3e692014-09-09 11:01:47 -0400277
278// NewStorageV1 creates a ServiceClient that may be used with the v1 object storage package.
Jon Perritt509fbb62014-09-10 13:29:56 -0500279func NewStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
280 eo.ApplyDefaults("object-store")
281 url, err := client.EndpointLocator(eo)
Ash Wilson1cd3e692014-09-09 11:01:47 -0400282 if err != nil {
283 return nil, err
284 }
285 return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
286}
Ash Wilson5e57c1b2014-09-17 09:24:46 -0400287
288// NewComputeV2 creates a ServiceClient that may be used with the v2 compute package.
289func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
290 eo.ApplyDefaults("compute")
291 url, err := client.EndpointLocator(eo)
292 if err != nil {
293 return nil, err
294 }
295 return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
296}
Ash Wilsonebc3d122014-09-24 13:44:05 -0400297
298// NewNetworkV2 creates a ServiceClient that may be used with the v2 network package.
Jamie Hannaford7ea29582014-09-11 15:49:46 +0200299func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
300 eo.ApplyDefaults("network")
301 url, err := client.EndpointLocator(eo)
302 if err != nil {
303 return nil, err
304 }
Ash Wilson99541ab2014-10-06 17:32:39 -0400305 return &gophercloud.ServiceClient{
306 Provider: client,
307 Endpoint: url,
308 ResourceBase: url + "v2.0/",
309 }, nil
Jamie Hannaford7ea29582014-09-11 15:49:46 +0200310}
Jon Perrittc5ee85e2014-09-17 00:53:19 -0500311
312// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 block storage service.
313func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
314 eo.ApplyDefaults("volume")
315 url, err := client.EndpointLocator(eo)
316 if err != nil {
317 return nil, err
318 }
319 return &gophercloud.ServiceClient{Provider: client, Endpoint: url}, nil
320}