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

     1  // Copyright 2011-2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package ec2
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/jsonschema"
    12  	"github.com/juju/loggo"
    13  	"gopkg.in/amz.v3/aws"
    14  	"gopkg.in/amz.v3/ec2"
    15  
    16  	"github.com/juju/juju/cloud"
    17  	"github.com/juju/juju/environs"
    18  	"github.com/juju/juju/environs/config"
    19  	"github.com/juju/juju/environs/context"
    20  	"github.com/juju/juju/environs/simplestreams"
    21  	"github.com/juju/juju/provider/common"
    22  )
    23  
    24  var logger = loggo.GetLogger("juju.provider.ec2")
    25  
    26  type environProvider struct {
    27  	environProviderCredentials
    28  }
    29  
    30  var providerInstance environProvider
    31  
    32  // Version is part of the EnvironProvider interface.
    33  func (environProvider) Version() int {
    34  	return 0
    35  }
    36  
    37  // Open is specified in the EnvironProvider interface.
    38  func (p environProvider) Open(args environs.OpenParams) (environs.Environ, error) {
    39  	logger.Infof("opening model %q", args.Config.Name())
    40  
    41  	e := new(environ)
    42  	e.cloud = args.Cloud
    43  	e.name = args.Config.Name()
    44  
    45  	// The endpoints in public-clouds.yaml from 2.0-rc2
    46  	// and before were wrong, so we use whatever is defined
    47  	// in goamz/aws if available.
    48  	if isBrokenCloud(e.cloud) {
    49  		if region, ok := aws.Regions[e.cloud.Region]; ok {
    50  			e.cloud.Endpoint = region.EC2Endpoint
    51  		}
    52  	}
    53  
    54  	var err error
    55  	e.ec2, err = awsClient(e.cloud)
    56  	if err != nil {
    57  		return nil, errors.Trace(err)
    58  	}
    59  
    60  	if err := e.SetConfig(args.Config); err != nil {
    61  		return nil, errors.Trace(err)
    62  	}
    63  	return e, nil
    64  }
    65  
    66  // isBrokenCloud reports whether the given CloudSpec is from an old,
    67  // broken version of public-clouds.yaml.
    68  func isBrokenCloud(cloud environs.CloudSpec) bool {
    69  	// The public-clouds.yaml from 2.0-rc2 and before was
    70  	// complete nonsense for general regions and for
    71  	// govcloud. The cn-north-1 region has a trailing slash,
    72  	// which we don't want as it means we won't match the
    73  	// simplestreams data.
    74  	switch cloud.Region {
    75  	case "us-east-1", "us-west-1", "us-west-2", "eu-west-1",
    76  		"eu-central-1", "ap-southeast-1", "ap-southeast-2",
    77  		"ap-northeast-1", "ap-northeast-2", "sa-east-1":
    78  		return cloud.Endpoint == fmt.Sprintf("https://%s.aws.amazon.com/v1.2/", cloud.Region)
    79  	case "cn-north-1":
    80  		return strings.HasSuffix(cloud.Endpoint, "/")
    81  	case "us-gov-west-1":
    82  		return cloud.Endpoint == "https://ec2.us-gov-west-1.amazonaws-govcloud.com"
    83  	}
    84  	return false
    85  }
    86  
    87  func awsClient(cloud environs.CloudSpec) (*ec2.EC2, error) {
    88  	if err := validateCloudSpec(cloud); err != nil {
    89  		return nil, errors.Annotate(err, "validating cloud spec")
    90  	}
    91  
    92  	credentialAttrs := cloud.Credential.Attributes()
    93  	accessKey := credentialAttrs["access-key"]
    94  	secretKey := credentialAttrs["secret-key"]
    95  	auth := aws.Auth{
    96  		AccessKey: accessKey,
    97  		SecretKey: secretKey,
    98  	}
    99  
   100  	region := aws.Region{
   101  		Name:        cloud.Region,
   102  		EC2Endpoint: cloud.Endpoint,
   103  	}
   104  	signer := aws.SignV4Factory(cloud.Region, "ec2")
   105  	return ec2.New(auth, region, signer), nil
   106  }
   107  
   108  // CloudSchema returns the schema used to validate input for add-cloud.  Since
   109  // this provider does not support custom clouds, this always returns nil.
   110  func (p environProvider) CloudSchema() *jsonschema.Schema {
   111  	return nil
   112  }
   113  
   114  // Ping tests the connection to the cloud, to verify the endpoint is valid.
   115  func (p environProvider) Ping(ctx context.ProviderCallContext, endpoint string) error {
   116  	return errors.NotImplementedf("Ping")
   117  }
   118  
   119  // PrepareConfig is specified in the EnvironProvider interface.
   120  func (p environProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) {
   121  	if err := validateCloudSpec(args.Cloud); err != nil {
   122  		return nil, errors.Annotate(err, "validating cloud spec")
   123  	}
   124  	// Set the default block-storage source.
   125  	attrs := make(map[string]interface{})
   126  	if _, ok := args.Config.StorageDefaultBlockSource(); !ok {
   127  		attrs[config.StorageDefaultBlockSourceKey] = EBS_ProviderType
   128  	}
   129  	if len(attrs) == 0 {
   130  		return args.Config, nil
   131  	}
   132  	return args.Config.Apply(attrs)
   133  }
   134  
   135  func validateCloudSpec(c environs.CloudSpec) error {
   136  	if err := c.Validate(); err != nil {
   137  		return errors.Trace(err)
   138  	}
   139  	if c.Credential == nil {
   140  		return errors.NotValidf("missing credential")
   141  	}
   142  	if authType := c.Credential.AuthType(); authType != cloud.AccessKeyAuthType {
   143  		return errors.NotSupportedf("%q auth-type", authType)
   144  	}
   145  	return nil
   146  }
   147  
   148  // Validate is specified in the EnvironProvider interface.
   149  func (environProvider) Validate(cfg, old *config.Config) (valid *config.Config, err error) {
   150  	newEcfg, err := validateConfig(cfg, old)
   151  	if err != nil {
   152  		return nil, fmt.Errorf("invalid EC2 provider config: %v", err)
   153  	}
   154  	return newEcfg.Apply(newEcfg.attrs)
   155  }
   156  
   157  // MetadataLookupParams returns parameters which are used to query image metadata to
   158  // find matching image information.
   159  func (p environProvider) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) {
   160  	if region == "" {
   161  		return nil, fmt.Errorf("region must be specified")
   162  	}
   163  	ec2Region, ok := aws.Regions[region]
   164  	if !ok {
   165  		return nil, fmt.Errorf("unknown region %q", region)
   166  	}
   167  	return &simplestreams.MetadataLookupParams{
   168  		Region:   region,
   169  		Endpoint: ec2Region.EC2Endpoint,
   170  	}, nil
   171  }
   172  
   173  const badKeys = `
   174  The provided credentials could not be validated and 
   175  may not be authorized to carry out the request.
   176  Ensure that your account is authorized to use the Amazon EC2 service and 
   177  that you are using the correct access keys. 
   178  These keys are obtained via the "Security Credentials"
   179  page in the AWS console.
   180  `
   181  
   182  const unauthorized = `
   183  Please subscribe to the requested Amazon service. 
   184  You are currently not authorized to use it.
   185  New Amazon accounts might take some time to be activated while 
   186  your details are being verified.`
   187  
   188  // verifyCredentials issues a cheap, non-modifying/idempotent request to EC2 to
   189  // verify the configured credentials. If verification fails, a user-friendly
   190  // error will be returned, and the original error will be logged at debug
   191  // level.
   192  var verifyCredentials = func(e *environ, ctx context.ProviderCallContext) error {
   193  	_, err := e.ec2.AccountAttributes()
   194  	return maybeConvertCredentialError(err, ctx)
   195  }
   196  
   197  // maybeConvertCredentialError examines the error received from the provider.
   198  // Authentication related errors are wrapped in common.CredentialNotValid.
   199  // Authorisation related errors are annotated with an additional
   200  // user-friendly explanation.
   201  // All other errors are returned un-wrapped and not annotated.
   202  var maybeConvertCredentialError = func(err error, ctx context.ProviderCallContext) error {
   203  	if err == nil {
   204  		return nil
   205  	}
   206  
   207  	convert := func(converted error) error {
   208  		callbackErr := ctx.InvalidateCredential(converted.Error())
   209  		if callbackErr != nil {
   210  			// We want to proceed with the actual proessing but still keep a log of a problem.
   211  			logger.Infof("callback to invalidate model credential failed with %v", converted)
   212  		}
   213  		return converted
   214  	}
   215  
   216  	if err, ok := err.(*ec2.Error); ok {
   217  		// EC2 error codes are from https://docs.aws.amazon.com/AWSEC2/latest/APIReference/errors-overview.html.
   218  		switch err.Code {
   219  		case "AuthFailure":
   220  			return convert(common.CredentialNotValidf(err, badKeys))
   221  		case "InvalidClientTokenId":
   222  			return convert(common.CredentialNotValidf(err, badKeys))
   223  		case "MissingAuthenticationToken":
   224  			return convert(common.CredentialNotValidf(err, badKeys))
   225  		case "Blocked":
   226  			return convert(common.CredentialNotValidf(err, "\nYour Amazon account is currently blocked."))
   227  		case "CustomerKeyHasBeenRevoked":
   228  			return convert(common.CredentialNotValidf(err, "\nYour Amazon keys have been revoked."))
   229  		case "PendingVerification":
   230  			return convert(common.CredentialNotValidf(err, "\nYour account is pending verification by Amazon."))
   231  		case "SignatureDoesNotMatch":
   232  			return convert(common.CredentialNotValidf(err, badKeys))
   233  		case "OptInRequired":
   234  			return errors.Annotate(err, unauthorized)
   235  		case "UnauthorizedOperation":
   236  			return errors.Annotate(err, unauthorized)
   237  		default:
   238  			// This error is unrelated to access keys, account or credentials...
   239  			return err
   240  		}
   241  	}
   242  	return err
   243  }