1 package clientconfig
2 3 import (
4 "errors"
5 "fmt"
6 "net/http"
7 "os"
8 "reflect"
9 "strings"
10 11 "github.com/gophercloud/gophercloud"
12 "github.com/gophercloud/gophercloud/openstack"
13 "github.com/gophercloud/utils/env"
14 "github.com/gophercloud/utils/gnocchi"
15 "github.com/gophercloud/utils/internal"
16 17 "github.com/hashicorp/go-uuid"
18 yaml "gopkg.in/yaml.v2"
19 )
20 21 // AuthType respresents a valid method of authentication.
22 type AuthType string
23 24 const (
25 // AuthPassword defines an unknown version of the password
26 AuthPassword AuthType = "password"
27 // AuthToken defined an unknown version of the token
28 AuthToken AuthType = "token"
29 30 // AuthV2Password defines version 2 of the password
31 AuthV2Password AuthType = "v2password"
32 // AuthV2Token defines version 2 of the token
33 AuthV2Token AuthType = "v2token"
34 35 // AuthV3Password defines version 3 of the password
36 AuthV3Password AuthType = "v3password"
37 // AuthV3Token defines version 3 of the token
38 AuthV3Token AuthType = "v3token"
39 40 // AuthV3ApplicationCredential defines version 3 of the application credential
41 AuthV3ApplicationCredential AuthType = "v3applicationcredential"
42 )
43 44 // ClientOpts represents options to customize the way a client is
45 // configured.
46 type ClientOpts struct {
47 // Cloud is the cloud entry in clouds.yaml to use.
48 Cloud string
49 50 // EnvPrefix allows a custom environment variable prefix to be used.
51 EnvPrefix string
52 53 // AuthType specifies the type of authentication to use.
54 // By default, this is "password".
55 AuthType AuthType
56 57 // AuthInfo defines the authentication information needed to
58 // authenticate to a cloud when clouds.yaml isn't used.
59 AuthInfo *AuthInfo
60 61 // RegionName is the region to create a Service Client in.
62 // This will override a region in clouds.yaml or can be used
63 // when authenticating directly with AuthInfo.
64 RegionName string
65 66 // EndpointType specifies whether to use the public, internal, or
67 // admin endpoint of a service.
68 EndpointType string
69 70 // HTTPClient provides the ability customize the ProviderClient's
71 // internal HTTP client.
72 HTTPClient *http.Client
73 74 // YAMLOpts provides the ability to pass a customized set
75 // of options and methods for loading the YAML file.
76 // It takes a YAMLOptsBuilder interface that is defined
77 // in this file. This is optional and the default behavior
78 // is to call the local LoadCloudsYAML functions defined
79 // in this file.
80 YAMLOpts YAMLOptsBuilder
81 }
82 83 // YAMLOptsBuilder defines an interface for customization when
84 // loading a clouds.yaml file.
85 type YAMLOptsBuilder interface {
86 LoadCloudsYAML() (map[string]Cloud, error)
87 LoadSecureCloudsYAML() (map[string]Cloud, error)
88 LoadPublicCloudsYAML() (map[string]Cloud, error)
89 }
90 91 // YAMLOpts represents options and methods to load a clouds.yaml file.
92 type YAMLOpts struct {
93 // By default, no options are specified.
94 }
95 96 // LoadCloudsYAML defines how to load a clouds.yaml file.
97 // By default, this calls the local LoadCloudsYAML function.
98 func (opts YAMLOpts) LoadCloudsYAML() (map[string]Cloud, error) {
99 return LoadCloudsYAML()
100 }
101 102 // LoadSecureCloudsYAML defines how to load a secure.yaml file.
103 // By default, this calls the local LoadSecureCloudsYAML function.
104 func (opts YAMLOpts) LoadSecureCloudsYAML() (map[string]Cloud, error) {
105 return LoadSecureCloudsYAML()
106 }
107 108 // LoadPublicCloudsYAML defines how to load a public-secure.yaml file.
109 // By default, this calls the local LoadPublicCloudsYAML function.
110 func (opts YAMLOpts) LoadPublicCloudsYAML() (map[string]Cloud, error) {
111 return LoadPublicCloudsYAML()
112 }
113 114 // LoadCloudsYAML will load a clouds.yaml file and return the full config.
115 // This is called by the YAMLOpts method. Calling this function directly
116 // is supported for now but has only been retained for backwards
117 // compatibility from before YAMLOpts was defined. This may be removed in
118 // the future.
119 func LoadCloudsYAML() (map[string]Cloud, error) {
120 _, content, err := FindAndReadCloudsYAML()
121 if err != nil {
122 return nil, err
123 }
124 125 var clouds Clouds
126 err = yaml.Unmarshal(content, &clouds)
127 if err != nil {
128 return nil, fmt.Errorf("failed to unmarshal yaml: %w", err)
129 }
130 131 return clouds.Clouds, nil
132 }
133 134 // LoadSecureCloudsYAML will load a secure.yaml file and return the full config.
135 // This is called by the YAMLOpts method. Calling this function directly
136 // is supported for now but has only been retained for backwards
137 // compatibility from before YAMLOpts was defined. This may be removed in
138 // the future.
139 func LoadSecureCloudsYAML() (map[string]Cloud, error) {
140 var secureClouds Clouds
141 142 _, content, err := FindAndReadSecureCloudsYAML()
143 if err != nil {
144 if errors.Is(err, os.ErrNotExist) {
145 // secure.yaml is optional so just ignore read error
146 return secureClouds.Clouds, nil
147 }
148 return nil, err
149 }
150 151 err = yaml.Unmarshal(content, &secureClouds)
152 if err != nil {
153 return nil, fmt.Errorf("failed to unmarshal yaml: %w", err)
154 }
155 156 return secureClouds.Clouds, nil
157 }
158 159 // LoadPublicCloudsYAML will load a public-clouds.yaml file and return the full config.
160 // This is called by the YAMLOpts method. Calling this function directly
161 // is supported for now but has only been retained for backwards
162 // compatibility from before YAMLOpts was defined. This may be removed in
163 // the future.
164 func LoadPublicCloudsYAML() (map[string]Cloud, error) {
165 var publicClouds PublicClouds
166 167 _, content, err := FindAndReadPublicCloudsYAML()
168 if err != nil {
169 if errors.Is(err, os.ErrNotExist) {
170 // clouds-public.yaml is optional so just ignore read error
171 return publicClouds.Clouds, nil
172 }
173 174 return nil, err
175 }
176 177 err = yaml.Unmarshal(content, &publicClouds)
178 if err != nil {
179 return nil, fmt.Errorf("failed to unmarshal yaml: %w", err)
180 }
181 182 return publicClouds.Clouds, nil
183 }
184 185 // GetCloudFromYAML will return a cloud entry from a clouds.yaml file.
186 func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) {
187 if opts == nil {
188 opts = new(ClientOpts)
189 }
190 191 if opts.YAMLOpts == nil {
192 opts.YAMLOpts = new(YAMLOpts)
193 }
194 195 yamlOpts := opts.YAMLOpts
196 197 clouds, err := yamlOpts.LoadCloudsYAML()
198 if err != nil {
199 return nil, fmt.Errorf("unable to load clouds.yaml: %w", err)
200 }
201 202 // Determine which cloud to use.
203 // First see if a cloud name was explicitly set in opts.
204 var cloudName string
205 if opts.Cloud != "" {
206 cloudName = opts.Cloud
207 } else {
208 // If not, see if a cloud name was specified as an environment variable.
209 envPrefix := "OS_"
210 if opts.EnvPrefix != "" {
211 envPrefix = opts.EnvPrefix
212 }
213 214 if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
215 cloudName = v
216 }
217 }
218 219 var cloud *Cloud
220 if cloudName != "" {
221 v, ok := clouds[cloudName]
222 if !ok {
223 return nil, fmt.Errorf("cloud %s does not exist in clouds.yaml", cloudName)
224 }
225 cloud = &v
226 }
227 228 // If a cloud was not specified, and clouds only contains
229 // a single entry, use that entry.
230 if cloudName == "" && len(clouds) == 1 {
231 for _, v := range clouds {
232 cloud = &v
233 }
234 }
235 236 if cloud != nil {
237 // A profile points to a public cloud entry.
238 // If one was specified, load a list of public clouds
239 // and then merge the information with the current cloud data.
240 profileName := defaultIfEmpty(cloud.Profile, cloud.Cloud)
241 242 if profileName != "" {
243 publicClouds, err := yamlOpts.LoadPublicCloudsYAML()
244 if err != nil {
245 return nil, fmt.Errorf("unable to load clouds-public.yaml: %w", err)
246 }
247 248 publicCloud, ok := publicClouds[profileName]
249 if !ok {
250 return nil, fmt.Errorf("cloud %s does not exist in clouds-public.yaml", profileName)
251 }
252 253 cloud, err = mergeClouds(cloud, publicCloud)
254 if err != nil {
255 return nil, fmt.Errorf("Could not merge information from clouds.yaml and clouds-public.yaml for cloud %s", profileName)
256 }
257 }
258 }
259 260 // Next, load a secure clouds file and see if a cloud entry
261 // can be found or merged.
262 secureClouds, err := yamlOpts.LoadSecureCloudsYAML()
263 if err != nil {
264 return nil, fmt.Errorf("unable to load secure.yaml: %w", err)
265 }
266 267 if secureClouds != nil {
268 // If no entry was found in clouds.yaml, no cloud name was specified,
269 // and only one secureCloud entry exists, use that as the cloud entry.
270 if cloud == nil && cloudName == "" && len(secureClouds) == 1 {
271 for _, v := range secureClouds {
272 cloud = &v
273 }
274 }
275 276 // Otherwise, see if the provided cloud name exists in the secure yaml file.
277 secureCloud, ok := secureClouds[cloudName]
278 if !ok && cloud == nil {
279 // cloud == nil serves two purposes here:
280 // if no entry in clouds.yaml was found and
281 // if a single-entry secureCloud wasn't used.
282 // At this point, no entry could be determined at all.
283 return nil, fmt.Errorf("Could not find cloud %s", cloudName)
284 }
285 286 // If secureCloud has content and it differs from the cloud entry,
287 // merge the two together.
288 if !reflect.DeepEqual((Cloud{}), secureCloud) && !reflect.DeepEqual(cloud, secureCloud) {
289 cloud, err = mergeClouds(secureCloud, cloud)
290 if err != nil {
291 return nil, fmt.Errorf("unable to merge information from clouds.yaml and secure.yaml")
292 }
293 }
294 }
295 296 // As an extra precaution, do one final check to see if cloud is nil.
297 // We shouldn't reach this point, though.
298 if cloud == nil {
299 return nil, fmt.Errorf("Could not find cloud %s", cloudName)
300 }
301 302 // Default is to verify SSL API requests
303 if cloud.Verify == nil {
304 iTrue := true
305 cloud.Verify = &iTrue
306 }
307 308 // merging per-region value overrides
309 if opts.RegionName != "" {
310 for _, v := range cloud.Regions {
311 if opts.RegionName == v.Name {
312 cloud, err = mergeClouds(v.Values, cloud)
313 break
314 }
315 }
316 }
317 318 // TODO: this is where reading vendor files should go be considered when not found in
319 // clouds-public.yml
320 // https://github.com/openstack/openstacksdk/tree/master/openstack/config/vendors
321 322 // Both Interface and EndpointType are valid settings in clouds.yaml,
323 // but we want to standardize on EndpointType for simplicity.
324 //
325 // If only Interface was set, we copy that to EndpointType to use as the setting.
326 // But in all other cases, EndpointType is used and Interface is cleared.
327 if cloud.Interface != "" && cloud.EndpointType == "" {
328 cloud.EndpointType = cloud.Interface
329 }
330 331 cloud.Interface = ""
332 333 return cloud, nil
334 }
335 336 // AuthOptions creates a gophercloud.AuthOptions structure with the
337 // settings found in a specific cloud entry of a clouds.yaml file or
338 // based on authentication settings given in ClientOpts.
339 //
340 // This attempts to be a single point of entry for all OpenStack authentication.
341 //
342 // See http://docs.openstack.org/developer/os-client-config and
343 // https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py.
344 func AuthOptions(opts *ClientOpts) (*gophercloud.AuthOptions, error) {
345 cloud := new(Cloud)
346 347 // If no opts were passed in, create an empty ClientOpts.
348 if opts == nil {
349 opts = new(ClientOpts)
350 }
351 352 // Determine if a clouds.yaml entry should be retrieved.
353 // Start by figuring out the cloud name.
354 // First check if one was explicitly specified in opts.
355 var cloudName string
356 if opts.Cloud != "" {
357 cloudName = opts.Cloud
358 } else {
359 // If not, see if a cloud name was specified as an environment
360 // variable.
361 envPrefix := "OS_"
362 if opts.EnvPrefix != "" {
363 envPrefix = opts.EnvPrefix
364 }
365 366 if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
367 cloudName = v
368 }
369 }
370 371 // If a cloud name was determined, try to look it up in clouds.yaml.
372 if cloudName != "" {
373 // Get the requested cloud.
374 var err error
375 cloud, err = GetCloudFromYAML(opts)
376 if err != nil {
377 return nil, err
378 }
379 }
380 381 // If cloud.AuthInfo is nil, then no cloud was specified.
382 if cloud.AuthInfo == nil {
383 // If opts.AuthInfo is not nil, then try using the auth settings from it.
384 if opts.AuthInfo != nil {
385 cloud.AuthInfo = opts.AuthInfo
386 }
387 388 // If cloud.AuthInfo is still nil, then set it to an empty Auth struct
389 // and rely on environment variables to do the authentication.
390 if cloud.AuthInfo == nil {
391 cloud.AuthInfo = new(AuthInfo)
392 }
393 }
394 395 identityAPI := determineIdentityAPI(cloud, opts)
396 switch identityAPI {
397 case "2.0", "2":
398 return v2auth(cloud, opts)
399 case "3":
400 return v3auth(cloud, opts)
401 }
402 403 return nil, fmt.Errorf("Unable to build AuthOptions")
404 }
405 406 func determineIdentityAPI(cloud *Cloud, opts *ClientOpts) string {
407 var identityAPI string
408 if cloud.IdentityAPIVersion != "" {
409 identityAPI = cloud.IdentityAPIVersion
410 }
411 412 envPrefix := "OS_"
413 if opts != nil && opts.EnvPrefix != "" {
414 envPrefix = opts.EnvPrefix
415 }
416 417 if v := env.Getenv(envPrefix + "IDENTITY_API_VERSION"); v != "" {
418 identityAPI = v
419 }
420 421 if identityAPI == "" {
422 if cloud.AuthInfo != nil {
423 if strings.Contains(cloud.AuthInfo.AuthURL, "v2.0") {
424 identityAPI = "2.0"
425 }
426 427 if strings.Contains(cloud.AuthInfo.AuthURL, "v3") {
428 identityAPI = "3"
429 }
430 }
431 }
432 433 if identityAPI == "" {
434 switch cloud.AuthType {
435 case AuthV2Password:
436 identityAPI = "2.0"
437 case AuthV2Token:
438 identityAPI = "2.0"
439 case AuthV3Password:
440 identityAPI = "3"
441 case AuthV3Token:
442 identityAPI = "3"
443 case AuthV3ApplicationCredential:
444 identityAPI = "3"
445 }
446 }
447 448 // If an Identity API version could not be determined,
449 // default to v3.
450 if identityAPI == "" {
451 identityAPI = "3"
452 }
453 454 return identityAPI
455 }
456 457 // v2auth creates a v2-compatible gophercloud.AuthOptions struct.
458 func v2auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
459 // Environment variable overrides.
460 envPrefix := "OS_"
461 if opts != nil && opts.EnvPrefix != "" {
462 envPrefix = opts.EnvPrefix
463 }
464 465 if cloud.AuthInfo.AuthURL == "" {
466 if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" {
467 cloud.AuthInfo.AuthURL = v
468 }
469 }
470 471 if cloud.AuthInfo.Token == "" {
472 if v := env.Getenv(envPrefix + "TOKEN"); v != "" {
473 cloud.AuthInfo.Token = v
474 }
475 476 if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
477 cloud.AuthInfo.Token = v
478 }
479 }
480 481 if cloud.AuthInfo.Username == "" {
482 if v := env.Getenv(envPrefix + "USERNAME"); v != "" {
483 cloud.AuthInfo.Username = v
484 }
485 }
486 487 if cloud.AuthInfo.Password == "" {
488 if v := env.Getenv(envPrefix + "PASSWORD"); v != "" {
489 cloud.AuthInfo.Password = v
490 }
491 }
492 493 if cloud.AuthInfo.ProjectID == "" {
494 if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" {
495 cloud.AuthInfo.ProjectID = v
496 }
497 498 if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" {
499 cloud.AuthInfo.ProjectID = v
500 }
501 }
502 503 if cloud.AuthInfo.ProjectName == "" {
504 if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" {
505 cloud.AuthInfo.ProjectName = v
506 }
507 508 if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
509 cloud.AuthInfo.ProjectName = v
510 }
511 }
512 513 ao := &gophercloud.AuthOptions{
514 IdentityEndpoint: cloud.AuthInfo.AuthURL,
515 TokenID: cloud.AuthInfo.Token,
516 Username: cloud.AuthInfo.Username,
517 Password: cloud.AuthInfo.Password,
518 TenantID: cloud.AuthInfo.ProjectID,
519 TenantName: cloud.AuthInfo.ProjectName,
520 AllowReauth: cloud.AuthInfo.AllowReauth,
521 }
522 523 return ao, nil
524 }
525 526 // v3auth creates a v3-compatible gophercloud.AuthOptions struct.
527 func v3auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) {
528 // Environment variable overrides.
529 envPrefix := "OS_"
530 if opts != nil && opts.EnvPrefix != "" {
531 envPrefix = opts.EnvPrefix
532 }
533 534 if cloud.AuthInfo.AuthURL == "" {
535 if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" {
536 cloud.AuthInfo.AuthURL = v
537 }
538 }
539 540 if cloud.AuthInfo.Token == "" {
541 if v := env.Getenv(envPrefix + "TOKEN"); v != "" {
542 cloud.AuthInfo.Token = v
543 }
544 545 if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" {
546 cloud.AuthInfo.Token = v
547 }
548 }
549 550 if cloud.AuthInfo.Username == "" {
551 if v := env.Getenv(envPrefix + "USERNAME"); v != "" {
552 cloud.AuthInfo.Username = v
553 }
554 }
555 556 if cloud.AuthInfo.UserID == "" {
557 if v := env.Getenv(envPrefix + "USER_ID"); v != "" {
558 cloud.AuthInfo.UserID = v
559 }
560 }
561 562 if cloud.AuthInfo.Password == "" {
563 if v := env.Getenv(envPrefix + "PASSWORD"); v != "" {
564 cloud.AuthInfo.Password = v
565 }
566 }
567 568 if cloud.AuthInfo.ProjectID == "" {
569 if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" {
570 cloud.AuthInfo.ProjectID = v
571 }
572 573 if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" {
574 cloud.AuthInfo.ProjectID = v
575 }
576 }
577 578 if cloud.AuthInfo.ProjectName == "" {
579 if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" {
580 cloud.AuthInfo.ProjectName = v
581 }
582 583 if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" {
584 cloud.AuthInfo.ProjectName = v
585 }
586 }
587 588 if cloud.AuthInfo.DomainID == "" {
589 if v := env.Getenv(envPrefix + "DOMAIN_ID"); v != "" {
590 cloud.AuthInfo.DomainID = v
591 }
592 }
593 594 if cloud.AuthInfo.DomainName == "" {
595 if v := env.Getenv(envPrefix + "DOMAIN_NAME"); v != "" {
596 cloud.AuthInfo.DomainName = v
597 }
598 }
599 600 if cloud.AuthInfo.DefaultDomain == "" {
601 if v := env.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" {
602 cloud.AuthInfo.DefaultDomain = v
603 }
604 }
605 606 if cloud.AuthInfo.ProjectDomainID == "" {
607 if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" {
608 cloud.AuthInfo.ProjectDomainID = v
609 }
610 }
611 612 if cloud.AuthInfo.ProjectDomainName == "" {
613 if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" {
614 cloud.AuthInfo.ProjectDomainName = v
615 }
616 }
617 618 if cloud.AuthInfo.UserDomainID == "" {
619 if v := env.Getenv(envPrefix + "USER_DOMAIN_ID"); v != "" {
620 cloud.AuthInfo.UserDomainID = v
621 }
622 }
623 624 if cloud.AuthInfo.UserDomainName == "" {
625 if v := env.Getenv(envPrefix + "USER_DOMAIN_NAME"); v != "" {
626 cloud.AuthInfo.UserDomainName = v
627 }
628 }
629 630 if cloud.AuthInfo.ApplicationCredentialID == "" {
631 if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_ID"); v != "" {
632 cloud.AuthInfo.ApplicationCredentialID = v
633 }
634 }
635 636 if cloud.AuthInfo.ApplicationCredentialName == "" {
637 if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_NAME"); v != "" {
638 cloud.AuthInfo.ApplicationCredentialName = v
639 }
640 }
641 642 if cloud.AuthInfo.ApplicationCredentialSecret == "" {
643 if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_SECRET"); v != "" {
644 cloud.AuthInfo.ApplicationCredentialSecret = v
645 }
646 }
647 648 if cloud.AuthInfo.SystemScope == "" {
649 if v := env.Getenv(envPrefix + "SYSTEM_SCOPE"); v != "" {
650 cloud.AuthInfo.SystemScope = v
651 }
652 }
653 654 // Build a scope and try to do it correctly.
655 // https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py#L595
656 scope := new(gophercloud.AuthScope)
657 658 // Application credentials don't support scope
659 if isApplicationCredential(cloud.AuthInfo) {
660 // If Domain* is set, but UserDomain* or ProjectDomain* aren't,
661 // then use Domain* as the default setting.
662 cloud = setDomainIfNeeded(cloud)
663 } else {
664 if !isProjectScoped(cloud.AuthInfo) {
665 if cloud.AuthInfo.DomainID != "" {
666 scope.DomainID = cloud.AuthInfo.DomainID
667 } else if cloud.AuthInfo.DomainName != "" {
668 scope.DomainName = cloud.AuthInfo.DomainName
669 }
670 if cloud.AuthInfo.SystemScope != "" {
671 scope.System = true
672 }
673 } else {
674 // If Domain* is set, but UserDomain* or ProjectDomain* aren't,
675 // then use Domain* as the default setting.
676 cloud = setDomainIfNeeded(cloud)
677 678 if cloud.AuthInfo.ProjectID != "" {
679 scope.ProjectID = cloud.AuthInfo.ProjectID
680 } else {
681 scope.ProjectName = cloud.AuthInfo.ProjectName
682 scope.DomainID = cloud.AuthInfo.ProjectDomainID
683 scope.DomainName = cloud.AuthInfo.ProjectDomainName
684 }
685 }
686 }
687 688 ao := &gophercloud.AuthOptions{
689 Scope: scope,
690 IdentityEndpoint: cloud.AuthInfo.AuthURL,
691 TokenID: cloud.AuthInfo.Token,
692 Username: cloud.AuthInfo.Username,
693 UserID: cloud.AuthInfo.UserID,
694 Password: cloud.AuthInfo.Password,
695 TenantID: cloud.AuthInfo.ProjectID,
696 TenantName: cloud.AuthInfo.ProjectName,
697 DomainID: cloud.AuthInfo.UserDomainID,
698 DomainName: cloud.AuthInfo.UserDomainName,
699 ApplicationCredentialID: cloud.AuthInfo.ApplicationCredentialID,
700 ApplicationCredentialName: cloud.AuthInfo.ApplicationCredentialName,
701 ApplicationCredentialSecret: cloud.AuthInfo.ApplicationCredentialSecret,
702 AllowReauth: cloud.AuthInfo.AllowReauth,
703 }
704 705 // If an auth_type of "token" was specified, then make sure
706 // Gophercloud properly authenticates with a token. This involves
707 // unsetting a few other auth options. The reason this is done
708 // here is to wait until all auth settings (both in clouds.yaml
709 // and via environment variables) are set and then unset them.
710 if strings.Contains(string(cloud.AuthType), "token") || ao.TokenID != "" {
711 ao.Username = ""
712 ao.Password = ""
713 ao.UserID = ""
714 ao.DomainID = ""
715 ao.DomainName = ""
716 }
717 718 // Check for absolute minimum requirements.
719 if ao.IdentityEndpoint == "" {
720 err := gophercloud.ErrMissingInput{Argument: "auth_url"}
721 return nil, err
722 }
723 724 return ao, nil
725 }
726 727 // AuthenticatedClient is a convenience function to get a new provider client
728 // based on a clouds.yaml entry.
729 func AuthenticatedClient(opts *ClientOpts) (*gophercloud.ProviderClient, error) {
730 ao, err := AuthOptions(opts)
731 if err != nil {
732 return nil, err
733 }
734 735 return openstack.AuthenticatedClient(*ao)
736 }
737 738 // NewServiceClient is a convenience function to get a new service client.
739 func NewServiceClient(service string, opts *ClientOpts) (*gophercloud.ServiceClient, error) {
740 cloud := new(Cloud)
741 742 // If no opts were passed in, create an empty ClientOpts.
743 if opts == nil {
744 opts = new(ClientOpts)
745 }
746 747 // Determine if a clouds.yaml entry should be retrieved.
748 // Start by figuring out the cloud name.
749 // First check if one was explicitly specified in opts.
750 var cloudName string
751 if opts.Cloud != "" {
752 cloudName = opts.Cloud
753 }
754 755 // Next see if a cloud name was specified as an environment variable.
756 envPrefix := "OS_"
757 if opts.EnvPrefix != "" {
758 envPrefix = opts.EnvPrefix
759 }
760 761 if v := env.Getenv(envPrefix + "CLOUD"); v != "" {
762 cloudName = v
763 }
764 765 // If a cloud name was determined, try to look it up in clouds.yaml.
766 if cloudName != "" {
767 // Get the requested cloud.
768 var err error
769 cloud, err = GetCloudFromYAML(opts)
770 if err != nil {
771 return nil, err
772 }
773 }
774 775 // Check if a custom CA cert was provided.
776 // First, check if the CACERT environment variable is set.
777 var caCertPath string
778 if v := env.Getenv(envPrefix + "CACERT"); v != "" {
779 caCertPath = v
780 }
781 // Next, check if the cloud entry sets a CA cert.
782 if v := cloud.CACertFile; v != "" {
783 caCertPath = v
784 }
785 786 // Check if a custom client cert was provided.
787 // First, check if the CERT environment variable is set.
788 var clientCertPath string
789 if v := env.Getenv(envPrefix + "CERT"); v != "" {
790 clientCertPath = v
791 }
792 // Next, check if the cloud entry sets a client cert.
793 if v := cloud.ClientCertFile; v != "" {
794 clientCertPath = v
795 }
796 797 // Check if a custom client key was provided.
798 // First, check if the KEY environment variable is set.
799 var clientKeyPath string
800 if v := env.Getenv(envPrefix + "KEY"); v != "" {
801 clientKeyPath = v
802 }
803 // Next, check if the cloud entry sets a client key.
804 if v := cloud.ClientKeyFile; v != "" {
805 clientKeyPath = v
806 }
807 808 // Define whether or not SSL API requests should be verified.
809 var insecurePtr *bool
810 if cloud.Verify != nil {
811 // Here we take the boolean pointer negation.
812 insecure := !*cloud.Verify
813 insecurePtr = &insecure
814 }
815 816 tlsConfig, err := internal.PrepareTLSConfig(caCertPath, clientCertPath, clientKeyPath, insecurePtr)
817 if err != nil {
818 return nil, err
819 }
820 821 // Get a Provider Client
822 ao, err := AuthOptions(opts)
823 if err != nil {
824 return nil, err
825 }
826 pClient, err := openstack.NewClient(ao.IdentityEndpoint)
827 if err != nil {
828 return nil, err
829 }
830 831 // If an HTTPClient was specified, use it.
832 if opts.HTTPClient != nil {
833 pClient.HTTPClient = *opts.HTTPClient
834 } else {
835 // Otherwise create a new HTTP client with the generated TLS config.
836 transport := http.DefaultTransport.(*http.Transport).Clone()
837 transport.TLSClientConfig = tlsConfig
838 pClient.HTTPClient = http.Client{Transport: transport}
839 }
840 841 err = openstack.Authenticate(pClient, *ao)
842 if err != nil {
843 return nil, err
844 }
845 846 // Determine the region to use.
847 // First, check if the REGION_NAME environment variable is set.
848 var region string
849 if v := env.Getenv(envPrefix + "REGION_NAME"); v != "" {
850 region = v
851 }
852 853 // Next, check if the cloud entry sets a region.
854 if v := cloud.RegionName; v != "" {
855 region = v
856 }
857 858 // Finally, see if one was specified in the ClientOpts.
859 // If so, this takes precedence.
860 if v := opts.RegionName; v != "" {
861 region = v
862 }
863 864 // Determine the endpoint type to use.
865 // First, check if the OS_INTERFACE environment variable is set.
866 var endpointType string
867 if v := env.Getenv(envPrefix + "INTERFACE"); v != "" {
868 endpointType = v
869 }
870 871 // Next, check if the cloud entry sets an endpoint type.
872 if v := cloud.EndpointType; v != "" {
873 endpointType = v
874 }
875 876 // Finally, see if one was specified in the ClientOpts.
877 // If so, this takes precedence.
878 if v := opts.EndpointType; v != "" {
879 endpointType = v
880 }
881 882 eo := gophercloud.EndpointOpts{
883 Region: region,
884 Availability: GetEndpointType(endpointType),
885 }
886 887 switch service {
888 case "baremetal":
889 return openstack.NewBareMetalV1(pClient, eo)
890 case "baremetal-introspection":
891 return openstack.NewBareMetalIntrospectionV1(pClient, eo)
892 case "clustering":
893 return openstack.NewClusteringV1(pClient, eo)
894 case "compute":
895 return openstack.NewComputeV2(pClient, eo)
896 case "container":
897 return openstack.NewContainerV1(pClient, eo)
898 case "container-infra":
899 return openstack.NewContainerInfraV1(pClient, eo)
900 case "database":
901 return openstack.NewDBV1(pClient, eo)
902 case "dns":
903 return openstack.NewDNSV2(pClient, eo)
904 case "gnocchi":
905 return gnocchi.NewGnocchiV1(pClient, eo)
906 case "identity":
907 identityVersion := "3"
908 if v := cloud.IdentityAPIVersion; v != "" {
909 identityVersion = v
910 }
911 912 switch identityVersion {
913 case "v2", "2", "2.0":
914 return openstack.NewIdentityV2(pClient, eo)
915 case "v3", "3":
916 return openstack.NewIdentityV3(pClient, eo)
917 default:
918 return nil, fmt.Errorf("invalid identity API version")
919 }
920 case "image":
921 return openstack.NewImageServiceV2(pClient, eo)
922 case "key-manager":
923 return openstack.NewKeyManagerV1(pClient, eo)
924 case "load-balancer":
925 return openstack.NewLoadBalancerV2(pClient, eo)
926 case "messaging":
927 clientID, err := uuid.GenerateUUID()
928 if err != nil {
929 return nil, fmt.Errorf("failed to generate UUID: %w", err)
930 }
931 return openstack.NewMessagingV2(pClient, clientID, eo)
932 case "network":
933 return openstack.NewNetworkV2(pClient, eo)
934 case "object-store":
935 return openstack.NewObjectStorageV1(pClient, eo)
936 case "orchestration":
937 return openstack.NewOrchestrationV1(pClient, eo)
938 case "placement":
939 return openstack.NewPlacementV1(pClient, eo)
940 case "sharev2":
941 return openstack.NewSharedFileSystemV2(pClient, eo)
942 case "volume":
943 volumeVersion := "3"
944 if v := cloud.VolumeAPIVersion; v != "" {
945 volumeVersion = v
946 }
947 948 switch volumeVersion {
949 case "v1", "1":
950 return openstack.NewBlockStorageV1(pClient, eo)
951 case "v2", "2":
952 return openstack.NewBlockStorageV2(pClient, eo)
953 case "v3", "3":
954 return openstack.NewBlockStorageV3(pClient, eo)
955 default:
956 return nil, fmt.Errorf("invalid volume API version")
957 }
958 case "workflowv2":
959 return openstack.NewWorkflowV2(pClient, eo)
960 }
961 962 return nil, fmt.Errorf("unable to create a service client for %s", service)
963 }
964 965 // isProjectScoped determines if an auth struct is project scoped.
966 func isProjectScoped(authInfo *AuthInfo) bool {
967 if authInfo.ProjectID == "" && authInfo.ProjectName == "" {
968 return false
969 }
970 971 return true
972 }
973 974 // setDomainIfNeeded will set a DomainID and DomainName
975 // to ProjectDomain* and UserDomain* if not already set.
976 func setDomainIfNeeded(cloud *Cloud) *Cloud {
977 if cloud.AuthInfo.DomainID != "" {
978 if cloud.AuthInfo.UserDomainID == "" {
979 cloud.AuthInfo.UserDomainID = cloud.AuthInfo.DomainID
980 }
981 982 if cloud.AuthInfo.ProjectDomainID == "" {
983 cloud.AuthInfo.ProjectDomainID = cloud.AuthInfo.DomainID
984 }
985 986 cloud.AuthInfo.DomainID = ""
987 }
988 989 if cloud.AuthInfo.DomainName != "" {
990 if cloud.AuthInfo.UserDomainName == "" {
991 cloud.AuthInfo.UserDomainName = cloud.AuthInfo.DomainName
992 }
993 994 if cloud.AuthInfo.ProjectDomainName == "" {
995 cloud.AuthInfo.ProjectDomainName = cloud.AuthInfo.DomainName
996 }
997 998 cloud.AuthInfo.DomainName = ""
999 }
1000 1001 // If Domain fields are still not set, and if DefaultDomain has a value,
1002 // set UserDomainID and ProjectDomainID to DefaultDomain.
1003 // https://github.com/openstack/osc-lib/blob/86129e6f88289ef14bfaa3f7c9cdfbea8d9fc944/osc_lib/cli/client_config.py#L117-L146
1004 if cloud.AuthInfo.DefaultDomain != "" {
1005 if cloud.AuthInfo.UserDomainName == "" && cloud.AuthInfo.UserDomainID == "" {
1006 cloud.AuthInfo.UserDomainID = cloud.AuthInfo.DefaultDomain
1007 }
1008 1009 if cloud.AuthInfo.ProjectDomainName == "" && cloud.AuthInfo.ProjectDomainID == "" {
1010 cloud.AuthInfo.ProjectDomainID = cloud.AuthInfo.DefaultDomain
1011 }
1012 }
1013 1014 return cloud
1015 }
1016 1017 // isApplicationCredential determines if an application credential is used to auth.
1018 func isApplicationCredential(authInfo *AuthInfo) bool {
1019 if authInfo.ApplicationCredentialID == "" && authInfo.ApplicationCredentialName == "" && authInfo.ApplicationCredentialSecret == "" {
1020 return false
1021 }
1022 return true
1023 }
1024