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 }