github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/manual/provider.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package manual
     5  
     6  import (
     7  	"strings"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/jsonschema"
    11  
    12  	"github.com/juju/juju/cloud"
    13  	"github.com/juju/juju/environs"
    14  	"github.com/juju/juju/environs/config"
    15  	"github.com/juju/juju/environs/context"
    16  	"github.com/juju/juju/environs/manual/sshprovisioner"
    17  )
    18  
    19  // ManualProvider contains the logic for using a random ubuntu machine as a
    20  // controller, connected via SSH.
    21  type ManualProvider struct {
    22  	environProviderCredentials
    23  	ping func(endpoint string) error
    24  }
    25  
    26  // Verify that we conform to the interface.
    27  var _ environs.EnvironProvider = (*ManualProvider)(nil)
    28  
    29  var initUbuntuUser = sshprovisioner.InitUbuntuUser
    30  
    31  func ensureBootstrapUbuntuUser(ctx environs.BootstrapContext, host, user string, cfg *environConfig) error {
    32  	err := initUbuntuUser(host, user, cfg.AuthorizedKeys(), ctx.GetStdin(), ctx.GetStdout())
    33  	if err != nil {
    34  		logger.Errorf("initializing ubuntu user: %v", err)
    35  		return err
    36  	}
    37  	logger.Infof("initialized ubuntu user")
    38  	return nil
    39  }
    40  
    41  // DetectRegions is specified in the environs.CloudRegionDetector interface.
    42  func (p ManualProvider) DetectRegions() ([]cloud.Region, error) {
    43  	return nil, errors.NotFoundf("regions")
    44  }
    45  
    46  var cloudSchema = &jsonschema.Schema{
    47  	Type:     []jsonschema.Type{jsonschema.ObjectType},
    48  	Required: []string{cloud.EndpointKey},
    49  	Properties: map[string]*jsonschema.Schema{
    50  		cloud.EndpointKey: {
    51  			Singular: "the controller's hostname or IP address",
    52  			Type:     []jsonschema.Type{jsonschema.StringType},
    53  			Format:   jsonschema.FormatURI,
    54  		},
    55  	},
    56  }
    57  
    58  // CloudSchema returns the schema for verifying the cloud configuration.
    59  func (p ManualProvider) CloudSchema() *jsonschema.Schema {
    60  	return cloudSchema
    61  }
    62  
    63  // Ping tests the connection to the cloud, to verify the endpoint is valid.
    64  func (p ManualProvider) Ping(ctx context.ProviderCallContext, endpoint string) error {
    65  	if p.ping != nil {
    66  		return p.ping(endpoint)
    67  	}
    68  	return pingMachine(endpoint)
    69  }
    70  
    71  // pingMachine is what is used in production by ManualProvider.Ping().
    72  // It does nothing at the moment.
    73  func pingMachine(endpoint string) error {
    74  	// (anastasiamac 2017-03-30) This method was introduced to verify
    75  	// manual endpoint by attempting to SSH into it.
    76  	// However, what we really wanted to do was to determine if
    77  	// we could connect to the endpoint not whether we could authenticate.
    78  	// In other words, we wanted to ignore authentication errors.
    79  	// These errors, at verification stage, when adding cloud details, are meaningless
    80  	// since authentication is configurable at bootstrap.
    81  	// With OpenSSH and crypto/ssh, both underlying current SSH client implementations, it is not
    82  	// possible to cleanly distinguish between authentication and connection failures
    83  	// without examining error string and looking for various matches.
    84  	// This feels dirty and flaky as the error messages can easily change
    85  	// between different libraries and their versions.
    86  	// So, it has been decided to just accept endpoint.
    87  	// If this ping(..) call will be used for other purposes, this decision may
    88  	// need to be re-visited.
    89  	return nil
    90  }
    91  
    92  // PrepareConfig is specified in the EnvironProvider interface.
    93  func (p ManualProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) {
    94  	if err := validateCloudSpec(args.Cloud); err != nil {
    95  		return nil, errors.Trace(err)
    96  	}
    97  	envConfig, err := p.validate(args.Config, nil)
    98  	if err != nil {
    99  		return nil, err
   100  	}
   101  	return args.Config.Apply(envConfig.attrs)
   102  }
   103  
   104  // Version is part of the EnvironProvider interface.
   105  func (ManualProvider) Version() int {
   106  	return 0
   107  }
   108  
   109  func (p ManualProvider) Open(args environs.OpenParams) (environs.Environ, error) {
   110  	if err := validateCloudSpec(args.Cloud); err != nil {
   111  		return nil, errors.Trace(err)
   112  	}
   113  	_, err := p.validate(args.Config, nil)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	// validate adds missing manual-specific config attributes
   118  	// with their defaults in the result; we don't want that in
   119  	// Open.
   120  	envConfig := newModelConfig(args.Config, args.Config.UnknownAttrs())
   121  	host, user := args.Cloud.Endpoint, ""
   122  	if i := strings.IndexRune(host, '@'); i >= 0 {
   123  		user, host = host[:i], host[i+1:]
   124  	}
   125  	return p.open(host, user, envConfig)
   126  }
   127  
   128  func validateCloudSpec(spec environs.CloudSpec) error {
   129  	if spec.Endpoint == "" {
   130  		return errors.Errorf(
   131  			"missing address of host to bootstrap: " +
   132  				`please specify "juju bootstrap manual/[user@]<host>"`,
   133  		)
   134  	}
   135  	return nil
   136  }
   137  
   138  func (p ManualProvider) open(host, user string, cfg *environConfig) (environs.Environ, error) {
   139  	env := &manualEnviron{host: host, user: user, cfg: cfg}
   140  	// Need to call SetConfig to initialise storage.
   141  	if err := env.SetConfig(cfg.Config); err != nil {
   142  		return nil, err
   143  	}
   144  	return env, nil
   145  }
   146  
   147  func (p ManualProvider) validate(cfg, old *config.Config) (*environConfig, error) {
   148  	// Check for valid changes for the base config values.
   149  	if err := config.Validate(cfg, old); err != nil {
   150  		return nil, err
   151  	}
   152  	validated, err := cfg.ValidateUnknownAttrs(configFields, configDefaults)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	envConfig := newModelConfig(cfg, validated)
   157  
   158  	// If the user hasn't already specified a value, set it to the
   159  	// given value.
   160  	defineIfNot := func(keyName string, value interface{}) {
   161  		if _, defined := cfg.AllAttrs()[keyName]; !defined {
   162  			logger.Infof("%s was not defined. Defaulting to %v.", keyName, value)
   163  			envConfig.attrs[keyName] = value
   164  		}
   165  	}
   166  
   167  	// If the user hasn't specified a value, refresh the
   168  	// available updates, but don't upgrade.
   169  	defineIfNot("enable-os-refresh-update", true)
   170  	defineIfNot("enable-os-upgrade", false)
   171  
   172  	return envConfig, nil
   173  }
   174  
   175  func (p ManualProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) {
   176  	envConfig, err := p.validate(cfg, old)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	return cfg.Apply(envConfig.attrs)
   181  }