github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/vsphere/provider.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package vsphere
     5  
     6  import (
     7  	stdcontext "context"
     8  	"net/url"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/jsonschema"
    12  	"github.com/juju/loggo"
    13  	"golang.org/x/net/context"
    14  
    15  	"github.com/juju/juju/cloud"
    16  	"github.com/juju/juju/environs"
    17  	environscloudspec "github.com/juju/juju/environs/cloudspec"
    18  	"github.com/juju/juju/environs/config"
    19  	callcontext "github.com/juju/juju/environs/context"
    20  )
    21  
    22  var logger = loggo.GetLogger("juju.provider.vmware")
    23  
    24  const (
    25  	// provider version 1 organises VMs into folders.
    26  	providerVersion1 = 1
    27  
    28  	currentProviderVersion = providerVersion1
    29  )
    30  
    31  type environProvider struct {
    32  	environProviderCredentials
    33  	dial DialFunc
    34  }
    35  
    36  var _ config.ConfigSchemaSource = (*environProvider)(nil)
    37  
    38  // EnvironProviderConfig contains configuration for the EnvironProvider.
    39  type EnvironProviderConfig struct {
    40  	// Dial is a function used for dialing connections to vCenter/ESXi.
    41  	Dial DialFunc
    42  }
    43  
    44  // NewEnvironProvider returns a new environs.EnvironProvider that will
    45  // dial vSphere connections with the given dial function.
    46  func NewEnvironProvider(config EnvironProviderConfig) environs.CloudEnvironProvider {
    47  	return &environProvider{
    48  		dial: config.Dial,
    49  	}
    50  }
    51  
    52  // Version implements environs.EnvironProvider.
    53  func (p *environProvider) Version() int {
    54  	return currentProviderVersion
    55  }
    56  
    57  // Open implements environs.EnvironProvider.
    58  func (p *environProvider) Open(_ stdcontext.Context, args environs.OpenParams) (environs.Environ, error) {
    59  	if err := validateCloudSpec(args.Cloud); err != nil {
    60  		return nil, errors.Annotate(err, "validating cloud spec")
    61  	}
    62  	env, err := newEnviron(p, args.Cloud, args.Config)
    63  	return env, errors.Trace(err)
    64  }
    65  
    66  var cloudSchema = &jsonschema.Schema{
    67  	Type:     []jsonschema.Type{jsonschema.ObjectType},
    68  	Required: []string{cloud.EndpointKey, cloud.AuthTypesKey, cloud.RegionsKey},
    69  	Order:    []string{cloud.EndpointKey, cloud.AuthTypesKey, cloud.RegionsKey},
    70  	Properties: map[string]*jsonschema.Schema{
    71  		cloud.EndpointKey: {
    72  			Singular: "the vCenter address or URL",
    73  			Type:     []jsonschema.Type{jsonschema.StringType},
    74  			Format:   jsonschema.FormatURI,
    75  		},
    76  		cloud.AuthTypesKey: {
    77  			// don't need a prompt, since there's only one choice.
    78  			Type: []jsonschema.Type{jsonschema.ArrayType},
    79  			Enum: []interface{}{[]string{string(cloud.UserPassAuthType)}},
    80  		},
    81  		cloud.RegionsKey: {
    82  			Type:     []jsonschema.Type{jsonschema.ObjectType},
    83  			Singular: "datacenter",
    84  			Plural:   "datacenters",
    85  			AdditionalProperties: &jsonschema.Schema{
    86  				Type:          []jsonschema.Type{jsonschema.ObjectType},
    87  				MaxProperties: jsonschema.Int(0),
    88  			},
    89  		},
    90  	},
    91  }
    92  
    93  // CloudSchema returns the schema for adding new clouds of this type.
    94  func (p *environProvider) CloudSchema() *jsonschema.Schema {
    95  	return cloudSchema
    96  }
    97  
    98  const failedLoginMsg = "ServerFaultCode: Cannot complete login due to an incorrect user name or password."
    99  
   100  // Ping tests the connection to the cloud, to verify the endpoint is valid.
   101  func (p *environProvider) Ping(callCtx callcontext.ProviderCallContext, endpoint string) error {
   102  	// try to be smart and not punish people for adding or forgetting http
   103  	u, err := url.Parse(endpoint)
   104  	if err != nil {
   105  		return errors.New("Invalid endpoint format, please give a full url or IP/hostname.")
   106  	}
   107  	switch u.Scheme {
   108  	case "http", "https":
   109  		// good!
   110  	case "":
   111  		u, err = url.Parse("https://" + endpoint + "/sdk")
   112  		if err != nil {
   113  			return errors.New("Invalid endpoint format, please give a full url or IP/hostname.")
   114  		}
   115  	default:
   116  		return errors.New("Invalid endpoint format, please use an http or https URL.")
   117  	}
   118  
   119  	// Set a user, to force the dial function to perform a login. The login
   120  	// should fail, since there's no password set.
   121  	u.User = url.User("juju")
   122  
   123  	ctx := context.Background()
   124  	client, err := p.dial(ctx, u, "")
   125  	if err != nil {
   126  		if err.Error() == failedLoginMsg {
   127  			// This is our expected error for trying to log into
   128  			// vSphere without any creds, so return nil.
   129  			return nil
   130  		}
   131  		logger.Errorf("Unexpected error dialing vSphere connection: %v", err)
   132  		return errors.Errorf("No vCenter/ESXi available at %s", endpoint)
   133  	}
   134  	defer client.Close(ctx)
   135  
   136  	// We shouldn't get here, since we haven't set a password, but it is
   137  	// theoretically possible to have user="juju", password="".
   138  	return nil
   139  }
   140  
   141  // PrepareConfig implements environs.EnvironProvider.
   142  func (p *environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) {
   143  	if err := validateCloudSpec(args.Cloud); err != nil {
   144  		return nil, errors.Annotate(err, "validating cloud spec")
   145  	}
   146  	return args.Config, nil
   147  }
   148  
   149  // Validate implements environs.EnvironProvider.
   150  func (*environProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) {
   151  	if old == nil {
   152  		ecfg, err := newValidConfig(cfg)
   153  		if err != nil {
   154  			return nil, errors.Annotate(err, "invalid config")
   155  		}
   156  		return ecfg.Config, nil
   157  	}
   158  
   159  	ecfg, err := newValidConfig(old)
   160  	if err != nil {
   161  		return nil, errors.Annotate(err, "invalid base config")
   162  	}
   163  
   164  	if err := ecfg.update(cfg); err != nil {
   165  		return nil, errors.Annotate(err, "invalid config change")
   166  	}
   167  
   168  	return ecfg.Config, nil
   169  }
   170  
   171  func validateCloudSpec(spec environscloudspec.CloudSpec) error {
   172  	if err := spec.Validate(); err != nil {
   173  		return errors.Trace(err)
   174  	}
   175  	// TODO(axw) add validation of endpoint/region.
   176  	if spec.Credential == nil {
   177  		return errors.NotValidf("missing credential")
   178  	}
   179  	if authType := spec.Credential.AuthType(); authType != cloud.UserPassAuthType {
   180  		return errors.NotSupportedf("%q auth-type", authType)
   181  	}
   182  	return nil
   183  }