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  }