github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/lxd/provider.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lxd 5 6 import ( 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 11 "github.com/juju/clock" 12 "github.com/juju/errors" 13 "github.com/juju/jsonschema" 14 "github.com/juju/schema" 15 "github.com/juju/utils" 16 "gopkg.in/juju/environschema.v1" 17 yaml "gopkg.in/yaml.v2" 18 19 "github.com/juju/juju/cloud" 20 "github.com/juju/juju/container/lxd" 21 "github.com/juju/juju/environs" 22 "github.com/juju/juju/environs/config" 23 "github.com/juju/juju/environs/context" 24 "github.com/juju/juju/provider/lxd/lxdnames" 25 ) 26 27 // LXCConfigReader reads files required for the LXC configuration. 28 //go:generate mockgen -package lxd -destination provider_mock_test.go github.com/juju/juju/provider/lxd LXCConfigReader 29 type LXCConfigReader interface { 30 // ReadConfig takes a path and returns a LXCConfig. 31 // Returns an error if there is an error with the location of the config 32 // file, or there was an error parsing the file. 33 ReadConfig(path string) (LXCConfig, error) 34 35 // ReadCert takes a path and returns a raw certificate, there is no 36 // validation of the certificate. 37 // Returns an error if there is an error with the location of the 38 // certificate. 39 ReadCert(path string) ([]byte, error) 40 } 41 42 // LXCConfig represents a configuration setup of a LXC configuration file. 43 // The LXCConfig expects the configuration file to be in a yaml representation. 44 type LXCConfig struct { 45 DefaultRemote string `yaml:"local"` 46 Remotes map[string]LXCRemoteConfig `yaml:"remotes"` 47 } 48 49 // LXCRemoteConfig defines a the remote servers of a LXC configuration. 50 type LXCRemoteConfig struct { 51 Addr string `yaml:"addr"` 52 Public bool `yaml:"public"` 53 Protocol string `yaml:"protocol"` 54 AuthType string `yaml:"auth_type"` 55 } 56 57 type environProvider struct { 58 environs.ProviderCredentials 59 environs.RequestFinalizeCredential 60 environs.ProviderCredentialsRegister 61 serverFactory ServerFactory 62 lxcConfigReader LXCConfigReader 63 Clock clock.Clock 64 } 65 66 var cloudSchema = &jsonschema.Schema{ 67 Type: []jsonschema.Type{jsonschema.ObjectType}, 68 Required: []string{cloud.EndpointKey, cloud.AuthTypesKey}, 69 Order: []string{cloud.EndpointKey, cloud.AuthTypesKey, cloud.RegionsKey}, 70 Properties: map[string]*jsonschema.Schema{ 71 cloud.EndpointKey: { 72 Singular: "the API endpoint url for the remote LXD server", 73 Type: []jsonschema.Type{jsonschema.StringType}, 74 Format: jsonschema.FormatURI, 75 }, 76 cloud.AuthTypesKey: { 77 Singular: "auth type", 78 Plural: "auth types", 79 Type: []jsonschema.Type{jsonschema.ArrayType}, 80 UniqueItems: jsonschema.Bool(true), 81 Default: string(cloud.CertificateAuthType), 82 Items: &jsonschema.ItemSpec{ 83 Schemas: []*jsonschema.Schema{{ 84 Type: []jsonschema.Type{jsonschema.StringType}, 85 Enum: []interface{}{ 86 string(cloud.CertificateAuthType), 87 }, 88 }}, 89 }, 90 }, 91 cloud.RegionsKey: { 92 Type: []jsonschema.Type{jsonschema.ObjectType}, 93 Singular: "region", 94 Plural: "regions", 95 Default: "default", 96 AdditionalProperties: &jsonschema.Schema{ 97 Type: []jsonschema.Type{jsonschema.ObjectType}, 98 Required: []string{cloud.EndpointKey}, 99 MaxProperties: jsonschema.Int(1), 100 Properties: map[string]*jsonschema.Schema{ 101 cloud.EndpointKey: { 102 Singular: "the API endpoint url for the region", 103 Type: []jsonschema.Type{jsonschema.StringType}, 104 Format: jsonschema.FormatURI, 105 Default: "", 106 PromptDefault: "use cloud api url", 107 }, 108 }, 109 }, 110 }, 111 }, 112 } 113 114 // NewProvider returns a new LXD EnvironProvider. 115 func NewProvider() environs.CloudEnvironProvider { 116 configReader := lxcConfigReader{} 117 factory := NewServerFactory() 118 credentials := environProviderCredentials{ 119 certReadWriter: certificateReadWriter{}, 120 certGenerator: certificateGenerator{}, 121 lookup: netLookup{}, 122 serverFactory: factory, 123 lxcConfigReader: configReader, 124 } 125 return &environProvider{ 126 ProviderCredentials: credentials, 127 RequestFinalizeCredential: credentials, 128 ProviderCredentialsRegister: credentials, 129 serverFactory: factory, 130 lxcConfigReader: configReader, 131 } 132 } 133 134 // Version is part of the EnvironProvider interface. 135 func (*environProvider) Version() int { 136 return 0 137 } 138 139 // Open implements environs.EnvironProvider. 140 func (p *environProvider) Open(args environs.OpenParams) (environs.Environ, error) { 141 if err := p.validateCloudSpec(args.Cloud); err != nil { 142 return nil, errors.Annotate(err, "validating cloud spec") 143 } 144 env, err := newEnviron( 145 p, 146 args.Cloud, 147 args.Config, 148 p.serverFactory, 149 ) 150 return env, errors.Trace(err) 151 } 152 153 // CloudSchema returns the schema used to validate input for add-cloud. 154 func (p *environProvider) CloudSchema() *jsonschema.Schema { 155 return cloudSchema 156 } 157 158 // Ping tests the connection to the cloud, to verify the endpoint is valid. 159 func (p *environProvider) Ping(ctx context.ProviderCallContext, endpoint string) error { 160 // if the endpoint is empty, then don't ping, as we can assume we're using 161 // local lxd 162 if endpoint == "" { 163 return nil 164 } 165 166 // Ensure the Port on the Host, if we get an error it is reasonable to 167 // assume that the address in the spec is invalid. 168 lxdEndpoint, err := lxd.EnsureHostPort(endpoint) 169 if err != nil { 170 return errors.Trace(err) 171 } 172 173 // Make sure we have an https url 174 if lxdEndpoint != endpoint { 175 return errors.Errorf("invalid URL %q: only HTTPS is supported", endpoint) 176 } 177 178 // Connect to the remote server anonymously so we can just verify it exists 179 // as we're not sure that the certificates are loaded in time for when the 180 // ping occurs i.e. interactive add-cloud 181 _, err = lxd.ConnectRemote(lxd.NewInsecureServerSpec(lxdEndpoint)) 182 if err != nil { 183 return errors.Errorf("no lxd server running at %s", lxdEndpoint) 184 } 185 return nil 186 } 187 188 // PrepareConfig implements environs.EnvironProvider. 189 func (p *environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { 190 if err := p.validateCloudSpec(args.Cloud); err != nil { 191 return nil, errors.Annotate(err, "validating cloud spec") 192 } 193 // Set the default filesystem-storage source. 194 attrs := make(map[string]interface{}) 195 if _, ok := args.Config.StorageDefaultFilesystemSource(); !ok { 196 attrs[config.StorageDefaultFilesystemSourceKey] = lxdStorageProviderType 197 } 198 if len(attrs) == 0 { 199 return args.Config, nil 200 } 201 cfg, err := args.Config.Apply(attrs) 202 return cfg, errors.Trace(err) 203 } 204 205 // Validate implements environs.EnvironProvider. 206 func (*environProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) { 207 if _, err := newValidConfig(cfg); err != nil { 208 return nil, errors.Annotate(err, "invalid base config") 209 } 210 return cfg, nil 211 } 212 213 // DetectClouds implements environs.CloudDetector. 214 func (p *environProvider) DetectClouds() ([]cloud.Cloud, error) { 215 configPath := filepath.Join(utils.Home(), ".config", "lxc", "config.yml") 216 config, err := p.lxcConfigReader.ReadConfig(configPath) 217 if err != nil { 218 logger.Errorf("unable to read/parse LXC config file: %s", err) 219 } 220 221 clouds := []cloud.Cloud{localhostCloud} 222 for name, remote := range config.Remotes { 223 if remote.Protocol == lxdnames.ProviderType { 224 clouds = append(clouds, cloud.Cloud{ 225 Name: name, 226 Type: lxdnames.ProviderType, 227 Endpoint: remote.Addr, 228 Description: "LXD Cluster", 229 AuthTypes: []cloud.AuthType{ 230 cloud.CertificateAuthType, 231 }, 232 Regions: []cloud.Region{{ 233 Name: lxdnames.DefaultRemoteRegion, 234 Endpoint: remote.Addr, 235 }}, 236 }) 237 } 238 } 239 240 return clouds, nil 241 } 242 243 // DetectCloud implements environs.CloudDetector. 244 func (p *environProvider) DetectCloud(name string) (cloud.Cloud, error) { 245 // For now we just return a hard-coded "localhost" cloud, 246 // i.e. the local LXD daemon. We may later want to detect 247 // locally-configured remotes. 248 switch name { 249 case lxdnames.ProviderType, lxdnames.DefaultCloud: 250 return localhostCloud, nil 251 default: 252 configPath := filepath.Join(utils.Home(), ".config", "lxc", "config.yml") 253 config, err := p.lxcConfigReader.ReadConfig(configPath) 254 if err != nil { 255 logger.Errorf("unable to read LXC config file %s", err) 256 break 257 } 258 259 if remote, ok := config.Remotes[name]; ok { 260 return cloud.Cloud{ 261 Name: name, 262 Type: lxdnames.ProviderType, 263 Endpoint: remote.Addr, 264 Description: "LXD Cluster", 265 AuthTypes: []cloud.AuthType{ 266 cloud.CertificateAuthType, 267 }, 268 Regions: []cloud.Region{{ 269 Name: lxdnames.DefaultRemoteRegion, 270 Endpoint: remote.Addr, 271 }}, 272 }, nil 273 } 274 } 275 return cloud.Cloud{}, errors.NotFoundf("cloud %s", name) 276 } 277 278 func (p *environProvider) detectCloud(name, path string) (cloud.Cloud, error) { 279 config, err := p.lxcConfigReader.ReadConfig(path) 280 if err != nil { 281 return cloud.Cloud{}, err 282 } 283 284 if remote, ok := config.Remotes[name]; ok { 285 return cloud.Cloud{ 286 Name: name, 287 Type: lxdnames.ProviderType, 288 Endpoint: remote.Addr, 289 Description: cloud.DefaultCloudDescription(lxdnames.ProviderType), 290 AuthTypes: []cloud.AuthType{ 291 cloud.CertificateAuthType, 292 }, 293 Regions: []cloud.Region{{ 294 Name: lxdnames.DefaultRemoteRegion, 295 Endpoint: remote.Addr, 296 }}, 297 }, nil 298 } 299 300 return cloud.Cloud{}, errors.NotFoundf("cloud %s", name) 301 } 302 303 // FinalizeCloud is part of the environs.CloudFinalizer interface. 304 func (p *environProvider) FinalizeCloud( 305 ctx environs.FinalizeCloudContext, 306 in cloud.Cloud, 307 ) (cloud.Cloud, error) { 308 var endpoint string 309 resolveEndpoint := func(name string, ep *string) error { 310 // If the name doesn't equal "localhost" then we shouldn't resolve 311 // the end point, instead we should just accept what we already have. 312 if name != lxdnames.DefaultCloud || *ep != "" { 313 return nil 314 } 315 if endpoint == "" { 316 // The cloud endpoint is empty, which means 317 // that we should connect to the local LXD. 318 hostAddress, err := p.getLocalHostAddress(ctx) 319 if err != nil { 320 return errors.Trace(err) 321 } 322 endpoint = hostAddress 323 } 324 *ep = endpoint 325 return nil 326 } 327 328 // If any of the endpoints point to localhost, go through and backfill the 329 // cloud spec with local host addresses. 330 if err := resolveEndpoint(in.Name, &in.Endpoint); err != nil { 331 return cloud.Cloud{}, errors.Trace(err) 332 } 333 for i, k := range in.Regions { 334 if err := resolveEndpoint(k.Name, &in.Regions[i].Endpoint); err != nil { 335 return cloud.Cloud{}, errors.Trace(err) 336 } 337 } 338 // If the provider type is not named localhost and there is no region, set the 339 // region to be a default region 340 if in.Name != lxdnames.DefaultCloud && len(in.Regions) == 0 { 341 in.Regions = append(in.Regions, cloud.Region{ 342 Name: lxdnames.DefaultRemoteRegion, 343 Endpoint: in.Endpoint, 344 }) 345 } 346 return in, nil 347 } 348 349 func (p *environProvider) getLocalHostAddress(ctx environs.FinalizeCloudContext) (string, error) { 350 svr, err := p.serverFactory.LocalServer() 351 if err != nil { 352 return "", errors.Trace(err) 353 } 354 355 bridgeName := svr.LocalBridgeName() 356 hostAddress, err := p.serverFactory.LocalServerAddress() 357 if err != nil { 358 return "", errors.Trace(err) 359 } 360 ctx.Verbosef( 361 "Resolved LXD host address on bridge %s: %s", 362 bridgeName, hostAddress, 363 ) 364 return hostAddress, nil 365 } 366 367 // localhostCloud is the predefined "localhost" LXD cloud. We leave the 368 // endpoints empty to indicate that LXD is on the local host. When the 369 // cloud is finalized (see FinalizeCloud), we resolve the bridge address 370 // of the LXD host, and use that as the endpoint. 371 var localhostCloud = cloud.Cloud{ 372 Name: lxdnames.DefaultCloud, 373 Type: lxdnames.ProviderType, 374 AuthTypes: []cloud.AuthType{ 375 cloud.CertificateAuthType, 376 }, 377 Endpoint: "", 378 Regions: []cloud.Region{{ 379 Name: lxdnames.DefaultLocalRegion, 380 Endpoint: "", 381 }}, 382 Description: cloud.DefaultCloudDescription(lxdnames.ProviderType), 383 } 384 385 // DetectRegions implements environs.CloudRegionDetector. 386 func (*environProvider) DetectRegions() ([]cloud.Region, error) { 387 // For now we just return a hard-coded "localhost" region, 388 // i.e. the local LXD daemon. We may later want to detect 389 // locally-configured remotes. 390 return []cloud.Region{{Name: lxdnames.DefaultLocalRegion}}, nil 391 } 392 393 // Schema returns the configuration schema for an environment. 394 func (*environProvider) Schema() environschema.Fields { 395 fields, err := config.Schema(configSchema) 396 if err != nil { 397 panic(err) 398 } 399 return fields 400 } 401 402 func (p *environProvider) validateCloudSpec(spec environs.CloudSpec) error { 403 if err := spec.Validate(); err != nil { 404 return errors.Trace(err) 405 } 406 if spec.Credential == nil { 407 return errors.NotValidf("missing credential") 408 } 409 410 // Always validate the spec.Endpoint, to ensure that it's valid. 411 if _, err := endpointURL(spec.Endpoint); err != nil { 412 return errors.Trace(err) 413 } 414 switch authType := spec.Credential.AuthType(); authType { 415 case cloud.CertificateAuthType: 416 if spec.Credential == nil { 417 return errors.NotFoundf("credentials") 418 } 419 if _, _, ok := getCertificates(*spec.Credential); !ok { 420 return errors.NotValidf("certificate credentials") 421 } 422 default: 423 return errors.NotSupportedf("%q auth-type", authType) 424 } 425 return nil 426 } 427 428 // ConfigSchema returns extra config attributes specific 429 // to this provider only. 430 func (p *environProvider) ConfigSchema() schema.Fields { 431 return configFields 432 } 433 434 // ConfigDefaults returns the default values for the 435 // provider specific config attributes. 436 func (p *environProvider) ConfigDefaults() schema.Defaults { 437 return configDefaults 438 } 439 440 // lxcConfigReader is the default implementation for reading files from disk. 441 type lxcConfigReader struct{} 442 443 func (lxcConfigReader) ReadConfig(path string) (LXCConfig, error) { 444 configFile, err := ioutil.ReadFile(path) 445 if err != nil { 446 if cause := errors.Cause(err); os.IsNotExist(cause) { 447 return LXCConfig{}, nil 448 } 449 return LXCConfig{}, errors.Trace(err) 450 } 451 452 var config LXCConfig 453 if err := yaml.Unmarshal(configFile, &config); err != nil { 454 return LXCConfig{}, errors.Trace(err) 455 } 456 457 return config, nil 458 } 459 460 func (lxcConfigReader) ReadCert(path string) ([]byte, error) { 461 certFile, err := ioutil.ReadFile(path) 462 return certFile, errors.Trace(err) 463 }