From f085f4d5d466daeb3dad3b04ffd329ed81afc4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ragnar=20Dahl=C3=A9n?= Date: Tue, 9 Sep 2014 22:13:02 +0100 Subject: [PATCH 1/2] Add new dependencies for AWS support - goamz: for interacting with AWS API - gcfg: for cloud provider config file (as suggested in issue) --- Godeps/Godeps.json | 16 + .../github.com/mitchellh/goamz/aws/attempt.go | 74 + .../mitchellh/goamz/aws/attempt_test.go | 57 + .../src/github.com/mitchellh/goamz/aws/aws.go | 423 +++ .../mitchellh/goamz/aws/aws_test.go | 203 ++ .../github.com/mitchellh/goamz/aws/client.go | 125 + .../mitchellh/goamz/aws/client_test.go | 121 + .../src/github.com/mitchellh/goamz/ec2/ec2.go | 2599 +++++++++++++++++ .../mitchellh/goamz/ec2/ec2_test.go | 1243 ++++++++ .../mitchellh/goamz/ec2/ec2i_test.go | 203 ++ .../mitchellh/goamz/ec2/ec2t_test.go | 580 ++++ .../mitchellh/goamz/ec2/ec2test/filter.go | 84 + .../mitchellh/goamz/ec2/ec2test/server.go | 993 +++++++ .../mitchellh/goamz/ec2/export_test.go | 22 + .../mitchellh/goamz/ec2/responses_test.go | 854 ++++++ .../github.com/mitchellh/goamz/ec2/sign.go | 45 + .../mitchellh/goamz/ec2/sign_test.go | 68 + .../src/github.com/vaughan0/go-ini/LICENSE | 14 + .../src/github.com/vaughan0/go-ini/README.md | 70 + .../src/github.com/vaughan0/go-ini/ini.go | 123 + .../vaughan0/go-ini/ini_linux_test.go | 43 + .../github.com/vaughan0/go-ini/ini_test.go | 89 + .../src/github.com/vaughan0/go-ini/test.ini | 2 + 23 files changed, 8051 insertions(+) create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt_test.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws_test.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client_test.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2_test.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2i_test.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2t_test.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/filter.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/server.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/export_test.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/responses_test.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign.go create mode 100644 Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign_test.go create mode 100644 Godeps/_workspace/src/github.com/vaughan0/go-ini/LICENSE create mode 100644 Godeps/_workspace/src/github.com/vaughan0/go-ini/README.md create mode 100644 Godeps/_workspace/src/github.com/vaughan0/go-ini/ini.go create mode 100644 Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_linux_test.go create mode 100644 Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_test.go create mode 100644 Godeps/_workspace/src/github.com/vaughan0/go-ini/test.ini diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 63c981ee151..237d9308dcf 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -5,6 +5,10 @@ "./..." ], "Deps": [ + { + "ImportPath": "code.google.com/p/gcfg", + "Rev": "c2d3050044d05357eaf6c3547249ba57c5e235cb" + }, { "ImportPath": "code.google.com/p/go-uuid/uuid", "Comment": "null-12", @@ -68,6 +72,14 @@ "ImportPath": "github.com/google/gofuzz", "Rev": "aef70dacbc78771e35beb261bb3a72986adf7906" }, + { + "ImportPath": "github.com/mitchellh/goamz/aws", + "Rev": "9cad7da945e699385c1a3e115aa255211921c9bb" + }, + { + "ImportPath": "github.com/mitchellh/goamz/ec2", + "Rev": "9cad7da945e699385c1a3e115aa255211921c9bb" + }, { "ImportPath": "github.com/stretchr/objx", "Rev": "d40df0cc104c06eae2dfe03d7dddb83802d52f9a" @@ -80,6 +92,10 @@ "ImportPath": "github.com/stretchr/testify/mock", "Rev": "37614ac27794505bf7867ca93aac883cadb6a5f7" }, + { + "ImportPath": "github.com/vaughan0/go-ini", + "Rev": "a98ad7ee00ec53921f08832bc06ecf7fd600e6a1" + }, { "ImportPath": "gopkg.in/v1/yaml", "Rev": "1b9791953ba4027efaeb728c7355e542a203be5e" diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt.go new file mode 100644 index 00000000000..c0654f5d851 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt.go @@ -0,0 +1,74 @@ +package aws + +import ( + "time" +) + +// AttemptStrategy represents a strategy for waiting for an action +// to complete successfully. This is an internal type used by the +// implementation of other goamz packages. +type AttemptStrategy struct { + Total time.Duration // total duration of attempt. + Delay time.Duration // interval between each try in the burst. + Min int // minimum number of retries; overrides Total +} + +type Attempt struct { + strategy AttemptStrategy + last time.Time + end time.Time + force bool + count int +} + +// Start begins a new sequence of attempts for the given strategy. +func (s AttemptStrategy) Start() *Attempt { + now := time.Now() + return &Attempt{ + strategy: s, + last: now, + end: now.Add(s.Total), + force: true, + } +} + +// Next waits until it is time to perform the next attempt or returns +// false if it is time to stop trying. +func (a *Attempt) Next() bool { + now := time.Now() + sleep := a.nextSleep(now) + if !a.force && !now.Add(sleep).Before(a.end) && a.strategy.Min <= a.count { + return false + } + a.force = false + if sleep > 0 && a.count > 0 { + time.Sleep(sleep) + now = time.Now() + } + a.count++ + a.last = now + return true +} + +func (a *Attempt) nextSleep(now time.Time) time.Duration { + sleep := a.strategy.Delay - now.Sub(a.last) + if sleep < 0 { + return 0 + } + return sleep +} + +// HasNext returns whether another attempt will be made if the current +// one fails. If it returns true, the following call to Next is +// guaranteed to return true. +func (a *Attempt) HasNext() bool { + if a.force || a.strategy.Min > a.count { + return true + } + now := time.Now() + if now.Add(a.nextSleep(now)).Before(a.end) { + a.force = true + return true + } + return false +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt_test.go new file mode 100644 index 00000000000..1fda5bf3c51 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/attempt_test.go @@ -0,0 +1,57 @@ +package aws_test + +import ( + "github.com/mitchellh/goamz/aws" + . "github.com/motain/gocheck" + "time" +) + +func (S) TestAttemptTiming(c *C) { + testAttempt := aws.AttemptStrategy{ + Total: 0.25e9, + Delay: 0.1e9, + } + want := []time.Duration{0, 0.1e9, 0.2e9, 0.2e9} + got := make([]time.Duration, 0, len(want)) // avoid allocation when testing timing + t0 := time.Now() + for a := testAttempt.Start(); a.Next(); { + got = append(got, time.Now().Sub(t0)) + } + got = append(got, time.Now().Sub(t0)) + c.Assert(got, HasLen, len(want)) + const margin = 0.01e9 + for i, got := range want { + lo := want[i] - margin + hi := want[i] + margin + if got < lo || got > hi { + c.Errorf("attempt %d want %g got %g", i, want[i].Seconds(), got.Seconds()) + } + } +} + +func (S) TestAttemptNextHasNext(c *C) { + a := aws.AttemptStrategy{}.Start() + c.Assert(a.Next(), Equals, true) + c.Assert(a.Next(), Equals, false) + + a = aws.AttemptStrategy{}.Start() + c.Assert(a.Next(), Equals, true) + c.Assert(a.HasNext(), Equals, false) + c.Assert(a.Next(), Equals, false) + + a = aws.AttemptStrategy{Total: 2e8}.Start() + c.Assert(a.Next(), Equals, true) + c.Assert(a.HasNext(), Equals, true) + time.Sleep(2e8) + c.Assert(a.HasNext(), Equals, true) + c.Assert(a.Next(), Equals, true) + c.Assert(a.Next(), Equals, false) + + a = aws.AttemptStrategy{Total: 1e8, Min: 2}.Start() + time.Sleep(1e8) + c.Assert(a.Next(), Equals, true) + c.Assert(a.HasNext(), Equals, true) + c.Assert(a.Next(), Equals, true) + c.Assert(a.HasNext(), Equals, false) + c.Assert(a.Next(), Equals, false) +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws.go new file mode 100644 index 00000000000..c304d5540ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws.go @@ -0,0 +1,423 @@ +// +// goamz - Go packages to interact with the Amazon Web Services. +// +// https://wiki.ubuntu.com/goamz +// +// Copyright (c) 2011 Canonical Ltd. +// +// Written by Gustavo Niemeyer +// +package aws + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/vaughan0/go-ini" + "io/ioutil" + "os" +) + +// Region defines the URLs where AWS services may be accessed. +// +// See http://goo.gl/d8BP1 for more details. +type Region struct { + Name string // the canonical name of this region. + EC2Endpoint string + S3Endpoint string + S3BucketEndpoint string // Not needed by AWS S3. Use ${bucket} for bucket name. + S3LocationConstraint bool // true if this region requires a LocationConstraint declaration. + S3LowercaseBucket bool // true if the region requires bucket names to be lower case. + SDBEndpoint string + SNSEndpoint string + SQSEndpoint string + IAMEndpoint string + ELBEndpoint string + AutoScalingEndpoint string + RdsEndpoint string + Route53Endpoint string +} + +var USGovWest = Region{ + "us-gov-west-1", + "https://ec2.us-gov-west-1.amazonaws.com", + "https://s3-fips-us-gov-west-1.amazonaws.com", + "", + true, + true, + "", + "https://sns.us-gov-west-1.amazonaws.com", + "https://sqs.us-gov-west-1.amazonaws.com", + "https://iam.us-gov.amazonaws.com", + "https://elasticloadbalancing.us-gov-west-1.amazonaws.com", + "https://autoscaling.us-gov-west-1.amazonaws.com", + "https://rds.us-gov-west-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var USEast = Region{ + "us-east-1", + "https://ec2.us-east-1.amazonaws.com", + "https://s3.amazonaws.com", + "", + false, + false, + "https://sdb.amazonaws.com", + "https://sns.us-east-1.amazonaws.com", + "https://sqs.us-east-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.us-east-1.amazonaws.com", + "https://autoscaling.us-east-1.amazonaws.com", + "https://rds.us-east-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var USWest = Region{ + "us-west-1", + "https://ec2.us-west-1.amazonaws.com", + "https://s3-us-west-1.amazonaws.com", + "", + true, + true, + "https://sdb.us-west-1.amazonaws.com", + "https://sns.us-west-1.amazonaws.com", + "https://sqs.us-west-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.us-west-1.amazonaws.com", + "https://autoscaling.us-west-1.amazonaws.com", + "https://rds.us-west-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var USWest2 = Region{ + "us-west-2", + "https://ec2.us-west-2.amazonaws.com", + "https://s3-us-west-2.amazonaws.com", + "", + true, + true, + "https://sdb.us-west-2.amazonaws.com", + "https://sns.us-west-2.amazonaws.com", + "https://sqs.us-west-2.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.us-west-2.amazonaws.com", + "https://autoscaling.us-west-2.amazonaws.com", + "https://rds.us-west-2.amazonaws.com", + "https://route53.amazonaws.com", +} + +var EUWest = Region{ + "eu-west-1", + "https://ec2.eu-west-1.amazonaws.com", + "https://s3-eu-west-1.amazonaws.com", + "", + true, + true, + "https://sdb.eu-west-1.amazonaws.com", + "https://sns.eu-west-1.amazonaws.com", + "https://sqs.eu-west-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.eu-west-1.amazonaws.com", + "https://autoscaling.eu-west-1.amazonaws.com", + "https://rds.eu-west-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var APSoutheast = Region{ + "ap-southeast-1", + "https://ec2.ap-southeast-1.amazonaws.com", + "https://s3-ap-southeast-1.amazonaws.com", + "", + true, + true, + "https://sdb.ap-southeast-1.amazonaws.com", + "https://sns.ap-southeast-1.amazonaws.com", + "https://sqs.ap-southeast-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.ap-southeast-1.amazonaws.com", + "https://autoscaling.ap-southeast-1.amazonaws.com", + "https://rds.ap-southeast-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var APSoutheast2 = Region{ + "ap-southeast-2", + "https://ec2.ap-southeast-2.amazonaws.com", + "https://s3-ap-southeast-2.amazonaws.com", + "", + true, + true, + "https://sdb.ap-southeast-2.amazonaws.com", + "https://sns.ap-southeast-2.amazonaws.com", + "https://sqs.ap-southeast-2.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.ap-southeast-2.amazonaws.com", + "https://autoscaling.ap-southeast-2.amazonaws.com", + "https://rds.ap-southeast-2.amazonaws.com", + "https://route53.amazonaws.com", +} + +var APNortheast = Region{ + "ap-northeast-1", + "https://ec2.ap-northeast-1.amazonaws.com", + "https://s3-ap-northeast-1.amazonaws.com", + "", + true, + true, + "https://sdb.ap-northeast-1.amazonaws.com", + "https://sns.ap-northeast-1.amazonaws.com", + "https://sqs.ap-northeast-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.ap-northeast-1.amazonaws.com", + "https://autoscaling.ap-northeast-1.amazonaws.com", + "https://rds.ap-northeast-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var SAEast = Region{ + "sa-east-1", + "https://ec2.sa-east-1.amazonaws.com", + "https://s3-sa-east-1.amazonaws.com", + "", + true, + true, + "https://sdb.sa-east-1.amazonaws.com", + "https://sns.sa-east-1.amazonaws.com", + "https://sqs.sa-east-1.amazonaws.com", + "https://iam.amazonaws.com", + "https://elasticloadbalancing.sa-east-1.amazonaws.com", + "https://autoscaling.sa-east-1.amazonaws.com", + "https://rds.sa-east-1.amazonaws.com", + "https://route53.amazonaws.com", +} + +var CNNorth = Region{ + "cn-north-1", + "https://ec2.cn-north-1.amazonaws.com.cn", + "https://s3.cn-north-1.amazonaws.com.cn", + "", + true, + true, + "", + "https://sns.cn-north-1.amazonaws.com.cn", + "https://sqs.cn-north-1.amazonaws.com.cn", + "https://iam.cn-north-1.amazonaws.com.cn", + "https://elasticloadbalancing.cn-north-1.amazonaws.com.cn", + "https://autoscaling.cn-north-1.amazonaws.com.cn", + "https://rds.cn-north-1.amazonaws.com.cn", + "https://route53.amazonaws.com", +} + +var Regions = map[string]Region{ + APNortheast.Name: APNortheast, + APSoutheast.Name: APSoutheast, + APSoutheast2.Name: APSoutheast2, + EUWest.Name: EUWest, + USEast.Name: USEast, + USWest.Name: USWest, + USWest2.Name: USWest2, + SAEast.Name: SAEast, + USGovWest.Name: USGovWest, + CNNorth.Name: CNNorth, +} + +type Auth struct { + AccessKey, SecretKey, Token string +} + +var unreserved = make([]bool, 128) +var hex = "0123456789ABCDEF" + +func init() { + // RFC3986 + u := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-_.~" + for _, c := range u { + unreserved[c] = true + } +} + +type credentials struct { + Code string + LastUpdated string + Type string + AccessKeyId string + SecretAccessKey string + Token string + Expiration string +} + +// GetMetaData retrieves instance metadata about the current machine. +// +// See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html for more details. +func GetMetaData(path string) (contents []byte, err error) { + url := "http://169.254.169.254/latest/meta-data/" + path + + resp, err := RetryingClient.Get(url) + if err != nil { + return + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + err = fmt.Errorf("Code %d returned for url %s", resp.StatusCode, url) + return + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + return []byte(body), err +} + +func getInstanceCredentials() (cred credentials, err error) { + credentialPath := "iam/security-credentials/" + + // Get the instance role + role, err := GetMetaData(credentialPath) + if err != nil { + return + } + + // Get the instance role credentials + credentialJSON, err := GetMetaData(credentialPath + string(role)) + if err != nil { + return + } + + err = json.Unmarshal([]byte(credentialJSON), &cred) + return +} + +// GetAuth creates an Auth based on either passed in credentials, +// environment information or instance based role credentials. +func GetAuth(accessKey string, secretKey string) (auth Auth, err error) { + // First try passed in credentials + if accessKey != "" && secretKey != "" { + return Auth{accessKey, secretKey, ""}, nil + } + + // Next try to get auth from the environment + auth, err = SharedAuth() + if err == nil { + // Found auth, return + return + } + + // Next try to get auth from the environment + auth, err = EnvAuth() + if err == nil { + // Found auth, return + return + } + + // Next try getting auth from the instance role + cred, err := getInstanceCredentials() + if err == nil { + // Found auth, return + auth.AccessKey = cred.AccessKeyId + auth.SecretKey = cred.SecretAccessKey + auth.Token = cred.Token + return + } + err = errors.New("No valid AWS authentication found") + return +} + +// SharedAuth creates an Auth based on shared credentials stored in +// $HOME/.aws/credentials. The AWS_PROFILE environment variables is used to +// select the profile. +func SharedAuth() (auth Auth, err error) { + var profileName = os.Getenv("AWS_PROFILE") + + if profileName == "" { + profileName = "default" + } + + var homeDir = os.Getenv("HOME") + if homeDir == "" { + err = errors.New("Could not get HOME") + return + } + + var credentialsFile = homeDir + "/.aws/credentials" + file, err := ini.LoadFile(credentialsFile) + if err != nil { + err = errors.New("Couldn't parse AWS credentials file") + return + } + + var profile = file[profileName] + if profile == nil { + err = errors.New("Couldn't find profile in AWS credentials file") + return + } + + auth.AccessKey = profile["aws_access_key_id"] + auth.SecretKey = profile["aws_secret_access_key"] + + if auth.AccessKey == "" { + err = errors.New("AWS_ACCESS_KEY_ID not found in environment in credentials file") + } + if auth.SecretKey == "" { + err = errors.New("AWS_SECRET_ACCESS_KEY not found in credentials file") + } + return +} + +// EnvAuth creates an Auth based on environment information. +// The AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment +// For accounts that require a security token, it is read from AWS_SECURITY_TOKEN +// variables are used. +func EnvAuth() (auth Auth, err error) { + auth.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID") + if auth.AccessKey == "" { + auth.AccessKey = os.Getenv("AWS_ACCESS_KEY") + } + + auth.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY") + if auth.SecretKey == "" { + auth.SecretKey = os.Getenv("AWS_SECRET_KEY") + } + + auth.Token = os.Getenv("AWS_SECURITY_TOKEN") + + if auth.AccessKey == "" { + err = errors.New("AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment") + } + if auth.SecretKey == "" { + err = errors.New("AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment") + } + return +} + +// Encode takes a string and URI-encodes it in a way suitable +// to be used in AWS signatures. +func Encode(s string) string { + encode := false + for i := 0; i != len(s); i++ { + c := s[i] + if c > 127 || !unreserved[c] { + encode = true + break + } + } + if !encode { + return s + } + e := make([]byte, len(s)*3) + ei := 0 + for i := 0; i != len(s); i++ { + c := s[i] + if c > 127 || !unreserved[c] { + e[ei] = '%' + e[ei+1] = hex[c>>4] + e[ei+2] = hex[c&0xF] + ei += 3 + } else { + e[ei] = c + ei += 1 + } + } + return string(e[:ei]) +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws_test.go new file mode 100644 index 00000000000..78cbbaf03c5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/aws_test.go @@ -0,0 +1,203 @@ +package aws_test + +import ( + "github.com/mitchellh/goamz/aws" + . "github.com/motain/gocheck" + "io/ioutil" + "os" + "strings" + "testing" +) + +func Test(t *testing.T) { + TestingT(t) +} + +var _ = Suite(&S{}) + +type S struct { + environ []string +} + +func (s *S) SetUpSuite(c *C) { + s.environ = os.Environ() +} + +func (s *S) TearDownTest(c *C) { + os.Clearenv() + for _, kv := range s.environ { + l := strings.SplitN(kv, "=", 2) + os.Setenv(l[0], l[1]) + } +} + +func (s *S) TestSharedAuthNoHome(c *C) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "foo") + _, err := aws.SharedAuth() + c.Assert(err, ErrorMatches, "Could not get HOME") +} + +func (s *S) TestSharedAuthNoCredentialsFile(c *C) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "foo") + os.Setenv("HOME", "/tmp") + _, err := aws.SharedAuth() + c.Assert(err, ErrorMatches, "Couldn't parse AWS credentials file") +} + +func (s *S) TestSharedAuthNoProfileInFile(c *C) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "foo") + + d, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(d) + + err = os.Mkdir(d+"/.aws", 0755) + if err != nil { + panic(err) + } + + ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\n"), 0644) + os.Setenv("HOME", d) + + _, err = aws.SharedAuth() + c.Assert(err, ErrorMatches, "Couldn't find profile in AWS credentials file") +} + +func (s *S) TestSharedAuthNoKeysInProfile(c *C) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "bar") + + d, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(d) + + err = os.Mkdir(d+"/.aws", 0755) + if err != nil { + panic(err) + } + + ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\nawsaccesskeyid = AK.."), 0644) + os.Setenv("HOME", d) + + _, err = aws.SharedAuth() + c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_KEY not found in credentials file") +} + +func (s *S) TestSharedAuthDefaultCredentials(c *C) { + os.Clearenv() + + d, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(d) + + err = os.Mkdir(d+"/.aws", 0755) + if err != nil { + panic(err) + } + + ioutil.WriteFile(d+"/.aws/credentials", []byte("[default]\naws_access_key_id = access\naws_secret_access_key = secret\n"), 0644) + os.Setenv("HOME", d) + + auth, err := aws.SharedAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestSharedAuth(c *C) { + os.Clearenv() + os.Setenv("AWS_PROFILE", "bar") + + d, err := ioutil.TempDir("", "") + if err != nil { + panic(err) + } + defer os.RemoveAll(d) + + err = os.Mkdir(d+"/.aws", 0755) + if err != nil { + panic(err) + } + + ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\naws_access_key_id = access\naws_secret_access_key = secret\n"), 0644) + os.Setenv("HOME", d) + + auth, err := aws.SharedAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestEnvAuthNoSecret(c *C) { + os.Clearenv() + _, err := aws.EnvAuth() + c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment") +} + +func (s *S) TestEnvAuthNoAccess(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_ACCESS_KEY", "foo") + _, err := aws.EnvAuth() + c.Assert(err, ErrorMatches, "AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment") +} + +func (s *S) TestEnvAuth(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_ACCESS_KEY", "secret") + os.Setenv("AWS_ACCESS_KEY_ID", "access") + auth, err := aws.EnvAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestEnvAuthWithToken(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_ACCESS_KEY", "secret") + os.Setenv("AWS_ACCESS_KEY_ID", "access") + os.Setenv("AWS_SECURITY_TOKEN", "token") + auth, err := aws.EnvAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access", Token: "token"}) +} + +func (s *S) TestEnvAuthAlt(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_KEY", "secret") + os.Setenv("AWS_ACCESS_KEY", "access") + auth, err := aws.EnvAuth() + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestGetAuthStatic(c *C) { + auth, err := aws.GetAuth("access", "secret") + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestGetAuthEnv(c *C) { + os.Clearenv() + os.Setenv("AWS_SECRET_ACCESS_KEY", "secret") + os.Setenv("AWS_ACCESS_KEY_ID", "access") + auth, err := aws.GetAuth("", "") + c.Assert(err, IsNil) + c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"}) +} + +func (s *S) TestEncode(c *C) { + c.Assert(aws.Encode("foo"), Equals, "foo") + c.Assert(aws.Encode("/"), Equals, "%2F") +} + +func (s *S) TestRegionsAreNamed(c *C) { + for n, r := range aws.Regions { + c.Assert(n, Equals, r.Name) + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client.go new file mode 100644 index 00000000000..ee53238f7b3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client.go @@ -0,0 +1,125 @@ +package aws + +import ( + "math" + "net" + "net/http" + "time" +) + +type RetryableFunc func(*http.Request, *http.Response, error) bool +type WaitFunc func(try int) +type DeadlineFunc func() time.Time + +type ResilientTransport struct { + // Timeout is the maximum amount of time a dial will wait for + // a connect to complete. + // + // The default is no timeout. + // + // With or without a timeout, the operating system may impose + // its own earlier timeout. For instance, TCP timeouts are + // often around 3 minutes. + DialTimeout time.Duration + + // MaxTries, if non-zero, specifies the number of times we will retry on + // failure. Retries are only attempted for temporary network errors or known + // safe failures. + MaxTries int + Deadline DeadlineFunc + ShouldRetry RetryableFunc + Wait WaitFunc + transport *http.Transport +} + +// Convenience method for creating an http client +func NewClient(rt *ResilientTransport) *http.Client { + rt.transport = &http.Transport{ + Dial: func(netw, addr string) (net.Conn, error) { + c, err := net.DialTimeout(netw, addr, rt.DialTimeout) + if err != nil { + return nil, err + } + c.SetDeadline(rt.Deadline()) + return c, nil + }, + DisableKeepAlives: true, + Proxy: http.ProxyFromEnvironment, + } + // TODO: Would be nice is ResilientTransport allowed clients to initialize + // with http.Transport attributes. + return &http.Client{ + Transport: rt, + } +} + +var retryingTransport = &ResilientTransport{ + Deadline: func() time.Time { + return time.Now().Add(5 * time.Second) + }, + DialTimeout: 10 * time.Second, + MaxTries: 3, + ShouldRetry: awsRetry, + Wait: ExpBackoff, +} + +// Exported default client +var RetryingClient = NewClient(retryingTransport) + +func (t *ResilientTransport) RoundTrip(req *http.Request) (*http.Response, error) { + return t.tries(req) +} + +// Retry a request a maximum of t.MaxTries times. +// We'll only retry if the proper criteria are met. +// If a wait function is specified, wait that amount of time +// In between requests. +func (t *ResilientTransport) tries(req *http.Request) (res *http.Response, err error) { + for try := 0; try < t.MaxTries; try += 1 { + res, err = t.transport.RoundTrip(req) + + if !t.ShouldRetry(req, res, err) { + break + } + if res != nil { + res.Body.Close() + } + if t.Wait != nil { + t.Wait(try) + } + } + + return +} + +func ExpBackoff(try int) { + time.Sleep(100 * time.Millisecond * + time.Duration(math.Exp2(float64(try)))) +} + +func LinearBackoff(try int) { + time.Sleep(time.Duration(try*100) * time.Millisecond) +} + +// Decide if we should retry a request. +// In general, the criteria for retrying a request is described here +// http://docs.aws.amazon.com/general/latest/gr/api-retries.html +func awsRetry(req *http.Request, res *http.Response, err error) bool { + retry := false + + // Retry if there's a temporary network error. + if neterr, ok := err.(net.Error); ok { + if neterr.Temporary() { + retry = true + } + } + + // Retry if we get a 5xx series error. + if res != nil { + if res.StatusCode >= 500 && res.StatusCode < 600 { + retry = true + } + } + + return retry +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client_test.go new file mode 100644 index 00000000000..2f6b39cf3af --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/aws/client_test.go @@ -0,0 +1,121 @@ +package aws_test + +import ( + "fmt" + "github.com/mitchellh/goamz/aws" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// Retrieve the response from handler using aws.RetryingClient +func serveAndGet(handler http.HandlerFunc) (body string, err error) { + ts := httptest.NewServer(handler) + defer ts.Close() + resp, err := aws.RetryingClient.Get(ts.URL) + if err != nil { + return + } + if resp.StatusCode != 200 { + return "", fmt.Errorf("Bad status code: %d", resp.StatusCode) + } + greeting, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + return + } + return strings.TrimSpace(string(greeting)), nil +} + +func TestClient_expected(t *testing.T) { + body := "foo bar" + + resp, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, body) + }) + if err != nil { + t.Fatal(err) + } + if resp != body { + t.Fatal("Body not as expected.") + } +} + +func TestClient_delay(t *testing.T) { + body := "baz" + wait := 4 + resp, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) { + if wait < 0 { + // If we dipped to zero delay and still failed. + t.Fatal("Never succeeded.") + } + wait -= 1 + time.Sleep(time.Second * time.Duration(wait)) + fmt.Fprintln(w, body) + }) + if err != nil { + t.Fatal(err) + } + if resp != body { + t.Fatal("Body not as expected.", resp) + } +} + +func TestClient_no4xxRetry(t *testing.T) { + tries := 0 + + // Fail once before succeeding. + _, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) { + tries += 1 + http.Error(w, "error", 404) + }) + + if err == nil { + t.Fatal("should have error") + } + + if tries != 1 { + t.Fatalf("should only try once: %d", tries) + } +} + +func TestClient_retries(t *testing.T) { + body := "biz" + failed := false + // Fail once before succeeding. + resp, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) { + if !failed { + http.Error(w, "error", 500) + failed = true + } else { + fmt.Fprintln(w, body) + } + }) + if failed != true { + t.Error("We didn't retry!") + } + if err != nil { + t.Fatal(err) + } + if resp != body { + t.Fatal("Body not as expected.") + } +} + +func TestClient_fails(t *testing.T) { + tries := 0 + // Fail 3 times and return the last error. + _, err := serveAndGet(func(w http.ResponseWriter, r *http.Request) { + tries += 1 + http.Error(w, "error", 500) + }) + if err == nil { + t.Fatal(err) + } + if tries != 3 { + t.Fatal("Didn't retry enough") + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2.go new file mode 100644 index 00000000000..8f94ad539fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2.go @@ -0,0 +1,2599 @@ +// +// goamz - Go packages to interact with the Amazon Web Services. +// +// https://wiki.ubuntu.com/goamz +// +// Copyright (c) 2011 Canonical Ltd. +// +// Written by Gustavo Niemeyer +// + +package ec2 + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/xml" + "fmt" + "log" + "net/http" + "net/http/httputil" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "github.com/mitchellh/goamz/aws" +) + +const debug = false + +// The EC2 type encapsulates operations with a specific EC2 region. +type EC2 struct { + aws.Auth + aws.Region + httpClient *http.Client + private byte // Reserve the right of using private data. +} + +// New creates a new EC2. +func NewWithClient(auth aws.Auth, region aws.Region, client *http.Client) *EC2 { + return &EC2{auth, region, client, 0} +} + +func New(auth aws.Auth, region aws.Region) *EC2 { + return NewWithClient(auth, region, aws.RetryingClient) +} + +// ---------------------------------------------------------------------------- +// Filtering helper. + +// Filter builds filtering parameters to be used in an EC2 query which supports +// filtering. For example: +// +// filter := NewFilter() +// filter.Add("architecture", "i386") +// filter.Add("launch-index", "0") +// resp, err := ec2.Instances(nil, filter) +// +type Filter struct { + m map[string][]string +} + +// NewFilter creates a new Filter. +func NewFilter() *Filter { + return &Filter{make(map[string][]string)} +} + +// Add appends a filtering parameter with the given name and value(s). +func (f *Filter) Add(name string, value ...string) { + f.m[name] = append(f.m[name], value...) +} + +func (f *Filter) addParams(params map[string]string) { + if f != nil { + a := make([]string, len(f.m)) + i := 0 + for k := range f.m { + a[i] = k + i++ + } + sort.StringSlice(a).Sort() + for i, k := range a { + prefix := "Filter." + strconv.Itoa(i+1) + params[prefix+".Name"] = k + for j, v := range f.m[k] { + params[prefix+".Value."+strconv.Itoa(j+1)] = v + } + } + } +} + +// ---------------------------------------------------------------------------- +// Request dispatching logic. + +// Error encapsulates an error returned by EC2. +// +// See http://goo.gl/VZGuC for more details. +type Error struct { + // HTTP status code (200, 403, ...) + StatusCode int + // EC2 error code ("UnsupportedOperation", ...) + Code string + // The human-oriented error message + Message string + RequestId string `xml:"RequestID"` +} + +func (err *Error) Error() string { + if err.Code == "" { + return err.Message + } + + return fmt.Sprintf("%s (%s)", err.Message, err.Code) +} + +// For now a single error inst is being exposed. In the future it may be useful +// to provide access to all of them, but rather than doing it as an array/slice, +// use a *next pointer, so that it's backward compatible and it continues to be +// easy to handle the first error, which is what most people will want. +type xmlErrors struct { + RequestId string `xml:"RequestID"` + Errors []Error `xml:"Errors>Error"` +} + +var timeNow = time.Now + +func (ec2 *EC2) query(params map[string]string, resp interface{}) error { + params["Version"] = "2014-05-01" + params["Timestamp"] = timeNow().In(time.UTC).Format(time.RFC3339) + endpoint, err := url.Parse(ec2.Region.EC2Endpoint) + if err != nil { + return err + } + if endpoint.Path == "" { + endpoint.Path = "/" + } + sign(ec2.Auth, "GET", endpoint.Path, params, endpoint.Host) + endpoint.RawQuery = multimap(params).Encode() + if debug { + log.Printf("get { %v } -> {\n", endpoint.String()) + } + + r, err := ec2.httpClient.Get(endpoint.String()) + if err != nil { + return err + } + defer r.Body.Close() + + if debug { + dump, _ := httputil.DumpResponse(r, true) + log.Printf("response:\n") + log.Printf("%v\n}\n", string(dump)) + } + if r.StatusCode != 200 { + return buildError(r) + } + err = xml.NewDecoder(r.Body).Decode(resp) + return err +} + +func multimap(p map[string]string) url.Values { + q := make(url.Values, len(p)) + for k, v := range p { + q[k] = []string{v} + } + return q +} + +func buildError(r *http.Response) error { + errors := xmlErrors{} + xml.NewDecoder(r.Body).Decode(&errors) + var err Error + if len(errors.Errors) > 0 { + err = errors.Errors[0] + } + err.RequestId = errors.RequestId + err.StatusCode = r.StatusCode + if err.Message == "" { + err.Message = r.Status + } + return &err +} + +func makeParams(action string) map[string]string { + params := make(map[string]string) + params["Action"] = action + return params +} + +func addParamsList(params map[string]string, label string, ids []string) { + for i, id := range ids { + params[label+"."+strconv.Itoa(i+1)] = id + } +} + +func addBlockDeviceParams(prename string, params map[string]string, blockdevices []BlockDeviceMapping) { + for i, k := range blockdevices { + // Fixup index since Amazon counts these from 1 + prefix := prename + "BlockDeviceMapping." + strconv.Itoa(i+1) + "." + + if k.DeviceName != "" { + params[prefix+"DeviceName"] = k.DeviceName + } + if k.VirtualName != "" { + params[prefix+"VirtualName"] = k.VirtualName + } + if k.SnapshotId != "" { + params[prefix+"Ebs.SnapshotId"] = k.SnapshotId + } + if k.VolumeType != "" { + params[prefix+"Ebs.VolumeType"] = k.VolumeType + } + if k.IOPS != 0 { + params[prefix+"Ebs.Iops"] = strconv.FormatInt(k.IOPS, 10) + } + if k.VolumeSize != 0 { + params[prefix+"Ebs.VolumeSize"] = strconv.FormatInt(k.VolumeSize, 10) + } + if k.DeleteOnTermination { + params[prefix+"Ebs.DeleteOnTermination"] = "true" + } + if k.Encrypted { + params[prefix+"Ebs.Encrypted"] = "true" + } + if k.NoDevice { + params[prefix+"NoDevice"] = "" + } + } +} + +// ---------------------------------------------------------------------------- +// Instance management functions and types. + +// The RunInstances type encapsulates options for the respective request in EC2. +// +// See http://goo.gl/Mcm3b for more details. +type RunInstances struct { + ImageId string + MinCount int + MaxCount int + KeyName string + InstanceType string + SecurityGroups []SecurityGroup + IamInstanceProfile string + KernelId string + RamdiskId string + UserData []byte + AvailZone string + PlacementGroupName string + Monitoring bool + SubnetId string + AssociatePublicIpAddress bool + DisableAPITermination bool + ShutdownBehavior string + PrivateIPAddress string + BlockDevices []BlockDeviceMapping +} + +// Response to a RunInstances request. +// +// See http://goo.gl/Mcm3b for more details. +type RunInstancesResp struct { + RequestId string `xml:"requestId"` + ReservationId string `xml:"reservationId"` + OwnerId string `xml:"ownerId"` + SecurityGroups []SecurityGroup `xml:"groupSet>item"` + Instances []Instance `xml:"instancesSet>item"` +} + +// Instance encapsulates a running instance in EC2. +// +// See http://goo.gl/OCH8a for more details. +type Instance struct { + InstanceId string `xml:"instanceId"` + InstanceType string `xml:"instanceType"` + ImageId string `xml:"imageId"` + PrivateDNSName string `xml:"privateDnsName"` + DNSName string `xml:"dnsName"` + KeyName string `xml:"keyName"` + AMILaunchIndex int `xml:"amiLaunchIndex"` + Hypervisor string `xml:"hypervisor"` + VirtType string `xml:"virtualizationType"` + Monitoring string `xml:"monitoring>state"` + AvailZone string `xml:"placement>availabilityZone"` + PlacementGroupName string `xml:"placement>groupName"` + State InstanceState `xml:"instanceState"` + Tags []Tag `xml:"tagSet>item"` + VpcId string `xml:"vpcId"` + SubnetId string `xml:"subnetId"` + IamInstanceProfile string `xml:"iamInstanceProfile"` + PrivateIpAddress string `xml:"privateIpAddress"` + PublicIpAddress string `xml:"ipAddress"` + Architecture string `xml:"architecture"` + LaunchTime time.Time `xml:"launchTime"` + SourceDestCheck bool `xml:"sourceDestCheck"` + SecurityGroups []SecurityGroup `xml:"groupSet>item"` +} + +// RunInstances starts new instances in EC2. +// If options.MinCount and options.MaxCount are both zero, a single instance +// will be started; otherwise if options.MaxCount is zero, options.MinCount +// will be used insteead. +// +// See http://goo.gl/Mcm3b for more details. +func (ec2 *EC2) RunInstances(options *RunInstances) (resp *RunInstancesResp, err error) { + params := makeParams("RunInstances") + params["ImageId"] = options.ImageId + params["InstanceType"] = options.InstanceType + var min, max int + if options.MinCount == 0 && options.MaxCount == 0 { + min = 1 + max = 1 + } else if options.MaxCount == 0 { + min = options.MinCount + max = min + } else { + min = options.MinCount + max = options.MaxCount + } + params["MinCount"] = strconv.Itoa(min) + params["MaxCount"] = strconv.Itoa(max) + token, err := clientToken() + if err != nil { + return nil, err + } + params["ClientToken"] = token + + if options.KeyName != "" { + params["KeyName"] = options.KeyName + } + if options.KernelId != "" { + params["KernelId"] = options.KernelId + } + if options.RamdiskId != "" { + params["RamdiskId"] = options.RamdiskId + } + if options.UserData != nil { + userData := make([]byte, b64.EncodedLen(len(options.UserData))) + b64.Encode(userData, options.UserData) + params["UserData"] = string(userData) + } + if options.AvailZone != "" { + params["Placement.AvailabilityZone"] = options.AvailZone + } + if options.PlacementGroupName != "" { + params["Placement.GroupName"] = options.PlacementGroupName + } + if options.Monitoring { + params["Monitoring.Enabled"] = "true" + } + if options.SubnetId != "" && options.AssociatePublicIpAddress { + // If we have a non-default VPC / Subnet specified, we can flag + // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. + // You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise + // you get: Network interfaces and an instance-level subnet ID may not be specified on the same request + // You also need to attach Security Groups to the NetworkInterface instead of the instance, + // to avoid: Network interfaces and an instance-level security groups may not be specified on + // the same request + params["NetworkInterface.0.DeviceIndex"] = "0" + params["NetworkInterface.0.AssociatePublicIpAddress"] = "true" + params["NetworkInterface.0.SubnetId"] = options.SubnetId + + i := 1 + for _, g := range options.SecurityGroups { + // We only have SecurityGroupId's on NetworkInterface's, no SecurityGroup params. + if g.Id != "" { + params["NetworkInterface.0.SecurityGroupId."+strconv.Itoa(i)] = g.Id + i++ + } + } + } else { + if options.SubnetId != "" { + params["SubnetId"] = options.SubnetId + } + + i, j := 1, 1 + for _, g := range options.SecurityGroups { + if g.Id != "" { + params["SecurityGroupId."+strconv.Itoa(i)] = g.Id + i++ + } else { + params["SecurityGroup."+strconv.Itoa(j)] = g.Name + j++ + } + } + } + if options.IamInstanceProfile != "" { + params["IamInstanceProfile.Name"] = options.IamInstanceProfile + } + if options.DisableAPITermination { + params["DisableApiTermination"] = "true" + } + if options.ShutdownBehavior != "" { + params["InstanceInitiatedShutdownBehavior"] = options.ShutdownBehavior + } + if options.PrivateIPAddress != "" { + params["PrivateIpAddress"] = options.PrivateIPAddress + } + addBlockDeviceParams("", params, options.BlockDevices) + + resp = &RunInstancesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +func clientToken() (string, error) { + // Maximum EC2 client token size is 64 bytes. + // Each byte expands to two when hex encoded. + buf := make([]byte, 32) + _, err := rand.Read(buf) + if err != nil { + return "", err + } + return hex.EncodeToString(buf), nil +} + +// ---------------------------------------------------------------------------- +// Spot Instance management functions and types. + +// The RequestSpotInstances type encapsulates options for the respective request in EC2. +// +// See http://goo.gl/GRZgCD for more details. +type RequestSpotInstances struct { + SpotPrice string + InstanceCount int + Type string + ImageId string + KeyName string + InstanceType string + SecurityGroups []SecurityGroup + IamInstanceProfile string + KernelId string + RamdiskId string + UserData []byte + AvailZone string + PlacementGroupName string + Monitoring bool + SubnetId string + AssociatePublicIpAddress bool + PrivateIPAddress string + BlockDevices []BlockDeviceMapping +} + +type SpotInstanceSpec struct { + ImageId string + KeyName string + InstanceType string + SecurityGroups []SecurityGroup + IamInstanceProfile string + KernelId string + RamdiskId string + UserData []byte + AvailZone string + PlacementGroupName string + Monitoring bool + SubnetId string + AssociatePublicIpAddress bool + PrivateIPAddress string + BlockDevices []BlockDeviceMapping +} + +type SpotLaunchSpec struct { + ImageId string `xml:"imageId"` + KeyName string `xml:"keyName"` + InstanceType string `xml:"instanceType"` + SecurityGroups []SecurityGroup `xml:"groupSet>item"` + IamInstanceProfile string `xml:"iamInstanceProfile"` + KernelId string `xml:"kernelId"` + RamdiskId string `xml:"ramdiskId"` + PlacementGroupName string `xml:"placement>groupName"` + Monitoring bool `xml:"monitoring>enabled"` + SubnetId string `xml:"subnetId"` + BlockDevices []BlockDeviceMapping `xml:"blockDeviceMapping>item"` +} + +type SpotStatus struct { + Code string `xml:"code"` + UpdateTime string `xml:"updateTime"` + Message string `xml:"message"` +} + +type SpotRequestResult struct { + SpotRequestId string `xml:"spotInstanceRequestId"` + SpotPrice string `xml:"spotPrice"` + Type string `xml:"type"` + AvailZone string `xml:"launchedAvailabilityZone"` + InstanceId string `xml:"instanceId"` + State string `xml:"state"` + Status SpotStatus `xml:"status"` + SpotLaunchSpec SpotLaunchSpec `xml:"launchSpecification"` + CreateTime string `xml:"createTime"` + Tags []Tag `xml:"tagSet>item"` +} + +// Response to a RequestSpotInstances request. +// +// See http://goo.gl/GRZgCD for more details. +type RequestSpotInstancesResp struct { + RequestId string `xml:"requestId"` + SpotRequestResults []SpotRequestResult `xml:"spotInstanceRequestSet>item"` +} + +// RequestSpotInstances requests a new spot instances in EC2. +func (ec2 *EC2) RequestSpotInstances(options *RequestSpotInstances) (resp *RequestSpotInstancesResp, err error) { + params := makeParams("RequestSpotInstances") + prefix := "LaunchSpecification" + "." + + params["SpotPrice"] = options.SpotPrice + params[prefix+"ImageId"] = options.ImageId + params[prefix+"InstanceType"] = options.InstanceType + + if options.InstanceCount != 0 { + params["InstanceCount"] = strconv.Itoa(options.InstanceCount) + } + if options.KeyName != "" { + params[prefix+"KeyName"] = options.KeyName + } + if options.KernelId != "" { + params[prefix+"KernelId"] = options.KernelId + } + if options.RamdiskId != "" { + params[prefix+"RamdiskId"] = options.RamdiskId + } + if options.UserData != nil { + userData := make([]byte, b64.EncodedLen(len(options.UserData))) + b64.Encode(userData, options.UserData) + params[prefix+"UserData"] = string(userData) + } + if options.AvailZone != "" { + params[prefix+"Placement.AvailabilityZone"] = options.AvailZone + } + if options.PlacementGroupName != "" { + params[prefix+"Placement.GroupName"] = options.PlacementGroupName + } + if options.Monitoring { + params[prefix+"Monitoring.Enabled"] = "true" + } + if options.SubnetId != "" && options.AssociatePublicIpAddress { + // If we have a non-default VPC / Subnet specified, we can flag + // AssociatePublicIpAddress to get a Public IP assigned. By default these are not provided. + // You cannot specify both SubnetId and the NetworkInterface.0.* parameters though, otherwise + // you get: Network interfaces and an instance-level subnet ID may not be specified on the same request + // You also need to attach Security Groups to the NetworkInterface instead of the instance, + // to avoid: Network interfaces and an instance-level security groups may not be specified on + // the same request + params[prefix+"NetworkInterface.0.DeviceIndex"] = "0" + params[prefix+"NetworkInterface.0.AssociatePublicIpAddress"] = "true" + params[prefix+"NetworkInterface.0.SubnetId"] = options.SubnetId + + i := 1 + for _, g := range options.SecurityGroups { + // We only have SecurityGroupId's on NetworkInterface's, no SecurityGroup params. + if g.Id != "" { + params[prefix+"NetworkInterface.0.SecurityGroupId."+strconv.Itoa(i)] = g.Id + i++ + } + } + } else { + if options.SubnetId != "" { + params[prefix+"SubnetId"] = options.SubnetId + } + + i, j := 1, 1 + for _, g := range options.SecurityGroups { + if g.Id != "" { + params[prefix+"SecurityGroupId."+strconv.Itoa(i)] = g.Id + i++ + } else { + params[prefix+"SecurityGroup."+strconv.Itoa(j)] = g.Name + j++ + } + } + } + if options.IamInstanceProfile != "" { + params[prefix+"IamInstanceProfile.Name"] = options.IamInstanceProfile + } + if options.PrivateIPAddress != "" { + params[prefix+"PrivateIpAddress"] = options.PrivateIPAddress + } + addBlockDeviceParams(prefix, params, options.BlockDevices) + + resp = &RequestSpotInstancesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Response to a DescribeSpotInstanceRequests request. +// +// See http://goo.gl/KsKJJk for more details. +type SpotRequestsResp struct { + RequestId string `xml:"requestId"` + SpotRequestResults []SpotRequestResult `xml:"spotInstanceRequestSet>item"` +} + +// DescribeSpotInstanceRequests returns details about spot requests in EC2. Both parameters +// are optional, and if provided will limit the spot requests returned to those +// matching the given spot request ids or filtering rules. +// +// See http://goo.gl/KsKJJk for more details. +func (ec2 *EC2) DescribeSpotRequests(spotrequestIds []string, filter *Filter) (resp *SpotRequestsResp, err error) { + params := makeParams("DescribeSpotInstanceRequests") + addParamsList(params, "SpotInstanceRequestId", spotrequestIds) + filter.addParams(params) + resp = &SpotRequestsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Response to a CancelSpotInstanceRequests request. +// +// See http://goo.gl/3BKHj for more details. +type CancelSpotRequestResult struct { + SpotRequestId string `xml:"spotInstanceRequestId"` + State string `xml:"state"` +} +type CancelSpotRequestsResp struct { + RequestId string `xml:"requestId"` + CancelSpotRequestResults []CancelSpotRequestResult `xml:"spotInstanceRequestSet>item"` +} + +// CancelSpotRequests requests the cancellation of spot requests when the given ids. +// +// See http://goo.gl/3BKHj for more details. +func (ec2 *EC2) CancelSpotRequests(spotrequestIds []string) (resp *CancelSpotRequestsResp, err error) { + params := makeParams("CancelSpotInstanceRequests") + addParamsList(params, "SpotInstanceRequestId", spotrequestIds) + resp = &CancelSpotRequestsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Response to a TerminateInstances request. +// +// See http://goo.gl/3BKHj for more details. +type TerminateInstancesResp struct { + RequestId string `xml:"requestId"` + StateChanges []InstanceStateChange `xml:"instancesSet>item"` +} + +// InstanceState encapsulates the state of an instance in EC2. +// +// See http://goo.gl/y3ZBq for more details. +type InstanceState struct { + Code int `xml:"code"` // Watch out, bits 15-8 have unpublished meaning. + Name string `xml:"name"` +} + +// InstanceStateChange informs of the previous and current states +// for an instance when a state change is requested. +type InstanceStateChange struct { + InstanceId string `xml:"instanceId"` + CurrentState InstanceState `xml:"currentState"` + PreviousState InstanceState `xml:"previousState"` +} + +// TerminateInstances requests the termination of instances when the given ids. +// +// See http://goo.gl/3BKHj for more details. +func (ec2 *EC2) TerminateInstances(instIds []string) (resp *TerminateInstancesResp, err error) { + params := makeParams("TerminateInstances") + addParamsList(params, "InstanceId", instIds) + resp = &TerminateInstancesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Response to a DescribeInstances request. +// +// See http://goo.gl/mLbmw for more details. +type InstancesResp struct { + RequestId string `xml:"requestId"` + Reservations []Reservation `xml:"reservationSet>item"` +} + +// Reservation represents details about a reservation in EC2. +// +// See http://goo.gl/0ItPT for more details. +type Reservation struct { + ReservationId string `xml:"reservationId"` + OwnerId string `xml:"ownerId"` + RequesterId string `xml:"requesterId"` + SecurityGroups []SecurityGroup `xml:"groupSet>item"` + Instances []Instance `xml:"instancesSet>item"` +} + +// Instances returns details about instances in EC2. Both parameters +// are optional, and if provided will limit the instances returned to those +// matching the given instance ids or filtering rules. +// +// See http://goo.gl/4No7c for more details. +func (ec2 *EC2) Instances(instIds []string, filter *Filter) (resp *InstancesResp, err error) { + params := makeParams("DescribeInstances") + addParamsList(params, "InstanceId", instIds) + filter.addParams(params) + resp = &InstancesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ---------------------------------------------------------------------------- +// Volume management + +// The CreateVolume request parameters +// +// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateVolume.html +type CreateVolume struct { + AvailZone string + Size int64 + SnapshotId string + VolumeType string + IOPS int64 + Encrypted bool +} + +// Response to an AttachVolume request +type AttachVolumeResp struct { + RequestId string `xml:"requestId"` + VolumeId string `xml:"volumeId"` + InstanceId string `xml:"instanceId"` + Device string `xml:"device"` + Status string `xml:"status"` + AttachTime string `xml:"attachTime"` +} + +// Response to a CreateVolume request +type CreateVolumeResp struct { + RequestId string `xml:"requestId"` + VolumeId string `xml:"volumeId"` + Size int64 `xml:"size"` + SnapshotId string `xml:"snapshotId"` + AvailZone string `xml:"availabilityZone"` + Status string `xml:"status"` + CreateTime string `xml:"createTime"` + VolumeType string `xml:"volumeType"` + IOPS int64 `xml:"iops"` + Encrypted bool `xml:"encrypted"` +} + +// Volume is a single volume. +type Volume struct { + VolumeId string `xml:"volumeId"` + Size string `xml:"size"` + SnapshotId string `xml:"snapshotId"` + AvailZone string `xml:"availabilityZone"` + Status string `xml:"status"` + Attachments []VolumeAttachment `xml:"attachmentSet>item"` + VolumeType string `xml:"volumeType"` + IOPS int64 `xml:"iops"` + Encrypted bool `xml:"encrypted"` + Tags []Tag `xml:"tagSet>item"` +} + +type VolumeAttachment struct { + VolumeId string `xml:"volumeId"` + InstanceId string `xml:"instanceId"` + Device string `xml:"device"` + Status string `xml:"status"` +} + +// Response to a DescribeVolumes request +type VolumesResp struct { + RequestId string `xml:"requestId"` + Volumes []Volume `xml:"volumeSet>item"` +} + +// Attach a volume. +func (ec2 *EC2) AttachVolume(volumeId string, instanceId string, device string) (resp *AttachVolumeResp, err error) { + params := makeParams("AttachVolume") + params["VolumeId"] = volumeId + params["InstanceId"] = instanceId + params["Device"] = device + + resp = &AttachVolumeResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Create a new volume. +func (ec2 *EC2) CreateVolume(options *CreateVolume) (resp *CreateVolumeResp, err error) { + params := makeParams("CreateVolume") + params["AvailabilityZone"] = options.AvailZone + if options.Size > 0 { + params["Size"] = strconv.FormatInt(options.Size, 10) + } + + if options.SnapshotId != "" { + params["SnapshotId"] = options.SnapshotId + } + + if options.VolumeType != "" { + params["VolumeType"] = options.VolumeType + } + + if options.IOPS > 0 { + params["Iops"] = strconv.FormatInt(options.IOPS, 10) + } + + if options.Encrypted { + params["Encrypted"] = "true" + } + + resp = &CreateVolumeResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Delete an EBS volume. +func (ec2 *EC2) DeleteVolume(id string) (resp *SimpleResp, err error) { + params := makeParams("DeleteVolume") + params["VolumeId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Detaches an EBS volume. +func (ec2 *EC2) DetachVolume(id string) (resp *SimpleResp, err error) { + params := makeParams("DetachVolume") + params["VolumeId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Finds or lists all volumes. +func (ec2 *EC2) Volumes(volIds []string, filter *Filter) (resp *VolumesResp, err error) { + params := makeParams("DescribeVolumes") + addParamsList(params, "VolumeId", volIds) + filter.addParams(params) + resp = &VolumesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ---------------------------------------------------------------------------- +// ElasticIp management (for VPC) + +// The AllocateAddress request parameters +// +// see http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-AllocateAddress.html +type AllocateAddress struct { + Domain string +} + +// Response to an AllocateAddress request +type AllocateAddressResp struct { + RequestId string `xml:"requestId"` + PublicIp string `xml:"publicIp"` + Domain string `xml:"domain"` + AllocationId string `xml:"allocationId"` +} + +// The AssociateAddress request parameters +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-AssociateAddress.html +type AssociateAddress struct { + InstanceId string + PublicIp string + AllocationId string + AllowReassociation bool +} + +// Response to an AssociateAddress request +type AssociateAddressResp struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` + AssociationId string `xml:"associationId"` +} + +// Address represents an Elastic IP Address +// See http://goo.gl/uxCjp7 for more details +type Address struct { + PublicIp string `xml:"publicIp"` + AllocationId string `xml:"allocationId"` + Domain string `xml:"domain"` + InstanceId string `xml:"instanceId"` + AssociationId string `xml:"associationId"` + NetworkInterfaceId string `xml:"networkInterfaceId"` + NetworkInterfaceOwnerId string `xml:"networkInterfaceOwnerId"` + PrivateIpAddress string `xml:"privateIpAddress"` +} + +type DescribeAddressesResp struct { + RequestId string `xml:"requestId"` + Addresses []Address `xml:"addressesSet>item"` +} + +// Allocate a new Elastic IP. +func (ec2 *EC2) AllocateAddress(options *AllocateAddress) (resp *AllocateAddressResp, err error) { + params := makeParams("AllocateAddress") + params["Domain"] = options.Domain + + resp = &AllocateAddressResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Release an Elastic IP (VPC). +func (ec2 *EC2) ReleaseAddress(id string) (resp *SimpleResp, err error) { + params := makeParams("ReleaseAddress") + params["AllocationId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Release an Elastic IP (Public) +func (ec2 *EC2) ReleasePublicAddress(publicIp string) (resp *SimpleResp, err error) { + params := makeParams("ReleaseAddress") + params["PublicIp"] = publicIp + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Associate an address with a VPC instance. +func (ec2 *EC2) AssociateAddress(options *AssociateAddress) (resp *AssociateAddressResp, err error) { + params := makeParams("AssociateAddress") + params["InstanceId"] = options.InstanceId + if options.PublicIp != "" { + params["PublicIp"] = options.PublicIp + } + if options.AllocationId != "" { + params["AllocationId"] = options.AllocationId + } + if options.AllowReassociation { + params["AllowReassociation"] = "true" + } + + resp = &AssociateAddressResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Disassociate an address from a VPC instance. +func (ec2 *EC2) DisassociateAddress(id string) (resp *SimpleResp, err error) { + params := makeParams("DisassociateAddress") + params["AssociationId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// DescribeAddresses returns details about one or more +// Elastic IP Addresses. Returned addresses can be +// filtered by Public IP, Allocation ID or multiple filters +// +// See http://goo.gl/zW7J4p for more details. +func (ec2 *EC2) Addresses(publicIps []string, allocationIds []string, filter *Filter) (resp *DescribeAddressesResp, err error) { + params := makeParams("DescribeAddresses") + addParamsList(params, "PublicIp", publicIps) + addParamsList(params, "AllocationId", allocationIds) + filter.addParams(params) + resp = &DescribeAddressesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ---------------------------------------------------------------------------- +// Image and snapshot management functions and types. + +// The CreateImage request parameters. +// +// See http://goo.gl/cxU41 for more details. +type CreateImage struct { + InstanceId string + Name string + Description string + NoReboot bool + BlockDevices []BlockDeviceMapping +} + +// Response to a CreateImage request. +// +// See http://goo.gl/cxU41 for more details. +type CreateImageResp struct { + RequestId string `xml:"requestId"` + ImageId string `xml:"imageId"` +} + +// Response to a DescribeImages request. +// +// See http://goo.gl/hLnyg for more details. +type ImagesResp struct { + RequestId string `xml:"requestId"` + Images []Image `xml:"imagesSet>item"` +} + +// Response to a DescribeImageAttribute request. +// +// See http://goo.gl/bHO3zT for more details. +type ImageAttributeResp struct { + RequestId string `xml:"requestId"` + ImageId string `xml:"imageId"` + Kernel string `xml:"kernel>value"` + RamDisk string `xml:"ramdisk>value"` + Description string `xml:"description>value"` + Group string `xml:"launchPermission>item>group"` + UserIds []string `xml:"launchPermission>item>userId"` + ProductCodes []string `xml:"productCodes>item>productCode"` + BlockDevices []BlockDeviceMapping `xml:"blockDeviceMapping>item"` +} + +// The RegisterImage request parameters. +type RegisterImage struct { + ImageLocation string + Name string + Description string + Architecture string + KernelId string + RamdiskId string + RootDeviceName string + VirtType string + SriovNetSupport string + BlockDevices []BlockDeviceMapping +} + +// Response to a RegisterImage request. +type RegisterImageResp struct { + RequestId string `xml:"requestId"` + ImageId string `xml:"imageId"` +} + +// Response to a DegisterImage request. +// +// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DeregisterImage.html +type DeregisterImageResp struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` +} + +// BlockDeviceMapping represents the association of a block device with an image. +// +// See http://goo.gl/wnDBf for more details. +type BlockDeviceMapping struct { + DeviceName string `xml:"deviceName"` + VirtualName string `xml:"virtualName"` + SnapshotId string `xml:"ebs>snapshotId"` + VolumeType string `xml:"ebs>volumeType"` + VolumeSize int64 `xml:"ebs>volumeSize"` + DeleteOnTermination bool `xml:"ebs>deleteOnTermination"` + Encrypted bool `xml:"ebs>encrypted"` + NoDevice bool `xml:"noDevice"` + + // The number of I/O operations per second (IOPS) that the volume supports. + IOPS int64 `xml:"ebs>iops"` +} + +// Image represents details about an image. +// +// See http://goo.gl/iSqJG for more details. +type Image struct { + Id string `xml:"imageId"` + Name string `xml:"name"` + Description string `xml:"description"` + Type string `xml:"imageType"` + State string `xml:"imageState"` + Location string `xml:"imageLocation"` + Public bool `xml:"isPublic"` + Architecture string `xml:"architecture"` + Platform string `xml:"platform"` + ProductCodes []string `xml:"productCode>item>productCode"` + KernelId string `xml:"kernelId"` + RamdiskId string `xml:"ramdiskId"` + StateReason string `xml:"stateReason"` + OwnerId string `xml:"imageOwnerId"` + OwnerAlias string `xml:"imageOwnerAlias"` + RootDeviceType string `xml:"rootDeviceType"` + RootDeviceName string `xml:"rootDeviceName"` + VirtualizationType string `xml:"virtualizationType"` + Hypervisor string `xml:"hypervisor"` + BlockDevices []BlockDeviceMapping `xml:"blockDeviceMapping>item"` + Tags []Tag `xml:"tagSet>item"` +} + +// The ModifyImageAttribute request parameters. +type ModifyImageAttribute struct { + AddUsers []string + RemoveUsers []string + AddGroups []string + RemoveGroups []string + ProductCodes []string + Description string +} + +// The CopyImage request parameters. +// +// See http://goo.gl/hQwPCK for more details. +type CopyImage struct { + SourceRegion string + SourceImageId string + Name string + Description string + ClientToken string +} + +// Response to a CopyImage request. +// +// See http://goo.gl/hQwPCK for more details. +type CopyImageResp struct { + RequestId string `xml:"requestId"` + ImageId string `xml:"imageId"` +} + +// Creates an Amazon EBS-backed AMI from an Amazon EBS-backed instance +// that is either running or stopped. +// +// See http://goo.gl/cxU41 for more details. +func (ec2 *EC2) CreateImage(options *CreateImage) (resp *CreateImageResp, err error) { + params := makeParams("CreateImage") + params["InstanceId"] = options.InstanceId + params["Name"] = options.Name + if options.Description != "" { + params["Description"] = options.Description + } + if options.NoReboot { + params["NoReboot"] = "true" + } + addBlockDeviceParams("", params, options.BlockDevices) + + resp = &CreateImageResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Images returns details about available images. +// The ids and filter parameters, if provided, will limit the images returned. +// For example, to get all the private images associated with this account set +// the boolean filter "is-public" to 0. +// For list of filters: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeImages.html +// +// Note: calling this function with nil ids and filter parameters will result in +// a very large number of images being returned. +// +// See http://goo.gl/SRBhW for more details. +func (ec2 *EC2) Images(ids []string, filter *Filter) (resp *ImagesResp, err error) { + params := makeParams("DescribeImages") + for i, id := range ids { + params["ImageId."+strconv.Itoa(i+1)] = id + } + filter.addParams(params) + + resp = &ImagesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ImagesByOwners returns details about available images. +// The ids, owners, and filter parameters, if provided, will limit the images returned. +// For example, to get all the private images associated with this account set +// the boolean filter "is-public" to 0. +// For list of filters: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeImages.html +// +// Note: calling this function with nil ids and filter parameters will result in +// a very large number of images being returned. +// +// See http://goo.gl/SRBhW for more details. +func (ec2 *EC2) ImagesByOwners(ids []string, owners []string, filter *Filter) (resp *ImagesResp, err error) { + params := makeParams("DescribeImages") + for i, id := range ids { + params["ImageId."+strconv.Itoa(i+1)] = id + } + for i, owner := range owners { + params[fmt.Sprintf("Owner.%d", i+1)] = owner + } + + filter.addParams(params) + + resp = &ImagesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ImageAttribute describes an attribute of an AMI. +// You can specify only one attribute at a time. +// Valid attributes are: +// description | kernel | ramdisk | launchPermission | productCodes | blockDeviceMapping +// +// See http://goo.gl/bHO3zT for more details. +func (ec2 *EC2) ImageAttribute(imageId, attribute string) (resp *ImageAttributeResp, err error) { + params := makeParams("DescribeImageAttribute") + params["ImageId"] = imageId + params["Attribute"] = attribute + + resp = &ImageAttributeResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ModifyImageAttribute sets attributes for an image. +// +// See http://goo.gl/YUjO4G for more details. +func (ec2 *EC2) ModifyImageAttribute(imageId string, options *ModifyImageAttribute) (resp *SimpleResp, err error) { + params := makeParams("ModifyImageAttribute") + params["ImageId"] = imageId + if options.Description != "" { + params["Description.Value"] = options.Description + } + + if options.AddUsers != nil { + for i, user := range options.AddUsers { + p := fmt.Sprintf("LaunchPermission.Add.%d.UserId", i+1) + params[p] = user + } + } + + if options.RemoveUsers != nil { + for i, user := range options.RemoveUsers { + p := fmt.Sprintf("LaunchPermission.Remove.%d.UserId", i+1) + params[p] = user + } + } + + if options.AddGroups != nil { + for i, group := range options.AddGroups { + p := fmt.Sprintf("LaunchPermission.Add.%d.Group", i+1) + params[p] = group + } + } + + if options.RemoveGroups != nil { + for i, group := range options.RemoveGroups { + p := fmt.Sprintf("LaunchPermission.Remove.%d.Group", i+1) + params[p] = group + } + } + + if options.ProductCodes != nil { + addParamsList(params, "ProductCode", options.ProductCodes) + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + resp = nil + } + + return +} + +// Registers a new AMI with EC2. +// +// See: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-RegisterImage.html +func (ec2 *EC2) RegisterImage(options *RegisterImage) (resp *RegisterImageResp, err error) { + params := makeParams("RegisterImage") + params["Name"] = options.Name + if options.ImageLocation != "" { + params["ImageLocation"] = options.ImageLocation + } + + if options.Description != "" { + params["Description"] = options.Description + } + + if options.Architecture != "" { + params["Architecture"] = options.Architecture + } + + if options.KernelId != "" { + params["KernelId"] = options.KernelId + } + + if options.RamdiskId != "" { + params["RamdiskId"] = options.RamdiskId + } + + if options.RootDeviceName != "" { + params["RootDeviceName"] = options.RootDeviceName + } + + if options.VirtType != "" { + params["VirtualizationType"] = options.VirtType + } + + if options.SriovNetSupport != "" { + params["SriovNetSupport"] = "simple" + } + + addBlockDeviceParams("", params, options.BlockDevices) + + resp = &RegisterImageResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Degisters an image. Note that this does not delete the backing stores of the AMI. +// +// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DeregisterImage.html +func (ec2 *EC2) DeregisterImage(imageId string) (resp *DeregisterImageResp, err error) { + params := makeParams("DeregisterImage") + params["ImageId"] = imageId + + resp = &DeregisterImageResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Copy and Image from one region to another. +// +// See http://goo.gl/hQwPCK for more details. +func (ec2 *EC2) CopyImage(options *CopyImage) (resp *CopyImageResp, err error) { + params := makeParams("CopyImage") + + if options.SourceRegion != "" { + params["SourceRegion"] = options.SourceRegion + } + + if options.SourceImageId != "" { + params["SourceImageId"] = options.SourceImageId + } + + if options.Name != "" { + params["Name"] = options.Name + } + + if options.Description != "" { + params["Description"] = options.Description + } + + if options.ClientToken != "" { + params["ClientToken"] = options.ClientToken + } + + resp = &CopyImageResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Response to a CreateSnapshot request. +// +// See http://goo.gl/ttcda for more details. +type CreateSnapshotResp struct { + RequestId string `xml:"requestId"` + Snapshot +} + +// CreateSnapshot creates a volume snapshot and stores it in S3. +// +// See http://goo.gl/ttcda for more details. +func (ec2 *EC2) CreateSnapshot(volumeId, description string) (resp *CreateSnapshotResp, err error) { + params := makeParams("CreateSnapshot") + params["VolumeId"] = volumeId + params["Description"] = description + + resp = &CreateSnapshotResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// DeleteSnapshots deletes the volume snapshots with the given ids. +// +// Note: If you make periodic snapshots of a volume, the snapshots are +// incremental so that only the blocks on the device that have changed +// since your last snapshot are incrementally saved in the new snapshot. +// Even though snapshots are saved incrementally, the snapshot deletion +// process is designed so that you need to retain only the most recent +// snapshot in order to restore the volume. +// +// See http://goo.gl/vwU1y for more details. +func (ec2 *EC2) DeleteSnapshots(ids []string) (resp *SimpleResp, err error) { + params := makeParams("DeleteSnapshot") + for i, id := range ids { + params["SnapshotId."+strconv.Itoa(i+1)] = id + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Response to a DescribeSnapshots request. +// +// See http://goo.gl/nClDT for more details. +type SnapshotsResp struct { + RequestId string `xml:"requestId"` + Snapshots []Snapshot `xml:"snapshotSet>item"` +} + +// Snapshot represents details about a volume snapshot. +// +// See http://goo.gl/nkovs for more details. +type Snapshot struct { + Id string `xml:"snapshotId"` + VolumeId string `xml:"volumeId"` + VolumeSize string `xml:"volumeSize"` + Status string `xml:"status"` + StartTime string `xml:"startTime"` + Description string `xml:"description"` + Progress string `xml:"progress"` + OwnerId string `xml:"ownerId"` + OwnerAlias string `xml:"ownerAlias"` + Encrypted bool `xml:"encrypted"` + Tags []Tag `xml:"tagSet>item"` +} + +// Snapshots returns details about volume snapshots available to the user. +// The ids and filter parameters, if provided, limit the snapshots returned. +// +// See http://goo.gl/ogJL4 for more details. +func (ec2 *EC2) Snapshots(ids []string, filter *Filter) (resp *SnapshotsResp, err error) { + params := makeParams("DescribeSnapshots") + for i, id := range ids { + params["SnapshotId."+strconv.Itoa(i+1)] = id + } + filter.addParams(params) + + resp = &SnapshotsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ---------------------------------------------------------------------------- +// KeyPair management functions and types. + +type KeyPair struct { + Name string `xml:"keyName"` + Fingerprint string `xml:"keyFingerprint"` +} + +type KeyPairsResp struct { + RequestId string `xml:"requestId"` + Keys []KeyPair `xml:"keySet>item"` +} + +type CreateKeyPairResp struct { + RequestId string `xml:"requestId"` + KeyName string `xml:"keyName"` + KeyFingerprint string `xml:"keyFingerprint"` + KeyMaterial string `xml:"keyMaterial"` +} + +type ImportKeyPairResponse struct { + RequestId string `xml:"requestId"` + KeyName string `xml:"keyName"` + KeyFingerprint string `xml:"keyFingerprint"` +} + +// CreateKeyPair creates a new key pair and returns the private key contents. +// +// See http://goo.gl/0S6hV +func (ec2 *EC2) CreateKeyPair(keyName string) (resp *CreateKeyPairResp, err error) { + params := makeParams("CreateKeyPair") + params["KeyName"] = keyName + + resp = &CreateKeyPairResp{} + err = ec2.query(params, resp) + if err == nil { + resp.KeyFingerprint = strings.TrimSpace(resp.KeyFingerprint) + } + return +} + +// DeleteKeyPair deletes a key pair. +// +// See http://goo.gl/0bqok +func (ec2 *EC2) DeleteKeyPair(name string) (resp *SimpleResp, err error) { + params := makeParams("DeleteKeyPair") + params["KeyName"] = name + + resp = &SimpleResp{} + err = ec2.query(params, resp) + return +} + +// KeyPairs returns list of key pairs for this account +// +// See http://goo.gl/Apzsfz +func (ec2 *EC2) KeyPairs(keynames []string, filter *Filter) (resp *KeyPairsResp, err error) { + params := makeParams("DescribeKeyPairs") + for i, name := range keynames { + params["KeyName."+strconv.Itoa(i)] = name + } + filter.addParams(params) + + resp = &KeyPairsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return resp, nil +} + +// ImportKeyPair imports a key into AWS +// +// See http://goo.gl/NbZUvw +func (ec2 *EC2) ImportKeyPair(keyname string, key string) (resp *ImportKeyPairResponse, err error) { + params := makeParams("ImportKeyPair") + params["KeyName"] = keyname + + // Oddly, AWS requires the key material to be base64-encoded, even if it was + // already encoded. So, we force another round of encoding... + // c.f. https://groups.google.com/forum/?fromgroups#!topic/boto-dev/IczrStO9Q8M + params["PublicKeyMaterial"] = base64.StdEncoding.EncodeToString([]byte(key)) + + resp = &ImportKeyPairResponse{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// ---------------------------------------------------------------------------- +// Security group management functions and types. + +// SimpleResp represents a response to an EC2 request which on success will +// return no other information besides a request id. +type SimpleResp struct { + XMLName xml.Name + RequestId string `xml:"requestId"` +} + +// CreateSecurityGroupResp represents a response to a CreateSecurityGroup request. +type CreateSecurityGroupResp struct { + SecurityGroup + RequestId string `xml:"requestId"` +} + +// CreateSecurityGroup run a CreateSecurityGroup request in EC2, with the provided +// name and description. +// +// See http://goo.gl/Eo7Yl for more details. +func (ec2 *EC2) CreateSecurityGroup(group SecurityGroup) (resp *CreateSecurityGroupResp, err error) { + params := makeParams("CreateSecurityGroup") + params["GroupName"] = group.Name + params["GroupDescription"] = group.Description + if group.VpcId != "" { + params["VpcId"] = group.VpcId + } + + resp = &CreateSecurityGroupResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + resp.Name = group.Name + return resp, nil +} + +// SecurityGroupsResp represents a response to a DescribeSecurityGroups +// request in EC2. +// +// See http://goo.gl/k12Uy for more details. +type SecurityGroupsResp struct { + RequestId string `xml:"requestId"` + Groups []SecurityGroupInfo `xml:"securityGroupInfo>item"` +} + +// SecurityGroup encapsulates details for a security group in EC2. +// +// See http://goo.gl/CIdyP for more details. +type SecurityGroupInfo struct { + SecurityGroup + OwnerId string `xml:"ownerId"` + Description string `xml:"groupDescription"` + IPPerms []IPPerm `xml:"ipPermissions>item"` +} + +// IPPerm represents an allowance within an EC2 security group. +// +// See http://goo.gl/4oTxv for more details. +type IPPerm struct { + Protocol string `xml:"ipProtocol"` + FromPort int `xml:"fromPort"` + ToPort int `xml:"toPort"` + SourceIPs []string `xml:"ipRanges>item>cidrIp"` + SourceGroups []UserSecurityGroup `xml:"groups>item"` +} + +// UserSecurityGroup holds a security group and the owner +// of that group. +type UserSecurityGroup struct { + Id string `xml:"groupId"` + Name string `xml:"groupName"` + OwnerId string `xml:"userId"` +} + +// SecurityGroup represents an EC2 security group. +// If SecurityGroup is used as a parameter, then one of Id or Name +// may be empty. If both are set, then Id is used. +type SecurityGroup struct { + Id string `xml:"groupId"` + Name string `xml:"groupName"` + Description string `xml:"groupDescription"` + VpcId string `xml:"vpcId"` +} + +// SecurityGroupNames is a convenience function that +// returns a slice of security groups with the given names. +func SecurityGroupNames(names ...string) []SecurityGroup { + g := make([]SecurityGroup, len(names)) + for i, name := range names { + g[i] = SecurityGroup{Name: name} + } + return g +} + +// SecurityGroupNames is a convenience function that +// returns a slice of security groups with the given ids. +func SecurityGroupIds(ids ...string) []SecurityGroup { + g := make([]SecurityGroup, len(ids)) + for i, id := range ids { + g[i] = SecurityGroup{Id: id} + } + return g +} + +// SecurityGroups returns details about security groups in EC2. Both parameters +// are optional, and if provided will limit the security groups returned to those +// matching the given groups or filtering rules. +// +// See http://goo.gl/k12Uy for more details. +func (ec2 *EC2) SecurityGroups(groups []SecurityGroup, filter *Filter) (resp *SecurityGroupsResp, err error) { + params := makeParams("DescribeSecurityGroups") + i, j := 1, 1 + for _, g := range groups { + if g.Id != "" { + params["GroupId."+strconv.Itoa(i)] = g.Id + i++ + } else { + params["GroupName."+strconv.Itoa(j)] = g.Name + j++ + } + } + filter.addParams(params) + + resp = &SecurityGroupsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// DeleteSecurityGroup removes the given security group in EC2. +// +// See http://goo.gl/QJJDO for more details. +func (ec2 *EC2) DeleteSecurityGroup(group SecurityGroup) (resp *SimpleResp, err error) { + params := makeParams("DeleteSecurityGroup") + if group.Id != "" { + params["GroupId"] = group.Id + } else { + params["GroupName"] = group.Name + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// AuthorizeSecurityGroup creates an allowance for clients matching the provided +// rules to access instances within the given security group. +// +// See http://goo.gl/u2sDJ for more details. +func (ec2 *EC2) AuthorizeSecurityGroup(group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) { + return ec2.authOrRevoke("AuthorizeSecurityGroupIngress", group, perms) +} + +// AuthorizeSecurityGroupEgress creates an allowance for clients matching the provided +// rules for egress access. +// +// See http://goo.gl/UHnH4L for more details. +func (ec2 *EC2) AuthorizeSecurityGroupEgress(group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) { + return ec2.authOrRevoke("AuthorizeSecurityGroupEgress", group, perms) +} + +// RevokeSecurityGroup revokes permissions from a group. +// +// See http://goo.gl/ZgdxA for more details. +func (ec2 *EC2) RevokeSecurityGroup(group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) { + return ec2.authOrRevoke("RevokeSecurityGroupIngress", group, perms) +} + +func (ec2 *EC2) authOrRevoke(op string, group SecurityGroup, perms []IPPerm) (resp *SimpleResp, err error) { + params := makeParams(op) + if group.Id != "" { + params["GroupId"] = group.Id + } else { + params["GroupName"] = group.Name + } + + for i, perm := range perms { + prefix := "IpPermissions." + strconv.Itoa(i+1) + params[prefix+".IpProtocol"] = perm.Protocol + params[prefix+".FromPort"] = strconv.Itoa(perm.FromPort) + params[prefix+".ToPort"] = strconv.Itoa(perm.ToPort) + for j, ip := range perm.SourceIPs { + params[prefix+".IpRanges."+strconv.Itoa(j+1)+".CidrIp"] = ip + } + for j, g := range perm.SourceGroups { + subprefix := prefix + ".Groups." + strconv.Itoa(j+1) + if g.OwnerId != "" { + params[subprefix+".UserId"] = g.OwnerId + } + if g.Id != "" { + params[subprefix+".GroupId"] = g.Id + } else { + params[subprefix+".GroupName"] = g.Name + } + } + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// ResourceTag represents key-value metadata used to classify and organize +// EC2 instances. +// +// See http://goo.gl/bncl3 for more details +type Tag struct { + Key string `xml:"key"` + Value string `xml:"value"` +} + +// CreateTags adds or overwrites one or more tags for the specified taggable resources. +// For a list of tagable resources, see: http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Using_Tags.html +// +// See http://goo.gl/Vmkqc for more details +func (ec2 *EC2) CreateTags(resourceIds []string, tags []Tag) (resp *SimpleResp, err error) { + params := makeParams("CreateTags") + addParamsList(params, "ResourceId", resourceIds) + + for j, tag := range tags { + params["Tag."+strconv.Itoa(j+1)+".Key"] = tag.Key + params["Tag."+strconv.Itoa(j+1)+".Value"] = tag.Value + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +type TagsResp struct { + RequestId string `xml:"requestId"` + Tags []ResourceTag `xml:"tagSet>item"` +} + +type ResourceTag struct { + Tag + ResourceId string `xml:"resourceId"` + ResourceType string `xml:"resourceType"` +} + +func (ec2 *EC2) Tags(filter *Filter) (*TagsResp, error) { + params := makeParams("DescribeTags") + filter.addParams(params) + + resp := &TagsResp{} + if err := ec2.query(params, resp); err != nil { + return nil, err + } + + return resp, nil +} + +// Response to a StartInstances request. +// +// See http://goo.gl/awKeF for more details. +type StartInstanceResp struct { + RequestId string `xml:"requestId"` + StateChanges []InstanceStateChange `xml:"instancesSet>item"` +} + +// Response to a StopInstances request. +// +// See http://goo.gl/436dJ for more details. +type StopInstanceResp struct { + RequestId string `xml:"requestId"` + StateChanges []InstanceStateChange `xml:"instancesSet>item"` +} + +// StartInstances starts an Amazon EBS-backed AMI that you've previously stopped. +// +// See http://goo.gl/awKeF for more details. +func (ec2 *EC2) StartInstances(ids ...string) (resp *StartInstanceResp, err error) { + params := makeParams("StartInstances") + addParamsList(params, "InstanceId", ids) + resp = &StartInstanceResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// StopInstances requests stopping one or more Amazon EBS-backed instances. +// +// See http://goo.gl/436dJ for more details. +func (ec2 *EC2) StopInstances(ids ...string) (resp *StopInstanceResp, err error) { + params := makeParams("StopInstances") + addParamsList(params, "InstanceId", ids) + resp = &StopInstanceResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// RebootInstance requests a reboot of one or more instances. This operation is asynchronous; +// it only queues a request to reboot the specified instance(s). The operation will succeed +// if the instances are valid and belong to you. +// +// Requests to reboot terminated instances are ignored. +// +// See http://goo.gl/baoUf for more details. +func (ec2 *EC2) RebootInstances(ids ...string) (resp *SimpleResp, err error) { + params := makeParams("RebootInstances") + addParamsList(params, "InstanceId", ids) + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// The ModifyInstanceAttribute request parameters. +type ModifyInstance struct { + InstanceType string + BlockDevices []BlockDeviceMapping + DisableAPITermination bool + EbsOptimized bool + SecurityGroups []SecurityGroup + ShutdownBehavior string + KernelId string + RamdiskId string + SourceDestCheck bool + SriovNetSupport bool + UserData []byte + + SetSourceDestCheck bool +} + +// Response to a ModifyInstanceAttribute request. +// +// http://goo.gl/icuXh5 for more details. +type ModifyInstanceResp struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` +} + +// ModifyImageAttribute modifies the specified attribute of the specified instance. +// You can specify only one attribute at a time. To modify some attributes, the +// instance must be stopped. +// +// See http://goo.gl/icuXh5 for more details. +func (ec2 *EC2) ModifyInstance(instId string, options *ModifyInstance) (resp *ModifyInstanceResp, err error) { + params := makeParams("ModifyInstanceAttribute") + params["InstanceId"] = instId + addBlockDeviceParams("", params, options.BlockDevices) + + if options.InstanceType != "" { + params["InstanceType.Value"] = options.InstanceType + } + + if options.DisableAPITermination { + params["DisableApiTermination.Value"] = "true" + } + + if options.EbsOptimized { + params["EbsOptimized"] = "true" + } + + if options.ShutdownBehavior != "" { + params["InstanceInitiatedShutdownBehavior.Value"] = options.ShutdownBehavior + } + + if options.KernelId != "" { + params["Kernel.Value"] = options.KernelId + } + + if options.RamdiskId != "" { + params["Ramdisk.Value"] = options.RamdiskId + } + + if options.SourceDestCheck || options.SetSourceDestCheck { + if options.SourceDestCheck { + params["SourceDestCheck.Value"] = "true" + } else { + params["SourceDestCheck.Value"] = "false" + } + } + + if options.SriovNetSupport { + params["SriovNetSupport.Value"] = "simple" + } + + if options.UserData != nil { + userData := make([]byte, b64.EncodedLen(len(options.UserData))) + b64.Encode(userData, options.UserData) + params["UserData"] = string(userData) + } + + i := 1 + for _, g := range options.SecurityGroups { + if g.Id != "" { + params["GroupId."+strconv.Itoa(i)] = g.Id + i++ + } + } + + resp = &ModifyInstanceResp{} + err = ec2.query(params, resp) + if err != nil { + resp = nil + } + return +} + +// ---------------------------------------------------------------------------- +// VPC management functions and types. + +// The CreateVpc request parameters +// +// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateVpc.html +type CreateVpc struct { + CidrBlock string + InstanceTenancy string +} + +// Response to a CreateVpc request +type CreateVpcResp struct { + RequestId string `xml:"requestId"` + VPC VPC `xml:"vpc"` +} + +// The ModifyVpcAttribute request parameters. +// +// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVpcAttribute.html for more details. +type ModifyVpcAttribute struct { + EnableDnsSupport bool + EnableDnsHostnames bool + + SetEnableDnsSupport bool + SetEnableDnsHostnames bool +} + +// Response to a DescribeVpcAttribute request. +// +// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVpcAttribute.html for more details. +type VpcAttributeResp struct { + RequestId string `xml:"requestId"` + VpcId string `xml:"vpcId"` + EnableDnsSupport bool `xml:"enableDnsSupport>value"` + EnableDnsHostnames bool `xml:"enableDnsHostnames>value"` +} + +// CreateInternetGateway request parameters. +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateInternetGateway.html +type CreateInternetGateway struct{} + +// CreateInternetGateway response +type CreateInternetGatewayResp struct { + RequestId string `xml:"requestId"` + InternetGateway InternetGateway `xml:"internetGateway"` +} + +// The CreateRouteTable request parameters. +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateRouteTable.html +type CreateRouteTable struct { + VpcId string +} + +// Response to a CreateRouteTable request. +type CreateRouteTableResp struct { + RequestId string `xml:"requestId"` + RouteTable RouteTable `xml:"routeTable"` +} + +// CreateRoute request parameters +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateRoute.html +type CreateRoute struct { + RouteTableId string + DestinationCidrBlock string + GatewayId string + InstanceId string + NetworkInterfaceId string + VpcPeeringConnectionId string +} +type ReplaceRoute struct { + RouteTableId string + DestinationCidrBlock string + GatewayId string + InstanceId string + NetworkInterfaceId string + VpcPeeringConnectionId string +} + +type AssociateRouteTableResp struct { + RequestId string `xml:"requestId"` + AssociationId string `xml:"associationId"` +} +type ReassociateRouteTableResp struct { + RequestId string `xml:"requestId"` + AssociationId string `xml:"newAssociationId"` +} + +// The CreateSubnet request parameters +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateSubnet.html +type CreateSubnet struct { + VpcId string + CidrBlock string + AvailabilityZone string +} + +// Response to a CreateSubnet request +type CreateSubnetResp struct { + RequestId string `xml:"requestId"` + Subnet Subnet `xml:"subnet"` +} + +// Response to a DescribeInternetGateways request. +type InternetGatewaysResp struct { + RequestId string `xml:"requestId"` + InternetGateways []InternetGateway `xml:"internetGatewaySet>item"` +} + +// Response to a DescribeRouteTables request. +type RouteTablesResp struct { + RequestId string `xml:"requestId"` + RouteTables []RouteTable `xml:"routeTableSet>item"` +} + +// Response to a DescribeVpcs request. +type VpcsResp struct { + RequestId string `xml:"requestId"` + VPCs []VPC `xml:"vpcSet>item"` +} + +// Internet Gateway +type InternetGateway struct { + InternetGatewayId string `xml:"internetGatewayId"` + Attachments []InternetGatewayAttachment `xml:"attachmentSet>item"` + Tags []Tag `xml:"tagSet>item"` +} + +type InternetGatewayAttachment struct { + VpcId string `xml:"vpcId"` + State string `xml:"state"` +} + +// Routing Table +type RouteTable struct { + RouteTableId string `xml:"routeTableId"` + VpcId string `xml:"vpcId"` + Associations []RouteTableAssociation `xml:"associationSet>item"` + Routes []Route `xml:"routeSet>item"` + Tags []Tag `xml:"tagSet>item"` +} + +type RouteTableAssociation struct { + AssociationId string `xml:"routeTableAssociationId"` + RouteTableId string `xml:"routeTableId"` + SubnetId string `xml:"subnetId"` + Main bool `xml:"main"` +} + +type Route struct { + DestinationCidrBlock string `xml:"destinationCidrBlock"` + GatewayId string `xml:"gatewayId"` + InstanceId string `xml:"instanceId"` + InstanceOwnerId string `xml:"instanceOwnerId"` + NetworkInterfaceId string `xml:"networkInterfaceId"` + State string `xml:"state"` + Origin string `xml:"origin"` + VpcPeeringConnectionId string `xml:"vpcPeeringConnectionId"` +} + +// Subnet +type Subnet struct { + SubnetId string `xml:"subnetId"` + State string `xml:"state"` + VpcId string `xml:"vpcId"` + CidrBlock string `xml:"cidrBlock"` + AvailableIpAddressCount int `xml:"availableIpAddressCount"` + AvailabilityZone string `xml:"availabilityZone"` + DefaultForAZ bool `xml:"defaultForAz"` + MapPublicIpOnLaunch bool `xml:"mapPublicIpOnLaunch"` + Tags []Tag `xml:"tagSet>item"` +} + +// VPC represents a single VPC. +type VPC struct { + VpcId string `xml:"vpcId"` + State string `xml:"state"` + CidrBlock string `xml:"cidrBlock"` + DHCPOptionsID string `xml:"dhcpOptionsId"` + InstanceTenancy string `xml:"instanceTenancy"` + IsDefault bool `xml:"isDefault"` + Tags []Tag `xml:"tagSet>item"` +} + +// Response to a DescribeSubnets request. +type SubnetsResp struct { + RequestId string `xml:"requestId"` + Subnets []Subnet `xml:"subnetSet>item"` +} + +// Create a new VPC. +func (ec2 *EC2) CreateVpc(options *CreateVpc) (resp *CreateVpcResp, err error) { + params := makeParams("CreateVpc") + params["CidrBlock"] = options.CidrBlock + + if options.InstanceTenancy != "" { + params["InstanceTenancy"] = options.InstanceTenancy + } + + resp = &CreateVpcResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Delete a VPC. +func (ec2 *EC2) DeleteVpc(id string) (resp *SimpleResp, err error) { + params := makeParams("DeleteVpc") + params["VpcId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// DescribeVpcs +// +// See http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeVpcs.html +func (ec2 *EC2) DescribeVpcs(ids []string, filter *Filter) (resp *VpcsResp, err error) { + params := makeParams("DescribeVpcs") + addParamsList(params, "VpcId", ids) + filter.addParams(params) + resp = &VpcsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// VpcAttribute describes an attribute of a VPC. +// You can specify only one attribute at a time. +// Valid attributes are: +// enableDnsSupport | enableDnsHostnames +// +// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVpcAttribute.html for more details. +func (ec2 *EC2) VpcAttribute(vpcId, attribute string) (resp *VpcAttributeResp, err error) { + params := makeParams("DescribeVpcAttribute") + params["VpcId"] = vpcId + params["Attribute"] = attribute + + resp = &VpcAttributeResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// ModifyVpcAttribute modifies the specified attribute of the specified VPC. +// +// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-ModifyVpcAttribute.html for more details. +func (ec2 *EC2) ModifyVpcAttribute(vpcId string, options *ModifyVpcAttribute) (*SimpleResp, error) { + params := makeParams("ModifyVpcAttribute") + + params["VpcId"] = vpcId + + if options.SetEnableDnsSupport { + params["EnableDnsSupport.Value"] = strconv.FormatBool(options.EnableDnsSupport) + } + + if options.SetEnableDnsHostnames { + params["EnableDnsHostnames.Value"] = strconv.FormatBool(options.EnableDnsHostnames) + } + + resp := &SimpleResp{} + if err := ec2.query(params, resp); err != nil { + return nil, err + } + + return resp, nil +} + +// Create a new subnet. +func (ec2 *EC2) CreateSubnet(options *CreateSubnet) (resp *CreateSubnetResp, err error) { + params := makeParams("CreateSubnet") + params["AvailabilityZone"] = options.AvailabilityZone + params["CidrBlock"] = options.CidrBlock + params["VpcId"] = options.VpcId + + resp = &CreateSubnetResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Delete a Subnet. +func (ec2 *EC2) DeleteSubnet(id string) (resp *SimpleResp, err error) { + params := makeParams("DeleteSubnet") + params["SubnetId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// DescribeSubnets +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeSubnets.html +func (ec2 *EC2) DescribeSubnets(ids []string, filter *Filter) (resp *SubnetsResp, err error) { + params := makeParams("DescribeSubnets") + addParamsList(params, "SubnetId", ids) + filter.addParams(params) + + resp = &SubnetsResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Create a new internet gateway. +func (ec2 *EC2) CreateInternetGateway( + options *CreateInternetGateway) (resp *CreateInternetGatewayResp, err error) { + params := makeParams("CreateInternetGateway") + + resp = &CreateInternetGatewayResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Attach an InternetGateway. +func (ec2 *EC2) AttachInternetGateway(id, vpcId string) (resp *SimpleResp, err error) { + params := makeParams("AttachInternetGateway") + params["InternetGatewayId"] = id + params["VpcId"] = vpcId + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Detach an InternetGateway. +func (ec2 *EC2) DetachInternetGateway(id, vpcId string) (resp *SimpleResp, err error) { + params := makeParams("DetachInternetGateway") + params["InternetGatewayId"] = id + params["VpcId"] = vpcId + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Delete an InternetGateway. +func (ec2 *EC2) DeleteInternetGateway(id string) (resp *SimpleResp, err error) { + params := makeParams("DeleteInternetGateway") + params["InternetGatewayId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// DescribeInternetGateways +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeInternetGateways.html +func (ec2 *EC2) DescribeInternetGateways(ids []string, filter *Filter) (resp *InternetGatewaysResp, err error) { + params := makeParams("DescribeInternetGateways") + addParamsList(params, "InternetGatewayId", ids) + filter.addParams(params) + + resp = &InternetGatewaysResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Create a new routing table. +func (ec2 *EC2) CreateRouteTable( + options *CreateRouteTable) (resp *CreateRouteTableResp, err error) { + params := makeParams("CreateRouteTable") + params["VpcId"] = options.VpcId + + resp = &CreateRouteTableResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Delete a RouteTable. +func (ec2 *EC2) DeleteRouteTable(id string) (resp *SimpleResp, err error) { + params := makeParams("DeleteRouteTable") + params["RouteTableId"] = id + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// DescribeRouteTables +// +// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeRouteTables.html +func (ec2 *EC2) DescribeRouteTables(ids []string, filter *Filter) (resp *RouteTablesResp, err error) { + params := makeParams("DescribeRouteTables") + addParamsList(params, "RouteTableId", ids) + filter.addParams(params) + + resp = &RouteTablesResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + + return +} + +// Associate a routing table. +func (ec2 *EC2) AssociateRouteTable(id, subnetId string) (*AssociateRouteTableResp, error) { + params := makeParams("AssociateRouteTable") + params["RouteTableId"] = id + params["SubnetId"] = subnetId + + resp := &AssociateRouteTableResp{} + err := ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// Disassociate a routing table. +func (ec2 *EC2) DisassociateRouteTable(id string) (*SimpleResp, error) { + params := makeParams("DisassociateRouteTable") + params["AssociationId"] = id + + resp := &SimpleResp{} + err := ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// Re-associate a routing table. +func (ec2 *EC2) ReassociateRouteTable(id, routeTableId string) (*ReassociateRouteTableResp, error) { + params := makeParams("ReplaceRouteTableAssociation") + params["AssociationId"] = id + params["RouteTableId"] = routeTableId + + resp := &ReassociateRouteTableResp{} + err := ec2.query(params, resp) + if err != nil { + return nil, err + } + return resp, nil +} + +// Create a new route. +func (ec2 *EC2) CreateRoute(options *CreateRoute) (resp *SimpleResp, err error) { + params := makeParams("CreateRoute") + params["RouteTableId"] = options.RouteTableId + params["DestinationCidrBlock"] = options.DestinationCidrBlock + + if v := options.GatewayId; v != "" { + params["GatewayId"] = v + } + if v := options.InstanceId; v != "" { + params["InstanceId"] = v + } + if v := options.NetworkInterfaceId; v != "" { + params["NetworkInterfaceId"] = v + } + if v := options.VpcPeeringConnectionId; v != "" { + params["VpcPeeringConnectionId"] = v + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Delete a Route. +func (ec2 *EC2) DeleteRoute(routeTableId, cidr string) (resp *SimpleResp, err error) { + params := makeParams("DeleteRoute") + params["RouteTableId"] = routeTableId + params["DestinationCidrBlock"] = cidr + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// Replace a new route. +func (ec2 *EC2) ReplaceRoute(options *ReplaceRoute) (resp *SimpleResp, err error) { + params := makeParams("ReplaceRoute") + params["RouteTableId"] = options.RouteTableId + params["DestinationCidrBlock"] = options.DestinationCidrBlock + + if v := options.GatewayId; v != "" { + params["GatewayId"] = v + } + if v := options.InstanceId; v != "" { + params["InstanceId"] = v + } + if v := options.NetworkInterfaceId; v != "" { + params["NetworkInterfaceId"] = v + } + if v := options.VpcPeeringConnectionId; v != "" { + params["VpcPeeringConnectionId"] = v + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} + +// The ResetImageAttribute request parameters. +type ResetImageAttribute struct { + Attribute string +} + +// ResetImageAttribute resets an attribute of an AMI to its default value. +// +// http://goo.gl/r6ZCPm for more details. +func (ec2 *EC2) ResetImageAttribute(imageId string, options *ResetImageAttribute) (resp *SimpleResp, err error) { + params := makeParams("ResetImageAttribute") + params["ImageId"] = imageId + + if options.Attribute != "" { + params["Attribute"] = options.Attribute + } + + resp = &SimpleResp{} + err = ec2.query(params, resp) + if err != nil { + return nil, err + } + return +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2_test.go new file mode 100644 index 00000000000..849bfe2e6bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2_test.go @@ -0,0 +1,1243 @@ +package ec2_test + +import ( + "testing" + + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/goamz/testutil" + . "github.com/motain/gocheck" +) + +func Test(t *testing.T) { + TestingT(t) +} + +var _ = Suite(&S{}) + +type S struct { + ec2 *ec2.EC2 +} + +var testServer = testutil.NewHTTPServer() + +func (s *S) SetUpSuite(c *C) { + testServer.Start() + auth := aws.Auth{"abc", "123", ""} + s.ec2 = ec2.NewWithClient( + auth, + aws.Region{EC2Endpoint: testServer.URL}, + testutil.DefaultClient, + ) +} + +func (s *S) TearDownTest(c *C) { + testServer.Flush() +} + +func (s *S) TestRunInstancesErrorDump(c *C) { + testServer.Response(400, nil, ErrorDump) + + options := ec2.RunInstances{ + ImageId: "ami-a6f504cf", // Ubuntu Maverick, i386, instance store + InstanceType: "t1.micro", // Doesn't work with micro, results in 400. + } + + msg := `AMIs with an instance-store root device are not supported for the instance type 't1\.micro'\.` + + resp, err := s.ec2.RunInstances(&options) + + testServer.WaitRequest() + + c.Assert(resp, IsNil) + c.Assert(err, ErrorMatches, msg+` \(UnsupportedOperation\)`) + + ec2err, ok := err.(*ec2.Error) + c.Assert(ok, Equals, true) + c.Assert(ec2err.StatusCode, Equals, 400) + c.Assert(ec2err.Code, Equals, "UnsupportedOperation") + c.Assert(ec2err.Message, Matches, msg) + c.Assert(ec2err.RequestId, Equals, "0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4") +} + +func (s *S) TestRequestSpotInstancesErrorDump(c *C) { + testServer.Response(400, nil, ErrorDump) + + options := ec2.RequestSpotInstances{ + SpotPrice: "0.01", + ImageId: "ami-a6f504cf", // Ubuntu Maverick, i386, instance store + InstanceType: "t1.micro", // Doesn't work with micro, results in 400. + } + + msg := `AMIs with an instance-store root device are not supported for the instance type 't1\.micro'\.` + + resp, err := s.ec2.RequestSpotInstances(&options) + + testServer.WaitRequest() + + c.Assert(resp, IsNil) + c.Assert(err, ErrorMatches, msg+` \(UnsupportedOperation\)`) + + ec2err, ok := err.(*ec2.Error) + c.Assert(ok, Equals, true) + c.Assert(ec2err.StatusCode, Equals, 400) + c.Assert(ec2err.Code, Equals, "UnsupportedOperation") + c.Assert(ec2err.Message, Matches, msg) + c.Assert(ec2err.RequestId, Equals, "0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4") +} + +func (s *S) TestRunInstancesErrorWithoutXML(c *C) { + testServer.Responses(5, 500, nil, "") + options := ec2.RunInstances{ImageId: "image-id"} + + resp, err := s.ec2.RunInstances(&options) + + testServer.WaitRequest() + + c.Assert(resp, IsNil) + c.Assert(err, ErrorMatches, "500 Internal Server Error") + + ec2err, ok := err.(*ec2.Error) + c.Assert(ok, Equals, true) + c.Assert(ec2err.StatusCode, Equals, 500) + c.Assert(ec2err.Code, Equals, "") + c.Assert(ec2err.Message, Equals, "500 Internal Server Error") + c.Assert(ec2err.RequestId, Equals, "") +} + +func (s *S) TestRequestSpotInstancesErrorWithoutXML(c *C) { + testServer.Responses(5, 500, nil, "") + options := ec2.RequestSpotInstances{SpotPrice: "spot-price", ImageId: "image-id"} + + resp, err := s.ec2.RequestSpotInstances(&options) + + testServer.WaitRequest() + + c.Assert(resp, IsNil) + c.Assert(err, ErrorMatches, "500 Internal Server Error") + + ec2err, ok := err.(*ec2.Error) + c.Assert(ok, Equals, true) + c.Assert(ec2err.StatusCode, Equals, 500) + c.Assert(ec2err.Code, Equals, "") + c.Assert(ec2err.Message, Equals, "500 Internal Server Error") + c.Assert(ec2err.RequestId, Equals, "") +} + +func (s *S) TestRunInstancesExample(c *C) { + testServer.Response(200, nil, RunInstancesExample) + + options := ec2.RunInstances{ + KeyName: "my-keys", + ImageId: "image-id", + InstanceType: "inst-type", + SecurityGroups: []ec2.SecurityGroup{{Name: "g1"}, {Id: "g2"}, {Name: "g3"}, {Id: "g4"}}, + UserData: []byte("1234"), + KernelId: "kernel-id", + RamdiskId: "ramdisk-id", + AvailZone: "zone", + PlacementGroupName: "group", + Monitoring: true, + SubnetId: "subnet-id", + DisableAPITermination: true, + ShutdownBehavior: "terminate", + PrivateIPAddress: "10.0.0.25", + BlockDevices: []ec2.BlockDeviceMapping{ + {DeviceName: "/dev/sdb", VirtualName: "ephemeral0"}, + {DeviceName: "/dev/sdc", SnapshotId: "snap-a08912c9", DeleteOnTermination: true}, + }, + } + resp, err := s.ec2.RunInstances(&options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"RunInstances"}) + c.Assert(req.Form["ImageId"], DeepEquals, []string{"image-id"}) + c.Assert(req.Form["MinCount"], DeepEquals, []string{"1"}) + c.Assert(req.Form["MaxCount"], DeepEquals, []string{"1"}) + c.Assert(req.Form["KeyName"], DeepEquals, []string{"my-keys"}) + c.Assert(req.Form["InstanceType"], DeepEquals, []string{"inst-type"}) + c.Assert(req.Form["SecurityGroup.1"], DeepEquals, []string{"g1"}) + c.Assert(req.Form["SecurityGroup.2"], DeepEquals, []string{"g3"}) + c.Assert(req.Form["SecurityGroupId.1"], DeepEquals, []string{"g2"}) + c.Assert(req.Form["SecurityGroupId.2"], DeepEquals, []string{"g4"}) + c.Assert(req.Form["UserData"], DeepEquals, []string{"MTIzNA=="}) + c.Assert(req.Form["KernelId"], DeepEquals, []string{"kernel-id"}) + c.Assert(req.Form["RamdiskId"], DeepEquals, []string{"ramdisk-id"}) + c.Assert(req.Form["Placement.AvailabilityZone"], DeepEquals, []string{"zone"}) + c.Assert(req.Form["Placement.GroupName"], DeepEquals, []string{"group"}) + c.Assert(req.Form["Monitoring.Enabled"], DeepEquals, []string{"true"}) + c.Assert(req.Form["SubnetId"], DeepEquals, []string{"subnet-id"}) + c.Assert(req.Form["DisableApiTermination"], DeepEquals, []string{"true"}) + c.Assert(req.Form["InstanceInitiatedShutdownBehavior"], DeepEquals, []string{"terminate"}) + c.Assert(req.Form["PrivateIpAddress"], DeepEquals, []string{"10.0.0.25"}) + c.Assert(req.Form["BlockDeviceMapping.1.DeviceName"], DeepEquals, []string{"/dev/sdb"}) + c.Assert(req.Form["BlockDeviceMapping.1.VirtualName"], DeepEquals, []string{"ephemeral0"}) + c.Assert(req.Form["BlockDeviceMapping.2.Ebs.SnapshotId"], DeepEquals, []string{"snap-a08912c9"}) + c.Assert(req.Form["BlockDeviceMapping.2.Ebs.DeleteOnTermination"], DeepEquals, []string{"true"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.ReservationId, Equals, "r-47a5402e") + c.Assert(resp.OwnerId, Equals, "999988887777") + c.Assert(resp.SecurityGroups, DeepEquals, []ec2.SecurityGroup{{Name: "default", Id: "sg-67ad940e"}}) + c.Assert(resp.Instances, HasLen, 3) + + i0 := resp.Instances[0] + c.Assert(i0.InstanceId, Equals, "i-2ba64342") + c.Assert(i0.InstanceType, Equals, "m1.small") + c.Assert(i0.ImageId, Equals, "ami-60a54009") + c.Assert(i0.Monitoring, Equals, "enabled") + c.Assert(i0.KeyName, Equals, "example-key-name") + c.Assert(i0.AMILaunchIndex, Equals, 0) + c.Assert(i0.VirtType, Equals, "paravirtual") + c.Assert(i0.Hypervisor, Equals, "xen") + + i1 := resp.Instances[1] + c.Assert(i1.InstanceId, Equals, "i-2bc64242") + c.Assert(i1.InstanceType, Equals, "m1.small") + c.Assert(i1.ImageId, Equals, "ami-60a54009") + c.Assert(i1.Monitoring, Equals, "enabled") + c.Assert(i1.KeyName, Equals, "example-key-name") + c.Assert(i1.AMILaunchIndex, Equals, 1) + c.Assert(i1.VirtType, Equals, "paravirtual") + c.Assert(i1.Hypervisor, Equals, "xen") + + i2 := resp.Instances[2] + c.Assert(i2.InstanceId, Equals, "i-2be64332") + c.Assert(i2.InstanceType, Equals, "m1.small") + c.Assert(i2.ImageId, Equals, "ami-60a54009") + c.Assert(i2.Monitoring, Equals, "enabled") + c.Assert(i2.KeyName, Equals, "example-key-name") + c.Assert(i2.AMILaunchIndex, Equals, 2) + c.Assert(i2.VirtType, Equals, "paravirtual") + c.Assert(i2.Hypervisor, Equals, "xen") +} + +func (s *S) TestRequestSpotInstancesExample(c *C) { + testServer.Response(200, nil, RequestSpotInstancesExample) + + options := ec2.RequestSpotInstances{ + SpotPrice: "0.5", + KeyName: "my-keys", + ImageId: "image-id", + InstanceType: "inst-type", + SecurityGroups: []ec2.SecurityGroup{{Name: "g1"}, {Id: "g2"}, {Name: "g3"}, {Id: "g4"}}, + UserData: []byte("1234"), + KernelId: "kernel-id", + RamdiskId: "ramdisk-id", + AvailZone: "zone", + PlacementGroupName: "group", + Monitoring: true, + SubnetId: "subnet-id", + PrivateIPAddress: "10.0.0.25", + BlockDevices: []ec2.BlockDeviceMapping{ + {DeviceName: "/dev/sdb", VirtualName: "ephemeral0"}, + {DeviceName: "/dev/sdc", SnapshotId: "snap-a08912c9", DeleteOnTermination: true}, + }, + } + resp, err := s.ec2.RequestSpotInstances(&options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"RequestSpotInstances"}) + c.Assert(req.Form["SpotPrice"], DeepEquals, []string{"0.5"}) + c.Assert(req.Form["LaunchSpecification.ImageId"], DeepEquals, []string{"image-id"}) + c.Assert(req.Form["LaunchSpecification.KeyName"], DeepEquals, []string{"my-keys"}) + c.Assert(req.Form["LaunchSpecification.InstanceType"], DeepEquals, []string{"inst-type"}) + c.Assert(req.Form["LaunchSpecification.SecurityGroup.1"], DeepEquals, []string{"g1"}) + c.Assert(req.Form["LaunchSpecification.SecurityGroup.2"], DeepEquals, []string{"g3"}) + c.Assert(req.Form["LaunchSpecification.SecurityGroupId.1"], DeepEquals, []string{"g2"}) + c.Assert(req.Form["LaunchSpecification.SecurityGroupId.2"], DeepEquals, []string{"g4"}) + c.Assert(req.Form["LaunchSpecification.UserData"], DeepEquals, []string{"MTIzNA=="}) + c.Assert(req.Form["LaunchSpecification.KernelId"], DeepEquals, []string{"kernel-id"}) + c.Assert(req.Form["LaunchSpecification.RamdiskId"], DeepEquals, []string{"ramdisk-id"}) + c.Assert(req.Form["LaunchSpecification.Placement.AvailabilityZone"], DeepEquals, []string{"zone"}) + c.Assert(req.Form["LaunchSpecification.Placement.GroupName"], DeepEquals, []string{"group"}) + c.Assert(req.Form["LaunchSpecification.Monitoring.Enabled"], DeepEquals, []string{"true"}) + c.Assert(req.Form["LaunchSpecification.SubnetId"], DeepEquals, []string{"subnet-id"}) + c.Assert(req.Form["LaunchSpecification.PrivateIpAddress"], DeepEquals, []string{"10.0.0.25"}) + c.Assert(req.Form["LaunchSpecification.BlockDeviceMapping.1.DeviceName"], DeepEquals, []string{"/dev/sdb"}) + c.Assert(req.Form["LaunchSpecification.BlockDeviceMapping.1.VirtualName"], DeepEquals, []string{"ephemeral0"}) + c.Assert(req.Form["LaunchSpecification.BlockDeviceMapping.2.Ebs.SnapshotId"], DeepEquals, []string{"snap-a08912c9"}) + c.Assert(req.Form["LaunchSpecification.BlockDeviceMapping.2.Ebs.DeleteOnTermination"], DeepEquals, []string{"true"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.SpotRequestResults[0].SpotRequestId, Equals, "sir-1a2b3c4d") + c.Assert(resp.SpotRequestResults[0].SpotPrice, Equals, "0.5") + c.Assert(resp.SpotRequestResults[0].State, Equals, "open") + c.Assert(resp.SpotRequestResults[0].SpotLaunchSpec.ImageId, Equals, "ami-1a2b3c4d") + c.Assert(resp.SpotRequestResults[0].Status.Code, Equals, "pending-evaluation") + c.Assert(resp.SpotRequestResults[0].Status.UpdateTime, Equals, "2008-05-07T12:51:50.000Z") + c.Assert(resp.SpotRequestResults[0].Status.Message, Equals, "Your Spot request has been submitted for review, and is pending evaluation.") +} + +func (s *S) TestCancelSpotRequestsExample(c *C) { + testServer.Response(200, nil, CancelSpotRequestsExample) + + resp, err := s.ec2.CancelSpotRequests([]string{"s-1", "s-2"}) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CancelSpotInstanceRequests"}) + c.Assert(req.Form["SpotInstanceRequestId.1"], DeepEquals, []string{"s-1"}) + c.Assert(req.Form["SpotInstanceRequestId.2"], DeepEquals, []string{"s-2"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.CancelSpotRequestResults[0].SpotRequestId, Equals, "sir-1a2b3c4d") + c.Assert(resp.CancelSpotRequestResults[0].State, Equals, "cancelled") +} + +func (s *S) TestTerminateInstancesExample(c *C) { + testServer.Response(200, nil, TerminateInstancesExample) + + resp, err := s.ec2.TerminateInstances([]string{"i-1", "i-2"}) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"TerminateInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-1"}) + c.Assert(req.Form["InstanceId.2"], DeepEquals, []string{"i-2"}) + c.Assert(req.Form["UserData"], IsNil) + c.Assert(req.Form["KernelId"], IsNil) + c.Assert(req.Form["RamdiskId"], IsNil) + c.Assert(req.Form["Placement.AvailabilityZone"], IsNil) + c.Assert(req.Form["Placement.GroupName"], IsNil) + c.Assert(req.Form["Monitoring.Enabled"], IsNil) + c.Assert(req.Form["SubnetId"], IsNil) + c.Assert(req.Form["DisableApiTermination"], IsNil) + c.Assert(req.Form["InstanceInitiatedShutdownBehavior"], IsNil) + c.Assert(req.Form["PrivateIpAddress"], IsNil) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.StateChanges, HasLen, 1) + c.Assert(resp.StateChanges[0].InstanceId, Equals, "i-3ea74257") + c.Assert(resp.StateChanges[0].CurrentState.Code, Equals, 32) + c.Assert(resp.StateChanges[0].CurrentState.Name, Equals, "shutting-down") + c.Assert(resp.StateChanges[0].PreviousState.Code, Equals, 16) + c.Assert(resp.StateChanges[0].PreviousState.Name, Equals, "running") +} + +func (s *S) TestDescribeSpotRequestsExample(c *C) { + testServer.Response(200, nil, DescribeSpotRequestsExample) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.DescribeSpotRequests([]string{"s-1", "s-2"}, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSpotInstanceRequests"}) + c.Assert(req.Form["SpotInstanceRequestId.1"], DeepEquals, []string{"s-1"}) + c.Assert(req.Form["SpotInstanceRequestId.2"], DeepEquals, []string{"s-2"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "b1719f2a-5334-4479-b2f1-26926EXAMPLE") + c.Assert(resp.SpotRequestResults[0].SpotRequestId, Equals, "sir-1a2b3c4d") + c.Assert(resp.SpotRequestResults[0].State, Equals, "active") + c.Assert(resp.SpotRequestResults[0].SpotPrice, Equals, "0.5") + c.Assert(resp.SpotRequestResults[0].SpotLaunchSpec.ImageId, Equals, "ami-1a2b3c4d") + c.Assert(resp.SpotRequestResults[0].Status.Code, Equals, "fulfilled") + c.Assert(resp.SpotRequestResults[0].Status.UpdateTime, Equals, "2008-05-07T12:51:50.000Z") + c.Assert(resp.SpotRequestResults[0].Status.Message, Equals, "Your Spot request is fulfilled.") +} + +func (s *S) TestDescribeInstancesExample1(c *C) { + testServer.Response(200, nil, DescribeInstancesExample1) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.Instances([]string{"i-1", "i-2"}, nil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-1"}) + c.Assert(req.Form["InstanceId.2"], DeepEquals, []string{"i-2"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "98e3c9a4-848c-4d6d-8e8a-b1bdEXAMPLE") + c.Assert(resp.Reservations, HasLen, 2) + + r0 := resp.Reservations[0] + c.Assert(r0.ReservationId, Equals, "r-b27e30d9") + c.Assert(r0.OwnerId, Equals, "999988887777") + c.Assert(r0.RequesterId, Equals, "854251627541") + c.Assert(r0.SecurityGroups, DeepEquals, []ec2.SecurityGroup{{Name: "default", Id: "sg-67ad940e"}}) + c.Assert(r0.Instances, HasLen, 1) + + r0i := r0.Instances[0] + c.Assert(r0i.InstanceId, Equals, "i-c5cd56af") + c.Assert(r0i.PrivateDNSName, Equals, "domU-12-31-39-10-56-34.compute-1.internal") + c.Assert(r0i.DNSName, Equals, "ec2-174-129-165-232.compute-1.amazonaws.com") + c.Assert(r0i.AvailZone, Equals, "us-east-1b") +} + +func (s *S) TestDescribeInstancesExample2(c *C) { + testServer.Response(200, nil, DescribeInstancesExample2) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.Instances([]string{"i-1", "i-2"}, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-1"}) + c.Assert(req.Form["InstanceId.2"], DeepEquals, []string{"i-2"}) + c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"}) + c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"}) + c.Assert(req.Form["Filter.1.Value.2"], IsNil) + c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"}) + c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"}) + c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Reservations, HasLen, 1) + + r0 := resp.Reservations[0] + r0i := r0.Instances[0] + c.Assert(r0i.State.Code, Equals, 16) + c.Assert(r0i.State.Name, Equals, "running") + + r0t0 := r0i.Tags[0] + r0t1 := r0i.Tags[1] + c.Assert(r0t0.Key, Equals, "webserver") + c.Assert(r0t0.Value, Equals, "") + c.Assert(r0t1.Key, Equals, "stack") + c.Assert(r0t1.Value, Equals, "Production") +} + +func (s *S) TestCreateImageExample(c *C) { + testServer.Response(200, nil, CreateImageExample) + + options := &ec2.CreateImage{ + InstanceId: "i-123456", + Name: "foo", + Description: "Test CreateImage", + NoReboot: true, + BlockDevices: []ec2.BlockDeviceMapping{ + {DeviceName: "/dev/sdb", VirtualName: "ephemeral0"}, + {DeviceName: "/dev/sdc", SnapshotId: "snap-a08912c9", DeleteOnTermination: true}, + }, + } + + resp, err := s.ec2.CreateImage(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CreateImage"}) + c.Assert(req.Form["InstanceId"], DeepEquals, []string{options.InstanceId}) + c.Assert(req.Form["Name"], DeepEquals, []string{options.Name}) + c.Assert(req.Form["Description"], DeepEquals, []string{options.Description}) + c.Assert(req.Form["NoReboot"], DeepEquals, []string{"true"}) + c.Assert(req.Form["BlockDeviceMapping.1.DeviceName"], DeepEquals, []string{"/dev/sdb"}) + c.Assert(req.Form["BlockDeviceMapping.1.VirtualName"], DeepEquals, []string{"ephemeral0"}) + c.Assert(req.Form["BlockDeviceMapping.2.DeviceName"], DeepEquals, []string{"/dev/sdc"}) + c.Assert(req.Form["BlockDeviceMapping.2.Ebs.SnapshotId"], DeepEquals, []string{"snap-a08912c9"}) + c.Assert(req.Form["BlockDeviceMapping.2.Ebs.DeleteOnTermination"], DeepEquals, []string{"true"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.ImageId, Equals, "ami-4fa54026") +} + +func (s *S) TestDescribeImagesExample(c *C) { + testServer.Response(200, nil, DescribeImagesExample) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.Images([]string{"ami-1", "ami-2"}, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeImages"}) + c.Assert(req.Form["ImageId.1"], DeepEquals, []string{"ami-1"}) + c.Assert(req.Form["ImageId.2"], DeepEquals, []string{"ami-2"}) + c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"}) + c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"}) + c.Assert(req.Form["Filter.1.Value.2"], IsNil) + c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"}) + c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"}) + c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "4a4a27a2-2e7c-475d-b35b-ca822EXAMPLE") + c.Assert(resp.Images, HasLen, 1) + + i0 := resp.Images[0] + c.Assert(i0.Id, Equals, "ami-a2469acf") + c.Assert(i0.Type, Equals, "machine") + c.Assert(i0.Name, Equals, "example-marketplace-amzn-ami.1") + c.Assert(i0.Description, Equals, "Amazon Linux AMI i386 EBS") + c.Assert(i0.Location, Equals, "aws-marketplace/example-marketplace-amzn-ami.1") + c.Assert(i0.State, Equals, "available") + c.Assert(i0.Public, Equals, true) + c.Assert(i0.OwnerId, Equals, "123456789999") + c.Assert(i0.OwnerAlias, Equals, "aws-marketplace") + c.Assert(i0.Architecture, Equals, "i386") + c.Assert(i0.KernelId, Equals, "aki-805ea7e9") + c.Assert(i0.RootDeviceType, Equals, "ebs") + c.Assert(i0.RootDeviceName, Equals, "/dev/sda1") + c.Assert(i0.VirtualizationType, Equals, "paravirtual") + c.Assert(i0.Hypervisor, Equals, "xen") + + c.Assert(i0.BlockDevices, HasLen, 1) + c.Assert(i0.BlockDevices[0].DeviceName, Equals, "/dev/sda1") + c.Assert(i0.BlockDevices[0].SnapshotId, Equals, "snap-787e9403") + c.Assert(i0.BlockDevices[0].VolumeSize, Equals, int64(8)) + c.Assert(i0.BlockDevices[0].DeleteOnTermination, Equals, true) + + testServer.Response(200, nil, DescribeImagesExample) + resp2, err := s.ec2.ImagesByOwners([]string{"ami-1", "ami-2"}, []string{"123456789999", "id2"}, filter) + + req2 := testServer.WaitRequest() + c.Assert(req2.Form["Action"], DeepEquals, []string{"DescribeImages"}) + c.Assert(req2.Form["ImageId.1"], DeepEquals, []string{"ami-1"}) + c.Assert(req2.Form["ImageId.2"], DeepEquals, []string{"ami-2"}) + c.Assert(req2.Form["Owner.1"], DeepEquals, []string{"123456789999"}) + c.Assert(req2.Form["Owner.2"], DeepEquals, []string{"id2"}) + c.Assert(req2.Form["Filter.1.Name"], DeepEquals, []string{"key1"}) + c.Assert(req2.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"}) + c.Assert(req2.Form["Filter.1.Value.2"], IsNil) + c.Assert(req2.Form["Filter.2.Name"], DeepEquals, []string{"key2"}) + c.Assert(req2.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"}) + c.Assert(req2.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"}) + + c.Assert(err, IsNil) + c.Assert(resp2.RequestId, Equals, "4a4a27a2-2e7c-475d-b35b-ca822EXAMPLE") + c.Assert(resp2.Images, HasLen, 1) + + i1 := resp2.Images[0] + c.Assert(i1.Id, Equals, "ami-a2469acf") + c.Assert(i1.Type, Equals, "machine") + c.Assert(i1.Name, Equals, "example-marketplace-amzn-ami.1") + c.Assert(i1.Description, Equals, "Amazon Linux AMI i386 EBS") + c.Assert(i1.Location, Equals, "aws-marketplace/example-marketplace-amzn-ami.1") + c.Assert(i1.State, Equals, "available") + c.Assert(i1.Public, Equals, true) + c.Assert(i1.OwnerId, Equals, "123456789999") + c.Assert(i1.OwnerAlias, Equals, "aws-marketplace") + c.Assert(i1.Architecture, Equals, "i386") + c.Assert(i1.KernelId, Equals, "aki-805ea7e9") + c.Assert(i1.RootDeviceType, Equals, "ebs") + c.Assert(i1.RootDeviceName, Equals, "/dev/sda1") + c.Assert(i1.VirtualizationType, Equals, "paravirtual") + c.Assert(i1.Hypervisor, Equals, "xen") + + c.Assert(i1.BlockDevices, HasLen, 1) + c.Assert(i1.BlockDevices[0].DeviceName, Equals, "/dev/sda1") + c.Assert(i1.BlockDevices[0].SnapshotId, Equals, "snap-787e9403") + c.Assert(i1.BlockDevices[0].VolumeSize, Equals, int64(8)) + c.Assert(i1.BlockDevices[0].DeleteOnTermination, Equals, true) +} + +func (s *S) TestImageAttributeExample(c *C) { + testServer.Response(200, nil, ImageAttributeExample) + + resp, err := s.ec2.ImageAttribute("ami-61a54008", "launchPermission") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeImageAttribute"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.ImageId, Equals, "ami-61a54008") + c.Assert(resp.Group, Equals, "all") + c.Assert(resp.UserIds[0], Equals, "495219933132") +} + +func (s *S) TestCreateSnapshotExample(c *C) { + testServer.Response(200, nil, CreateSnapshotExample) + + resp, err := s.ec2.CreateSnapshot("vol-4d826724", "Daily Backup") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CreateSnapshot"}) + c.Assert(req.Form["VolumeId"], DeepEquals, []string{"vol-4d826724"}) + c.Assert(req.Form["Description"], DeepEquals, []string{"Daily Backup"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Snapshot.Id, Equals, "snap-78a54011") + c.Assert(resp.Snapshot.VolumeId, Equals, "vol-4d826724") + c.Assert(resp.Snapshot.Status, Equals, "pending") + c.Assert(resp.Snapshot.StartTime, Equals, "2008-05-07T12:51:50.000Z") + c.Assert(resp.Snapshot.Progress, Equals, "60%") + c.Assert(resp.Snapshot.OwnerId, Equals, "111122223333") + c.Assert(resp.Snapshot.VolumeSize, Equals, "10") + c.Assert(resp.Snapshot.Description, Equals, "Daily Backup") +} + +func (s *S) TestDeleteSnapshotsExample(c *C) { + testServer.Response(200, nil, DeleteSnapshotExample) + + resp, err := s.ec2.DeleteSnapshots([]string{"snap-78a54011"}) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteSnapshot"}) + c.Assert(req.Form["SnapshotId.1"], DeepEquals, []string{"snap-78a54011"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestDescribeSnapshotsExample(c *C) { + testServer.Response(200, nil, DescribeSnapshotsExample) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.Snapshots([]string{"snap-1", "snap-2"}, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSnapshots"}) + c.Assert(req.Form["SnapshotId.1"], DeepEquals, []string{"snap-1"}) + c.Assert(req.Form["SnapshotId.2"], DeepEquals, []string{"snap-2"}) + c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"}) + c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"}) + c.Assert(req.Form["Filter.1.Value.2"], IsNil) + c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"}) + c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"}) + c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Snapshots, HasLen, 1) + + s0 := resp.Snapshots[0] + c.Assert(s0.Id, Equals, "snap-1a2b3c4d") + c.Assert(s0.VolumeId, Equals, "vol-8875daef") + c.Assert(s0.VolumeSize, Equals, "15") + c.Assert(s0.Status, Equals, "pending") + c.Assert(s0.StartTime, Equals, "2010-07-29T04:12:01.000Z") + c.Assert(s0.Progress, Equals, "30%") + c.Assert(s0.OwnerId, Equals, "111122223333") + c.Assert(s0.Description, Equals, "Daily Backup") + + c.Assert(s0.Tags, HasLen, 1) + c.Assert(s0.Tags[0].Key, Equals, "Purpose") + c.Assert(s0.Tags[0].Value, Equals, "demo_db_14_backup") +} + +func (s *S) TestModifyImageAttributeExample(c *C) { + testServer.Response(200, nil, ModifyImageAttributeExample) + + options := ec2.ModifyImageAttribute{ + Description: "Test Description", + } + + resp, err := s.ec2.ModifyImageAttribute("ami-4fa54026", &options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"ModifyImageAttribute"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestModifyImageAttributeExample_complex(c *C) { + testServer.Response(200, nil, ModifyImageAttributeExample) + + options := ec2.ModifyImageAttribute{ + AddUsers: []string{"u1", "u2"}, + RemoveUsers: []string{"u3"}, + AddGroups: []string{"g1", "g3"}, + RemoveGroups: []string{"g2"}, + Description: "Test Description", + } + + resp, err := s.ec2.ModifyImageAttribute("ami-4fa54026", &options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"ModifyImageAttribute"}) + c.Assert(req.Form["LaunchPermission.Add.1.UserId"], DeepEquals, []string{"u1"}) + c.Assert(req.Form["LaunchPermission.Add.2.UserId"], DeepEquals, []string{"u2"}) + c.Assert(req.Form["LaunchPermission.Remove.1.UserId"], DeepEquals, []string{"u3"}) + c.Assert(req.Form["LaunchPermission.Add.1.Group"], DeepEquals, []string{"g1"}) + c.Assert(req.Form["LaunchPermission.Add.2.Group"], DeepEquals, []string{"g3"}) + c.Assert(req.Form["LaunchPermission.Remove.1.Group"], DeepEquals, []string{"g2"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestCopyImageExample(c *C) { + testServer.Response(200, nil, CopyImageExample) + + options := ec2.CopyImage{ + SourceRegion: "us-west-2", + SourceImageId: "ami-1a2b3c4d", + Description: "Test Description", + } + + resp, err := s.ec2.CopyImage(&options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CopyImage"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "60bc441d-fa2c-494d-b155-5d6a3EXAMPLE") +} + +func (s *S) TestCreateKeyPairExample(c *C) { + testServer.Response(200, nil, CreateKeyPairExample) + + resp, err := s.ec2.CreateKeyPair("foo") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CreateKeyPair"}) + c.Assert(req.Form["KeyName"], DeepEquals, []string{"foo"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.KeyName, Equals, "foo") + c.Assert(resp.KeyFingerprint, Equals, "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00") +} + +func (s *S) TestDeleteKeyPairExample(c *C) { + testServer.Response(200, nil, DeleteKeyPairExample) + + resp, err := s.ec2.DeleteKeyPair("foo") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteKeyPair"}) + c.Assert(req.Form["KeyName"], DeepEquals, []string{"foo"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestCreateSecurityGroupExample(c *C) { + testServer.Response(200, nil, CreateSecurityGroupExample) + + resp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: "websrv", Description: "Web Servers"}) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"CreateSecurityGroup"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(req.Form["GroupDescription"], DeepEquals, []string{"Web Servers"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Name, Equals, "websrv") + c.Assert(resp.Id, Equals, "sg-67ad940e") +} + +func (s *S) TestDescribeSecurityGroupsExample(c *C) { + testServer.Response(200, nil, DescribeSecurityGroupsExample) + + resp, err := s.ec2.SecurityGroups([]ec2.SecurityGroup{{Name: "WebServers"}, {Name: "RangedPortsBySource"}}, nil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSecurityGroups"}) + c.Assert(req.Form["GroupName.1"], DeepEquals, []string{"WebServers"}) + c.Assert(req.Form["GroupName.2"], DeepEquals, []string{"RangedPortsBySource"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.Groups, HasLen, 2) + + g0 := resp.Groups[0] + c.Assert(g0.OwnerId, Equals, "999988887777") + c.Assert(g0.Name, Equals, "WebServers") + c.Assert(g0.Id, Equals, "sg-67ad940e") + c.Assert(g0.Description, Equals, "Web Servers") + c.Assert(g0.IPPerms, HasLen, 1) + + g0ipp := g0.IPPerms[0] + c.Assert(g0ipp.Protocol, Equals, "tcp") + c.Assert(g0ipp.FromPort, Equals, 80) + c.Assert(g0ipp.ToPort, Equals, 80) + c.Assert(g0ipp.SourceIPs, DeepEquals, []string{"0.0.0.0/0"}) + + g1 := resp.Groups[1] + c.Assert(g1.OwnerId, Equals, "999988887777") + c.Assert(g1.Name, Equals, "RangedPortsBySource") + c.Assert(g1.Id, Equals, "sg-76abc467") + c.Assert(g1.Description, Equals, "Group A") + c.Assert(g1.IPPerms, HasLen, 1) + + g1ipp := g1.IPPerms[0] + c.Assert(g1ipp.Protocol, Equals, "tcp") + c.Assert(g1ipp.FromPort, Equals, 6000) + c.Assert(g1ipp.ToPort, Equals, 7000) + c.Assert(g1ipp.SourceIPs, IsNil) +} + +func (s *S) TestDescribeSecurityGroupsExampleWithFilter(c *C) { + testServer.Response(200, nil, DescribeSecurityGroupsExample) + + filter := ec2.NewFilter() + filter.Add("ip-permission.protocol", "tcp") + filter.Add("ip-permission.from-port", "22") + filter.Add("ip-permission.to-port", "22") + filter.Add("ip-permission.group-name", "app_server_group", "database_group") + + _, err := s.ec2.SecurityGroups(nil, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSecurityGroups"}) + c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"ip-permission.from-port"}) + c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"22"}) + c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"ip-permission.group-name"}) + c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"app_server_group"}) + c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"database_group"}) + c.Assert(req.Form["Filter.3.Name"], DeepEquals, []string{"ip-permission.protocol"}) + c.Assert(req.Form["Filter.3.Value.1"], DeepEquals, []string{"tcp"}) + c.Assert(req.Form["Filter.4.Name"], DeepEquals, []string{"ip-permission.to-port"}) + c.Assert(req.Form["Filter.4.Value.1"], DeepEquals, []string{"22"}) + + c.Assert(err, IsNil) +} + +func (s *S) TestDescribeSecurityGroupsDumpWithGroup(c *C) { + testServer.Response(200, nil, DescribeSecurityGroupsDump) + + resp, err := s.ec2.SecurityGroups(nil, nil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeSecurityGroups"}) + c.Assert(err, IsNil) + c.Check(resp.Groups, HasLen, 1) + c.Check(resp.Groups[0].IPPerms, HasLen, 2) + + ipp0 := resp.Groups[0].IPPerms[0] + c.Assert(ipp0.SourceIPs, IsNil) + c.Check(ipp0.Protocol, Equals, "icmp") + c.Assert(ipp0.SourceGroups, HasLen, 1) + c.Check(ipp0.SourceGroups[0].OwnerId, Equals, "12345") + c.Check(ipp0.SourceGroups[0].Name, Equals, "default") + c.Check(ipp0.SourceGroups[0].Id, Equals, "sg-67ad940e") + + ipp1 := resp.Groups[0].IPPerms[1] + c.Check(ipp1.Protocol, Equals, "tcp") + c.Assert(ipp0.SourceIPs, IsNil) + c.Assert(ipp0.SourceGroups, HasLen, 1) + c.Check(ipp1.SourceGroups[0].Id, Equals, "sg-76abc467") + c.Check(ipp1.SourceGroups[0].OwnerId, Equals, "12345") + c.Check(ipp1.SourceGroups[0].Name, Equals, "other") +} + +func (s *S) TestDeleteSecurityGroupExample(c *C) { + testServer.Response(200, nil, DeleteSecurityGroupExample) + + resp, err := s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: "websrv"}) + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteSecurityGroup"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(req.Form["GroupId"], IsNil) + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestDeleteSecurityGroupExampleWithId(c *C) { + testServer.Response(200, nil, DeleteSecurityGroupExample) + + // ignore return and error - we're only want to check the parameter handling. + s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Id: "sg-67ad940e", Name: "ignored"}) + req := testServer.WaitRequest() + + c.Assert(req.Form["GroupName"], IsNil) + c.Assert(req.Form["GroupId"], DeepEquals, []string{"sg-67ad940e"}) +} + +func (s *S) TestAuthorizeSecurityGroupExample1(c *C) { + testServer.Response(200, nil, AuthorizeSecurityGroupIngressExample) + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 80, + ToPort: 80, + SourceIPs: []string{"205.192.0.0/16", "205.159.0.0/16"}, + }} + resp, err := s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: "websrv"}, perms) + + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"AuthorizeSecurityGroupIngress"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(req.Form["IpPermissions.1.IpProtocol"], DeepEquals, []string{"tcp"}) + c.Assert(req.Form["IpPermissions.1.FromPort"], DeepEquals, []string{"80"}) + c.Assert(req.Form["IpPermissions.1.ToPort"], DeepEquals, []string{"80"}) + c.Assert(req.Form["IpPermissions.1.IpRanges.1.CidrIp"], DeepEquals, []string{"205.192.0.0/16"}) + c.Assert(req.Form["IpPermissions.1.IpRanges.2.CidrIp"], DeepEquals, []string{"205.159.0.0/16"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestAuthorizeSecurityGroupEgress(c *C) { + testServer.Response(200, nil, AuthorizeSecurityGroupEgressExample) + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 80, + ToPort: 80, + SourceIPs: []string{"205.192.0.0/16", "205.159.0.0/16"}, + }} + resp, err := s.ec2.AuthorizeSecurityGroupEgress(ec2.SecurityGroup{Name: "websrv"}, perms) + + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"AuthorizeSecurityGroupEgress"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(req.Form["IpPermissions.1.IpProtocol"], DeepEquals, []string{"tcp"}) + c.Assert(req.Form["IpPermissions.1.FromPort"], DeepEquals, []string{"80"}) + c.Assert(req.Form["IpPermissions.1.ToPort"], DeepEquals, []string{"80"}) + c.Assert(req.Form["IpPermissions.1.IpRanges.1.CidrIp"], DeepEquals, []string{"205.192.0.0/16"}) + c.Assert(req.Form["IpPermissions.1.IpRanges.2.CidrIp"], DeepEquals, []string{"205.159.0.0/16"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestAuthorizeSecurityGroupExample1WithId(c *C) { + testServer.Response(200, nil, AuthorizeSecurityGroupIngressExample) + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 80, + ToPort: 80, + SourceIPs: []string{"205.192.0.0/16", "205.159.0.0/16"}, + }} + // ignore return and error - we're only want to check the parameter handling. + s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Id: "sg-67ad940e", Name: "ignored"}, perms) + + req := testServer.WaitRequest() + + c.Assert(req.Form["GroupName"], IsNil) + c.Assert(req.Form["GroupId"], DeepEquals, []string{"sg-67ad940e"}) +} + +func (s *S) TestAuthorizeSecurityGroupExample2(c *C) { + testServer.Response(200, nil, AuthorizeSecurityGroupIngressExample) + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 80, + ToPort: 81, + SourceGroups: []ec2.UserSecurityGroup{ + {OwnerId: "999988887777", Name: "OtherAccountGroup"}, + {Id: "sg-67ad940e"}, + }, + }} + resp, err := s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: "websrv"}, perms) + + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"AuthorizeSecurityGroupIngress"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(req.Form["IpPermissions.1.IpProtocol"], DeepEquals, []string{"tcp"}) + c.Assert(req.Form["IpPermissions.1.FromPort"], DeepEquals, []string{"80"}) + c.Assert(req.Form["IpPermissions.1.ToPort"], DeepEquals, []string{"81"}) + c.Assert(req.Form["IpPermissions.1.Groups.1.UserId"], DeepEquals, []string{"999988887777"}) + c.Assert(req.Form["IpPermissions.1.Groups.1.GroupName"], DeepEquals, []string{"OtherAccountGroup"}) + c.Assert(req.Form["IpPermissions.1.Groups.2.UserId"], IsNil) + c.Assert(req.Form["IpPermissions.1.Groups.2.GroupName"], IsNil) + c.Assert(req.Form["IpPermissions.1.Groups.2.GroupId"], DeepEquals, []string{"sg-67ad940e"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestRevokeSecurityGroupExample(c *C) { + // RevokeSecurityGroup is implemented by the same code as AuthorizeSecurityGroup + // so there's no need to duplicate all the tests. + testServer.Response(200, nil, RevokeSecurityGroupIngressExample) + + resp, err := s.ec2.RevokeSecurityGroup(ec2.SecurityGroup{Name: "websrv"}, nil) + + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"RevokeSecurityGroupIngress"}) + c.Assert(req.Form["GroupName"], DeepEquals, []string{"websrv"}) + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestCreateTags(c *C) { + testServer.Response(200, nil, CreateTagsExample) + + resp, err := s.ec2.CreateTags([]string{"ami-1a2b3c4d", "i-7f4d3a2b"}, []ec2.Tag{{"webserver", ""}, {"stack", "Production"}}) + + req := testServer.WaitRequest() + c.Assert(req.Form["ResourceId.1"], DeepEquals, []string{"ami-1a2b3c4d"}) + c.Assert(req.Form["ResourceId.2"], DeepEquals, []string{"i-7f4d3a2b"}) + c.Assert(req.Form["Tag.1.Key"], DeepEquals, []string{"webserver"}) + c.Assert(req.Form["Tag.1.Value"], DeepEquals, []string{""}) + c.Assert(req.Form["Tag.2.Key"], DeepEquals, []string{"stack"}) + c.Assert(req.Form["Tag.2.Value"], DeepEquals, []string{"Production"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestStartInstances(c *C) { + testServer.Response(200, nil, StartInstancesExample) + + resp, err := s.ec2.StartInstances("i-10a64379") + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"StartInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-10a64379"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + + s0 := resp.StateChanges[0] + c.Assert(s0.InstanceId, Equals, "i-10a64379") + c.Assert(s0.CurrentState.Code, Equals, 0) + c.Assert(s0.CurrentState.Name, Equals, "pending") + c.Assert(s0.PreviousState.Code, Equals, 80) + c.Assert(s0.PreviousState.Name, Equals, "stopped") +} + +func (s *S) TestStopInstances(c *C) { + testServer.Response(200, nil, StopInstancesExample) + + resp, err := s.ec2.StopInstances("i-10a64379") + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"StopInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-10a64379"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + + s0 := resp.StateChanges[0] + c.Assert(s0.InstanceId, Equals, "i-10a64379") + c.Assert(s0.CurrentState.Code, Equals, 64) + c.Assert(s0.CurrentState.Name, Equals, "stopping") + c.Assert(s0.PreviousState.Code, Equals, 16) + c.Assert(s0.PreviousState.Name, Equals, "running") +} + +func (s *S) TestRebootInstances(c *C) { + testServer.Response(200, nil, RebootInstancesExample) + + resp, err := s.ec2.RebootInstances("i-10a64379") + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"RebootInstances"}) + c.Assert(req.Form["InstanceId.1"], DeepEquals, []string{"i-10a64379"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestSignatureWithEndpointPath(c *C) { + ec2.FakeTime(true) + defer ec2.FakeTime(false) + + testServer.Response(200, nil, RebootInstancesExample) + + // https://bugs.launchpad.net/goamz/+bug/1022749 + ec2 := ec2.NewWithClient(s.ec2.Auth, aws.Region{EC2Endpoint: testServer.URL + "/services/Cloud"}, testutil.DefaultClient) + + _, err := ec2.RebootInstances("i-10a64379") + c.Assert(err, IsNil) + + req := testServer.WaitRequest() + c.Assert(req.Form["Signature"], DeepEquals, []string{"QmvgkYGn19WirCuCz/jRp3RmRgFwWR5WRkKZ5AZnyXQ="}) +} + +func (s *S) TestAllocateAddressExample(c *C) { + testServer.Response(200, nil, AllocateAddressExample) + + options := &ec2.AllocateAddress{ + Domain: "vpc", + } + + resp, err := s.ec2.AllocateAddress(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"AllocateAddress"}) + c.Assert(req.Form["Domain"], DeepEquals, []string{"vpc"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.PublicIp, Equals, "198.51.100.1") + c.Assert(resp.Domain, Equals, "vpc") + c.Assert(resp.AllocationId, Equals, "eipalloc-5723d13e") +} + +func (s *S) TestReleaseAddressExample(c *C) { + testServer.Response(200, nil, ReleaseAddressExample) + + resp, err := s.ec2.ReleaseAddress("eipalloc-5723d13e") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"ReleaseAddress"}) + c.Assert(req.Form["AllocationId"], DeepEquals, []string{"eipalloc-5723d13e"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestAssociateAddressExample(c *C) { + testServer.Response(200, nil, AssociateAddressExample) + + options := &ec2.AssociateAddress{ + InstanceId: "i-4fd2431a", + AllocationId: "eipalloc-5723d13e", + AllowReassociation: true, + } + + resp, err := s.ec2.AssociateAddress(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"AssociateAddress"}) + c.Assert(req.Form["InstanceId"], DeepEquals, []string{"i-4fd2431a"}) + c.Assert(req.Form["AllocationId"], DeepEquals, []string{"eipalloc-5723d13e"}) + c.Assert(req.Form["AllowReassociation"], DeepEquals, []string{"true"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") + c.Assert(resp.AssociationId, Equals, "eipassoc-fc5ca095") +} + +func (s *S) TestDisassociateAddressExample(c *C) { + testServer.Response(200, nil, DisassociateAddressExample) + + resp, err := s.ec2.DisassociateAddress("eipassoc-aa7486c3") + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DisassociateAddress"}) + c.Assert(req.Form["AssociationId"], DeepEquals, []string{"eipassoc-aa7486c3"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestModifyInstance(c *C) { + testServer.Response(200, nil, ModifyInstanceExample) + + options := ec2.ModifyInstance{ + InstanceType: "m1.small", + DisableAPITermination: true, + EbsOptimized: true, + SecurityGroups: []ec2.SecurityGroup{{Id: "g1"}, {Id: "g2"}}, + ShutdownBehavior: "terminate", + KernelId: "kernel-id", + RamdiskId: "ramdisk-id", + SourceDestCheck: true, + SriovNetSupport: true, + UserData: []byte("1234"), + BlockDevices: []ec2.BlockDeviceMapping{ + {DeviceName: "/dev/sda1", SnapshotId: "snap-a08912c9", DeleteOnTermination: true}, + }, + } + + resp, err := s.ec2.ModifyInstance("i-2ba64342", &options) + req := testServer.WaitRequest() + + c.Assert(req.Form["Action"], DeepEquals, []string{"ModifyInstanceAttribute"}) + c.Assert(req.Form["InstanceId"], DeepEquals, []string{"i-2ba64342"}) + c.Assert(req.Form["InstanceType.Value"], DeepEquals, []string{"m1.small"}) + c.Assert(req.Form["BlockDeviceMapping.1.DeviceName"], DeepEquals, []string{"/dev/sda1"}) + c.Assert(req.Form["BlockDeviceMapping.1.Ebs.SnapshotId"], DeepEquals, []string{"snap-a08912c9"}) + c.Assert(req.Form["BlockDeviceMapping.1.Ebs.DeleteOnTermination"], DeepEquals, []string{"true"}) + c.Assert(req.Form["DisableApiTermination.Value"], DeepEquals, []string{"true"}) + c.Assert(req.Form["EbsOptimized"], DeepEquals, []string{"true"}) + c.Assert(req.Form["GroupId.1"], DeepEquals, []string{"g1"}) + c.Assert(req.Form["GroupId.2"], DeepEquals, []string{"g2"}) + c.Assert(req.Form["InstanceInitiatedShutdownBehavior.Value"], DeepEquals, []string{"terminate"}) + c.Assert(req.Form["Kernel.Value"], DeepEquals, []string{"kernel-id"}) + c.Assert(req.Form["Ramdisk.Value"], DeepEquals, []string{"ramdisk-id"}) + c.Assert(req.Form["SourceDestCheck.Value"], DeepEquals, []string{"true"}) + c.Assert(req.Form["SriovNetSupport.Value"], DeepEquals, []string{"simple"}) + c.Assert(req.Form["UserData"], DeepEquals, []string{"MTIzNA=="}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} + +func (s *S) TestCreateVpc(c *C) { + testServer.Response(200, nil, CreateVpcExample) + + options := &ec2.CreateVpc{ + CidrBlock: "foo", + } + + resp, err := s.ec2.CreateVpc(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["CidrBlock"], DeepEquals, []string{"foo"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE") + c.Assert(resp.VPC.VpcId, Equals, "vpc-1a2b3c4d") + c.Assert(resp.VPC.State, Equals, "pending") + c.Assert(resp.VPC.CidrBlock, Equals, "10.0.0.0/16") + c.Assert(resp.VPC.DHCPOptionsID, Equals, "dopt-1a2b3c4d2") + c.Assert(resp.VPC.InstanceTenancy, Equals, "default") +} + +func (s *S) TestDescribeVpcs(c *C) { + testServer.Response(200, nil, DescribeVpcsExample) + + filter := ec2.NewFilter() + filter.Add("key1", "value1") + filter.Add("key2", "value2", "value3") + + resp, err := s.ec2.DescribeVpcs([]string{"id1", "id2"}, filter) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeVpcs"}) + c.Assert(req.Form["VpcId.1"], DeepEquals, []string{"id1"}) + c.Assert(req.Form["VpcId.2"], DeepEquals, []string{"id2"}) + c.Assert(req.Form["Filter.1.Name"], DeepEquals, []string{"key1"}) + c.Assert(req.Form["Filter.1.Value.1"], DeepEquals, []string{"value1"}) + c.Assert(req.Form["Filter.1.Value.2"], IsNil) + c.Assert(req.Form["Filter.2.Name"], DeepEquals, []string{"key2"}) + c.Assert(req.Form["Filter.2.Value.1"], DeepEquals, []string{"value2"}) + c.Assert(req.Form["Filter.2.Value.2"], DeepEquals, []string{"value3"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE") + c.Assert(resp.VPCs, HasLen, 1) +} + +func (s *S) TestCreateSubnet(c *C) { + testServer.Response(200, nil, CreateSubnetExample) + + options := &ec2.CreateSubnet{ + AvailabilityZone: "baz", + CidrBlock: "foo", + VpcId: "bar", + } + + resp, err := s.ec2.CreateSubnet(options) + + req := testServer.WaitRequest() + c.Assert(req.Form["VpcId"], DeepEquals, []string{"bar"}) + c.Assert(req.Form["CidrBlock"], DeepEquals, []string{"foo"}) + c.Assert(req.Form["AvailabilityZone"], DeepEquals, []string{"baz"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "7a62c49f-347e-4fc4-9331-6e8eEXAMPLE") + c.Assert(resp.Subnet.SubnetId, Equals, "subnet-9d4a7b6c") + c.Assert(resp.Subnet.State, Equals, "pending") + c.Assert(resp.Subnet.VpcId, Equals, "vpc-1a2b3c4d") + c.Assert(resp.Subnet.CidrBlock, Equals, "10.0.1.0/24") + c.Assert(resp.Subnet.AvailableIpAddressCount, Equals, 251) +} + +func (s *S) TestResetImageAttribute(c *C) { + testServer.Response(200, nil, ResetImageAttributeExample) + + options := ec2.ResetImageAttribute{Attribute: "launchPermission"} + resp, err := s.ec2.ResetImageAttribute("i-2ba64342", &options) + + req := testServer.WaitRequest() + c.Assert(req.Form["Action"], DeepEquals, []string{"ResetImageAttribute"}) + + c.Assert(err, IsNil) + c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE") +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2i_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2i_test.go new file mode 100644 index 00000000000..3773041bf52 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2i_test.go @@ -0,0 +1,203 @@ +package ec2_test + +import ( + "crypto/rand" + "fmt" + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/goamz/testutil" + . "github.com/motain/gocheck" +) + +// AmazonServer represents an Amazon EC2 server. +type AmazonServer struct { + auth aws.Auth +} + +func (s *AmazonServer) SetUp(c *C) { + auth, err := aws.EnvAuth() + if err != nil { + c.Fatal(err.Error()) + } + s.auth = auth +} + +// Suite cost per run: 0.02 USD +var _ = Suite(&AmazonClientSuite{}) + +// AmazonClientSuite tests the client against a live EC2 server. +type AmazonClientSuite struct { + srv AmazonServer + ClientTests +} + +func (s *AmazonClientSuite) SetUpSuite(c *C) { + if !testutil.Amazon { + c.Skip("AmazonClientSuite tests not enabled") + } + s.srv.SetUp(c) + s.ec2 = ec2.NewWithClient(s.srv.auth, aws.USEast, testutil.DefaultClient) +} + +// ClientTests defines integration tests designed to test the client. +// It is not used as a test suite in itself, but embedded within +// another type. +type ClientTests struct { + ec2 *ec2.EC2 +} + +var imageId = "ami-ccf405a5" // Ubuntu Maverick, i386, EBS store + +// Cost: 0.00 USD +func (s *ClientTests) TestRunInstancesError(c *C) { + options := ec2.RunInstances{ + ImageId: "ami-a6f504cf", // Ubuntu Maverick, i386, instance store + InstanceType: "t1.micro", // Doesn't work with micro, results in 400. + } + + resp, err := s.ec2.RunInstances(&options) + + c.Assert(resp, IsNil) + c.Assert(err, ErrorMatches, "AMI.*root device.*not supported.*") + + ec2err, ok := err.(*ec2.Error) + c.Assert(ok, Equals, true) + c.Assert(ec2err.StatusCode, Equals, 400) + c.Assert(ec2err.Code, Equals, "UnsupportedOperation") + c.Assert(ec2err.Message, Matches, "AMI.*root device.*not supported.*") + c.Assert(ec2err.RequestId, Matches, ".+") +} + +// Cost: 0.02 USD +func (s *ClientTests) TestRunAndTerminate(c *C) { + options := ec2.RunInstances{ + ImageId: imageId, + InstanceType: "t1.micro", + } + resp1, err := s.ec2.RunInstances(&options) + c.Assert(err, IsNil) + c.Check(resp1.ReservationId, Matches, "r-[0-9a-f]*") + c.Check(resp1.OwnerId, Matches, "[0-9]+") + c.Check(resp1.Instances, HasLen, 1) + c.Check(resp1.Instances[0].InstanceType, Equals, "t1.micro") + + instId := resp1.Instances[0].InstanceId + + resp2, err := s.ec2.Instances([]string{instId}, nil) + c.Assert(err, IsNil) + if c.Check(resp2.Reservations, HasLen, 1) && c.Check(len(resp2.Reservations[0].Instances), Equals, 1) { + inst := resp2.Reservations[0].Instances[0] + c.Check(inst.InstanceId, Equals, instId) + } + + resp3, err := s.ec2.TerminateInstances([]string{instId}) + c.Assert(err, IsNil) + c.Check(resp3.StateChanges, HasLen, 1) + c.Check(resp3.StateChanges[0].InstanceId, Equals, instId) + c.Check(resp3.StateChanges[0].CurrentState.Name, Equals, "shutting-down") + c.Check(resp3.StateChanges[0].CurrentState.Code, Equals, 32) +} + +// Cost: 0.00 USD +func (s *ClientTests) TestSecurityGroups(c *C) { + name := "goamz-test" + descr := "goamz security group for tests" + + // Clean it up, if a previous test left it around and avoid leaving it around. + s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + defer s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + + resp1, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr}) + c.Assert(err, IsNil) + c.Assert(resp1.RequestId, Matches, ".+") + c.Assert(resp1.Name, Equals, name) + c.Assert(resp1.Id, Matches, ".+") + + resp1, err = s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr}) + ec2err, _ := err.(*ec2.Error) + c.Assert(resp1, IsNil) + c.Assert(ec2err, NotNil) + c.Assert(ec2err.Code, Equals, "InvalidGroup.Duplicate") + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 0, + ToPort: 1024, + SourceIPs: []string{"127.0.0.1/24"}, + }} + + resp2, err := s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms) + c.Assert(err, IsNil) + c.Assert(resp2.RequestId, Matches, ".+") + + resp3, err := s.ec2.SecurityGroups(ec2.SecurityGroupNames(name), nil) + c.Assert(err, IsNil) + c.Assert(resp3.RequestId, Matches, ".+") + c.Assert(resp3.Groups, HasLen, 1) + + g0 := resp3.Groups[0] + c.Assert(g0.Name, Equals, name) + c.Assert(g0.Description, Equals, descr) + c.Assert(g0.IPPerms, HasLen, 1) + c.Assert(g0.IPPerms[0].Protocol, Equals, "tcp") + c.Assert(g0.IPPerms[0].FromPort, Equals, 0) + c.Assert(g0.IPPerms[0].ToPort, Equals, 1024) + c.Assert(g0.IPPerms[0].SourceIPs, DeepEquals, []string{"127.0.0.1/24"}) + + resp2, err = s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + c.Assert(err, IsNil) + c.Assert(resp2.RequestId, Matches, ".+") +} + +var sessionId = func() string { + buf := make([]byte, 8) + // if we have no randomness, we'll just make do, so ignore the error. + rand.Read(buf) + return fmt.Sprintf("%x", buf) +}() + +// sessionName reutrns a name that is probably +// unique to this test session. +func sessionName(prefix string) string { + return prefix + "-" + sessionId +} + +var allRegions = []aws.Region{ + aws.USEast, + aws.USWest, + aws.EUWest, + aws.APSoutheast, + aws.APNortheast, +} + +// Communicate with all EC2 endpoints to see if they are alive. +func (s *ClientTests) TestRegions(c *C) { + name := sessionName("goamz-region-test") + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 80, + ToPort: 80, + SourceIPs: []string{"127.0.0.1/32"}, + }} + errs := make(chan error, len(allRegions)) + for _, region := range allRegions { + go func(r aws.Region) { + e := ec2.NewWithClient(s.ec2.Auth, r, testutil.DefaultClient) + _, err := e.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms) + errs <- err + }(region) + } + for _ = range allRegions { + err := <-errs + if err != nil { + ec2_err, ok := err.(*ec2.Error) + if ok { + c.Check(ec2_err.Code, Matches, "InvalidGroup.NotFound") + } else { + c.Errorf("Non-EC2 error: %s", err) + } + } else { + c.Errorf("Test should have errored but it seems to have succeeded") + } + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2t_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2t_test.go new file mode 100644 index 00000000000..fe50356f908 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2t_test.go @@ -0,0 +1,580 @@ +package ec2_test + +import ( + "fmt" + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + "github.com/mitchellh/goamz/ec2/ec2test" + "github.com/mitchellh/goamz/testutil" + . "github.com/motain/gocheck" + "regexp" + "sort" +) + +// LocalServer represents a local ec2test fake server. +type LocalServer struct { + auth aws.Auth + region aws.Region + srv *ec2test.Server +} + +func (s *LocalServer) SetUp(c *C) { + srv, err := ec2test.NewServer() + c.Assert(err, IsNil) + c.Assert(srv, NotNil) + + s.srv = srv + s.region = aws.Region{EC2Endpoint: srv.URL()} +} + +// LocalServerSuite defines tests that will run +// against the local ec2test server. It includes +// selected tests from ClientTests; +// when the ec2test functionality is sufficient, it should +// include all of them, and ClientTests can be simply embedded. +type LocalServerSuite struct { + srv LocalServer + ServerTests + clientTests ClientTests +} + +var _ = Suite(&LocalServerSuite{}) + +func (s *LocalServerSuite) SetUpSuite(c *C) { + s.srv.SetUp(c) + s.ServerTests.ec2 = ec2.NewWithClient(s.srv.auth, s.srv.region, testutil.DefaultClient) + s.clientTests.ec2 = ec2.NewWithClient(s.srv.auth, s.srv.region, testutil.DefaultClient) +} + +func (s *LocalServerSuite) TestRunAndTerminate(c *C) { + s.clientTests.TestRunAndTerminate(c) +} + +func (s *LocalServerSuite) TestSecurityGroups(c *C) { + s.clientTests.TestSecurityGroups(c) +} + +// TestUserData is not defined on ServerTests because it +// requires the ec2test server to function. +func (s *LocalServerSuite) TestUserData(c *C) { + data := make([]byte, 256) + for i := range data { + data[i] = byte(i) + } + inst, err := s.ec2.RunInstances(&ec2.RunInstances{ + ImageId: imageId, + InstanceType: "t1.micro", + UserData: data, + }) + c.Assert(err, IsNil) + c.Assert(inst, NotNil) + c.Assert(inst.Instances[0].DNSName, Equals, inst.Instances[0].InstanceId+".example.com") + + id := inst.Instances[0].InstanceId + + defer s.ec2.TerminateInstances([]string{id}) + + tinst := s.srv.srv.Instance(id) + c.Assert(tinst, NotNil) + c.Assert(tinst.UserData, DeepEquals, data) +} + +// AmazonServerSuite runs the ec2test server tests against a live EC2 server. +// It will only be activated if the -all flag is specified. +type AmazonServerSuite struct { + srv AmazonServer + ServerTests +} + +var _ = Suite(&AmazonServerSuite{}) + +func (s *AmazonServerSuite) SetUpSuite(c *C) { + if !testutil.Amazon { + c.Skip("AmazonServerSuite tests not enabled") + } + s.srv.SetUp(c) + s.ServerTests.ec2 = ec2.NewWithClient(s.srv.auth, aws.USEast, testutil.DefaultClient) +} + +// ServerTests defines a set of tests designed to test +// the ec2test local fake ec2 server. +// It is not used as a test suite in itself, but embedded within +// another type. +type ServerTests struct { + ec2 *ec2.EC2 +} + +func terminateInstances(c *C, e *ec2.EC2, insts []*ec2.Instance) { + var ids []string + for _, inst := range insts { + if inst != nil { + ids = append(ids, inst.InstanceId) + } + } + _, err := e.TerminateInstances(ids) + c.Check(err, IsNil, Commentf("%d INSTANCES LEFT RUNNING!!!", len(ids))) +} + +func (s *ServerTests) makeTestGroup(c *C, name, descr string) ec2.SecurityGroup { + // Clean it up if a previous test left it around. + _, err := s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + if err != nil && err.(*ec2.Error).Code != "InvalidGroup.NotFound" { + c.Fatalf("delete security group: %v", err) + } + + resp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr}) + c.Assert(err, IsNil) + c.Assert(resp.Name, Equals, name) + return resp.SecurityGroup +} + +func (s *ServerTests) TestIPPerms(c *C) { + g0 := s.makeTestGroup(c, "goamz-test0", "ec2test group 0") + defer s.ec2.DeleteSecurityGroup(g0) + + g1 := s.makeTestGroup(c, "goamz-test1", "ec2test group 1") + defer s.ec2.DeleteSecurityGroup(g1) + + resp, err := s.ec2.SecurityGroups([]ec2.SecurityGroup{g0, g1}, nil) + c.Assert(err, IsNil) + c.Assert(resp.Groups, HasLen, 2) + c.Assert(resp.Groups[0].IPPerms, HasLen, 0) + c.Assert(resp.Groups[1].IPPerms, HasLen, 0) + + ownerId := resp.Groups[0].OwnerId + + // test some invalid parameters + // TODO more + _, err = s.ec2.AuthorizeSecurityGroup(g0, []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 0, + ToPort: 1024, + SourceIPs: []string{"z127.0.0.1/24"}, + }}) + c.Assert(err, NotNil) + c.Check(err.(*ec2.Error).Code, Equals, "InvalidPermission.Malformed") + + // Check that AuthorizeSecurityGroup adds the correct authorizations. + _, err = s.ec2.AuthorizeSecurityGroup(g0, []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 2000, + ToPort: 2001, + SourceIPs: []string{"127.0.0.0/24"}, + SourceGroups: []ec2.UserSecurityGroup{{ + Name: g1.Name, + }, { + Id: g0.Id, + }}, + }, { + Protocol: "tcp", + FromPort: 2000, + ToPort: 2001, + SourceIPs: []string{"200.1.1.34/32"}, + }}) + c.Assert(err, IsNil) + + resp, err = s.ec2.SecurityGroups([]ec2.SecurityGroup{g0}, nil) + c.Assert(err, IsNil) + c.Assert(resp.Groups, HasLen, 1) + c.Assert(resp.Groups[0].IPPerms, HasLen, 1) + + perm := resp.Groups[0].IPPerms[0] + srcg := perm.SourceGroups + c.Assert(srcg, HasLen, 2) + + // Normalize so we don't care about returned order. + if srcg[0].Name == g1.Name { + srcg[0], srcg[1] = srcg[1], srcg[0] + } + c.Check(srcg[0].Name, Equals, g0.Name) + c.Check(srcg[0].Id, Equals, g0.Id) + c.Check(srcg[0].OwnerId, Equals, ownerId) + c.Check(srcg[1].Name, Equals, g1.Name) + c.Check(srcg[1].Id, Equals, g1.Id) + c.Check(srcg[1].OwnerId, Equals, ownerId) + + sort.Strings(perm.SourceIPs) + c.Check(perm.SourceIPs, DeepEquals, []string{"127.0.0.0/24", "200.1.1.34/32"}) + + // Check that we can't delete g1 (because g0 is using it) + _, err = s.ec2.DeleteSecurityGroup(g1) + c.Assert(err, NotNil) + c.Check(err.(*ec2.Error).Code, Equals, "InvalidGroup.InUse") + + _, err = s.ec2.RevokeSecurityGroup(g0, []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 2000, + ToPort: 2001, + SourceGroups: []ec2.UserSecurityGroup{{Id: g1.Id}}, + }, { + Protocol: "tcp", + FromPort: 2000, + ToPort: 2001, + SourceIPs: []string{"200.1.1.34/32"}, + }}) + c.Assert(err, IsNil) + + resp, err = s.ec2.SecurityGroups([]ec2.SecurityGroup{g0}, nil) + c.Assert(err, IsNil) + c.Assert(resp.Groups, HasLen, 1) + c.Assert(resp.Groups[0].IPPerms, HasLen, 1) + + perm = resp.Groups[0].IPPerms[0] + srcg = perm.SourceGroups + c.Assert(srcg, HasLen, 1) + c.Check(srcg[0].Name, Equals, g0.Name) + c.Check(srcg[0].Id, Equals, g0.Id) + c.Check(srcg[0].OwnerId, Equals, ownerId) + + c.Check(perm.SourceIPs, DeepEquals, []string{"127.0.0.0/24"}) + + // We should be able to delete g1 now because we've removed its only use. + _, err = s.ec2.DeleteSecurityGroup(g1) + c.Assert(err, IsNil) + + _, err = s.ec2.DeleteSecurityGroup(g0) + c.Assert(err, IsNil) + + f := ec2.NewFilter() + f.Add("group-id", g0.Id, g1.Id) + resp, err = s.ec2.SecurityGroups(nil, f) + c.Assert(err, IsNil) + c.Assert(resp.Groups, HasLen, 0) +} + +func (s *ServerTests) TestDuplicateIPPerm(c *C) { + name := "goamz-test" + descr := "goamz security group for tests" + + // Clean it up, if a previous test left it around and avoid leaving it around. + s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + defer s.ec2.DeleteSecurityGroup(ec2.SecurityGroup{Name: name}) + + resp1, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: name, Description: descr}) + c.Assert(err, IsNil) + c.Assert(resp1.Name, Equals, name) + + perms := []ec2.IPPerm{{ + Protocol: "tcp", + FromPort: 200, + ToPort: 1024, + SourceIPs: []string{"127.0.0.1/24"}, + }, { + Protocol: "tcp", + FromPort: 0, + ToPort: 100, + SourceIPs: []string{"127.0.0.1/24"}, + }} + + _, err = s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms[0:1]) + c.Assert(err, IsNil) + + _, err = s.ec2.AuthorizeSecurityGroup(ec2.SecurityGroup{Name: name}, perms[0:2]) + c.Assert(err, ErrorMatches, `.*\(InvalidPermission.Duplicate\)`) +} + +type filterSpec struct { + name string + values []string +} + +func (s *ServerTests) TestInstanceFiltering(c *C) { + groupResp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: sessionName("testgroup1"), Description: "testgroup one description"}) + c.Assert(err, IsNil) + group1 := groupResp.SecurityGroup + defer s.ec2.DeleteSecurityGroup(group1) + + groupResp, err = s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: sessionName("testgroup2"), Description: "testgroup two description"}) + c.Assert(err, IsNil) + group2 := groupResp.SecurityGroup + defer s.ec2.DeleteSecurityGroup(group2) + + insts := make([]*ec2.Instance, 3) + inst, err := s.ec2.RunInstances(&ec2.RunInstances{ + MinCount: 2, + ImageId: imageId, + InstanceType: "t1.micro", + SecurityGroups: []ec2.SecurityGroup{group1}, + }) + c.Assert(err, IsNil) + insts[0] = &inst.Instances[0] + insts[1] = &inst.Instances[1] + defer terminateInstances(c, s.ec2, insts) + + imageId2 := "ami-e358958a" // Natty server, i386, EBS store + inst, err = s.ec2.RunInstances(&ec2.RunInstances{ + ImageId: imageId2, + InstanceType: "t1.micro", + SecurityGroups: []ec2.SecurityGroup{group2}, + }) + c.Assert(err, IsNil) + insts[2] = &inst.Instances[0] + + ids := func(indices ...int) (instIds []string) { + for _, index := range indices { + instIds = append(instIds, insts[index].InstanceId) + } + return + } + + tests := []struct { + about string + instanceIds []string // instanceIds argument to Instances method. + filters []filterSpec // filters argument to Instances method. + resultIds []string // set of instance ids of expected results. + allowExtra bool // resultIds may be incomplete. + err string // expected error. + }{ + { + about: "check that Instances returns all instances", + resultIds: ids(0, 1, 2), + allowExtra: true, + }, { + about: "check that specifying two instance ids returns them", + instanceIds: ids(0, 2), + resultIds: ids(0, 2), + }, { + about: "check that specifying a non-existent instance id gives an error", + instanceIds: append(ids(0), "i-deadbeef"), + err: `.*\(InvalidInstanceID\.NotFound\)`, + }, { + about: "check that a filter allowed both instances returns both of them", + filters: []filterSpec{ + {"instance-id", ids(0, 2)}, + }, + resultIds: ids(0, 2), + }, { + about: "check that a filter allowing only one instance returns it", + filters: []filterSpec{ + {"instance-id", ids(1)}, + }, + resultIds: ids(1), + }, { + about: "check that a filter allowing no instances returns none", + filters: []filterSpec{ + {"instance-id", []string{"i-deadbeef12345"}}, + }, + }, { + about: "check that filtering on group id works", + filters: []filterSpec{ + {"group-id", []string{group1.Id}}, + }, + resultIds: ids(0, 1), + }, { + about: "check that filtering on group name works", + filters: []filterSpec{ + {"group-name", []string{group1.Name}}, + }, + resultIds: ids(0, 1), + }, { + about: "check that filtering on image id works", + filters: []filterSpec{ + {"image-id", []string{imageId}}, + }, + resultIds: ids(0, 1), + allowExtra: true, + }, { + about: "combination filters 1", + filters: []filterSpec{ + {"image-id", []string{imageId, imageId2}}, + {"group-name", []string{group1.Name}}, + }, + resultIds: ids(0, 1), + }, { + about: "combination filters 2", + filters: []filterSpec{ + {"image-id", []string{imageId2}}, + {"group-name", []string{group1.Name}}, + }, + }, + } + for i, t := range tests { + c.Logf("%d. %s", i, t.about) + var f *ec2.Filter + if t.filters != nil { + f = ec2.NewFilter() + for _, spec := range t.filters { + f.Add(spec.name, spec.values...) + } + } + resp, err := s.ec2.Instances(t.instanceIds, f) + if t.err != "" { + c.Check(err, ErrorMatches, t.err) + continue + } + c.Assert(err, IsNil) + insts := make(map[string]*ec2.Instance) + for _, r := range resp.Reservations { + for j := range r.Instances { + inst := &r.Instances[j] + c.Check(insts[inst.InstanceId], IsNil, Commentf("duplicate instance id: %q", inst.InstanceId)) + insts[inst.InstanceId] = inst + } + } + if !t.allowExtra { + c.Check(insts, HasLen, len(t.resultIds), Commentf("expected %d instances got %#v", len(t.resultIds), insts)) + } + for j, id := range t.resultIds { + c.Check(insts[id], NotNil, Commentf("instance id %d (%q) not found; got %#v", j, id, insts)) + } + } +} + +func idsOnly(gs []ec2.SecurityGroup) []ec2.SecurityGroup { + for i := range gs { + gs[i].Name = "" + } + return gs +} + +func namesOnly(gs []ec2.SecurityGroup) []ec2.SecurityGroup { + for i := range gs { + gs[i].Id = "" + } + return gs +} + +func (s *ServerTests) TestGroupFiltering(c *C) { + g := make([]ec2.SecurityGroup, 4) + for i := range g { + resp, err := s.ec2.CreateSecurityGroup(ec2.SecurityGroup{Name: sessionName(fmt.Sprintf("testgroup%d", i)), Description: fmt.Sprintf("testdescription%d", i)}) + c.Assert(err, IsNil) + g[i] = resp.SecurityGroup + c.Logf("group %d: %v", i, g[i]) + defer s.ec2.DeleteSecurityGroup(g[i]) + } + + perms := [][]ec2.IPPerm{ + {{ + Protocol: "tcp", + FromPort: 100, + ToPort: 200, + SourceIPs: []string{"1.2.3.4/32"}, + }}, + {{ + Protocol: "tcp", + FromPort: 200, + ToPort: 300, + SourceGroups: []ec2.UserSecurityGroup{{Id: g[1].Id}}, + }}, + {{ + Protocol: "udp", + FromPort: 200, + ToPort: 400, + SourceGroups: []ec2.UserSecurityGroup{{Id: g[1].Id}}, + }}, + } + for i, ps := range perms { + _, err := s.ec2.AuthorizeSecurityGroup(g[i], ps) + c.Assert(err, IsNil) + } + + groups := func(indices ...int) (gs []ec2.SecurityGroup) { + for _, index := range indices { + gs = append(gs, g[index]) + } + return + } + + type groupTest struct { + about string + groups []ec2.SecurityGroup // groupIds argument to SecurityGroups method. + filters []filterSpec // filters argument to SecurityGroups method. + results []ec2.SecurityGroup // set of expected result groups. + allowExtra bool // specified results may be incomplete. + err string // expected error. + } + filterCheck := func(name, val string, gs []ec2.SecurityGroup) groupTest { + return groupTest{ + about: "filter check " + name, + filters: []filterSpec{{name, []string{val}}}, + results: gs, + allowExtra: true, + } + } + tests := []groupTest{ + { + about: "check that SecurityGroups returns all groups", + results: groups(0, 1, 2, 3), + allowExtra: true, + }, { + about: "check that specifying two group ids returns them", + groups: idsOnly(groups(0, 2)), + results: groups(0, 2), + }, { + about: "check that specifying names only works", + groups: namesOnly(groups(0, 2)), + results: groups(0, 2), + }, { + about: "check that specifying a non-existent group id gives an error", + groups: append(groups(0), ec2.SecurityGroup{Id: "sg-eeeeeeeee"}), + err: `.*\(InvalidGroup\.NotFound\)`, + }, { + about: "check that a filter allowed two groups returns both of them", + filters: []filterSpec{ + {"group-id", []string{g[0].Id, g[2].Id}}, + }, + results: groups(0, 2), + }, + { + about: "check that the previous filter works when specifying a list of ids", + groups: groups(1, 2), + filters: []filterSpec{ + {"group-id", []string{g[0].Id, g[2].Id}}, + }, + results: groups(2), + }, { + about: "check that a filter allowing no groups returns none", + filters: []filterSpec{ + {"group-id", []string{"sg-eeeeeeeee"}}, + }, + }, + filterCheck("description", "testdescription1", groups(1)), + filterCheck("group-name", g[2].Name, groups(2)), + filterCheck("ip-permission.cidr", "1.2.3.4/32", groups(0)), + filterCheck("ip-permission.group-name", g[1].Name, groups(1, 2)), + filterCheck("ip-permission.protocol", "udp", groups(2)), + filterCheck("ip-permission.from-port", "200", groups(1, 2)), + filterCheck("ip-permission.to-port", "200", groups(0)), + // TODO owner-id + } + for i, t := range tests { + c.Logf("%d. %s", i, t.about) + var f *ec2.Filter + if t.filters != nil { + f = ec2.NewFilter() + for _, spec := range t.filters { + f.Add(spec.name, spec.values...) + } + } + resp, err := s.ec2.SecurityGroups(t.groups, f) + if t.err != "" { + c.Check(err, ErrorMatches, t.err) + continue + } + c.Assert(err, IsNil) + groups := make(map[string]*ec2.SecurityGroup) + for j := range resp.Groups { + group := &resp.Groups[j].SecurityGroup + c.Check(groups[group.Id], IsNil, Commentf("duplicate group id: %q", group.Id)) + + groups[group.Id] = group + } + // If extra groups may be returned, eliminate all groups that + // we did not create in this session apart from the default group. + if t.allowExtra { + namePat := regexp.MustCompile(sessionName("testgroup[0-9]")) + for id, g := range groups { + if !namePat.MatchString(g.Name) { + delete(groups, id) + } + } + } + c.Check(groups, HasLen, len(t.results)) + for j, g := range t.results { + rg := groups[g.Id] + c.Assert(rg, NotNil, Commentf("group %d (%v) not found; got %#v", j, g, groups)) + c.Check(rg.Name, Equals, g.Name, Commentf("group %d (%v)", j, g)) + } + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/filter.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/filter.go new file mode 100644 index 00000000000..1a0c0461937 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/filter.go @@ -0,0 +1,84 @@ +package ec2test + +import ( + "fmt" + "net/url" + "strings" +) + +// filter holds an ec2 filter. A filter maps an attribute to a set of +// possible values for that attribute. For an item to pass through the +// filter, every attribute of the item mentioned in the filter must match +// at least one of its given values. +type filter map[string][]string + +// newFilter creates a new filter from the Filter fields in the url form. +// +// The filtering is specified through a map of name=>values, where the +// name is a well-defined key identifying the data to be matched, +// and the list of values holds the possible values the filtered +// item can take for the key to be included in the +// result set. For example: +// +// Filter.1.Name=instance-type +// Filter.1.Value.1=m1.small +// Filter.1.Value.2=m1.large +// +func newFilter(form url.Values) filter { + // TODO return an error if the fields are not well formed? + names := make(map[int]string) + values := make(map[int][]string) + maxId := 0 + for name, fvalues := range form { + var rest string + var id int + if x, _ := fmt.Sscanf(name, "Filter.%d.%s", &id, &rest); x != 2 { + continue + } + if id > maxId { + maxId = id + } + if rest == "Name" { + names[id] = fvalues[0] + continue + } + if !strings.HasPrefix(rest, "Value.") { + continue + } + values[id] = append(values[id], fvalues[0]) + } + + f := make(filter) + for id, name := range names { + f[name] = values[id] + } + return f +} + +func notDigit(r rune) bool { + return r < '0' || r > '9' +} + +// filterable represents an object that can be passed through a filter. +type filterable interface { + // matchAttr returns true if given attribute of the + // object matches value. It returns an error if the + // attribute is not recognised or the value is malformed. + matchAttr(attr, value string) (bool, error) +} + +// ok returns true if x passes through the filter. +func (f filter) ok(x filterable) (bool, error) { +next: + for a, vs := range f { + for _, v := range vs { + if ok, err := x.matchAttr(a, v); ok { + continue next + } else if err != nil { + return false, fmt.Errorf("bad attribute or value %q=%q for type %T: %v", a, v, x, err) + } + } + return false, nil + } + return true, nil +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/server.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/server.go new file mode 100644 index 00000000000..2f24cb2a244 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/ec2test/server.go @@ -0,0 +1,993 @@ +// The ec2test package implements a fake EC2 provider with +// the capability of inducing errors on any given operation, +// and retrospectively determining what operations have been +// carried out. +package ec2test + +import ( + "encoding/base64" + "encoding/xml" + "fmt" + "github.com/mitchellh/goamz/ec2" + "io" + "net" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" + "sync" +) + +var b64 = base64.StdEncoding + +// Action represents a request that changes the ec2 state. +type Action struct { + RequestId string + + // Request holds the requested action as a url.Values instance + Request url.Values + + // If the action succeeded, Response holds the value that + // was marshalled to build the XML response for the request. + Response interface{} + + // If the action failed, Err holds an error giving details of the failure. + Err *ec2.Error +} + +// TODO possible other things: +// - some virtual time stamp interface, so a client +// can ask for all actions after a certain virtual time. + +// Server implements an EC2 simulator for use in testing. +type Server struct { + url string + listener net.Listener + mu sync.Mutex + reqs []*Action + + instances map[string]*Instance // id -> instance + reservations map[string]*reservation // id -> reservation + groups map[string]*securityGroup // id -> group + maxId counter + reqId counter + reservationId counter + groupId counter + initialInstanceState ec2.InstanceState +} + +// reservation holds a simulated ec2 reservation. +type reservation struct { + id string + instances map[string]*Instance + groups []*securityGroup +} + +// instance holds a simulated ec2 instance +type Instance struct { + // UserData holds the data that was passed to the RunInstances request + // when the instance was started. + UserData []byte + id string + imageId string + reservation *reservation + instType string + state ec2.InstanceState +} + +// permKey represents permission for a given security +// group or IP address (but not both) to access a given range of +// ports. Equality of permKeys is used in the implementation of +// permission sets, relying on the uniqueness of securityGroup +// instances. +type permKey struct { + protocol string + fromPort int + toPort int + group *securityGroup + ipAddr string +} + +// securityGroup holds a simulated ec2 security group. +// Instances of securityGroup should only be created through +// Server.createSecurityGroup to ensure that groups can be +// compared by pointer value. +type securityGroup struct { + id string + name string + description string + + perms map[permKey]bool +} + +func (g *securityGroup) ec2SecurityGroup() ec2.SecurityGroup { + return ec2.SecurityGroup{ + Name: g.name, + Id: g.id, + } +} + +func (g *securityGroup) matchAttr(attr, value string) (ok bool, err error) { + switch attr { + case "description": + return g.description == value, nil + case "group-id": + return g.id == value, nil + case "group-name": + return g.name == value, nil + case "ip-permission.cidr": + return g.hasPerm(func(k permKey) bool { return k.ipAddr == value }), nil + case "ip-permission.group-name": + return g.hasPerm(func(k permKey) bool { + return k.group != nil && k.group.name == value + }), nil + case "ip-permission.from-port": + port, err := strconv.Atoi(value) + if err != nil { + return false, err + } + return g.hasPerm(func(k permKey) bool { return k.fromPort == port }), nil + case "ip-permission.to-port": + port, err := strconv.Atoi(value) + if err != nil { + return false, err + } + return g.hasPerm(func(k permKey) bool { return k.toPort == port }), nil + case "ip-permission.protocol": + return g.hasPerm(func(k permKey) bool { return k.protocol == value }), nil + case "owner-id": + return value == ownerId, nil + } + return false, fmt.Errorf("unknown attribute %q", attr) +} + +func (g *securityGroup) hasPerm(test func(k permKey) bool) bool { + for k := range g.perms { + if test(k) { + return true + } + } + return false +} + +// ec2Perms returns the list of EC2 permissions granted +// to g. It groups permissions by port range and protocol. +func (g *securityGroup) ec2Perms() (perms []ec2.IPPerm) { + // The grouping is held in result. We use permKey for convenience, + // (ensuring that the group and ipAddr of each key is zero). For + // each protocol/port range combination, we build up the permission + // set in the associated value. + result := make(map[permKey]*ec2.IPPerm) + for k := range g.perms { + groupKey := k + groupKey.group = nil + groupKey.ipAddr = "" + + ec2p := result[groupKey] + if ec2p == nil { + ec2p = &ec2.IPPerm{ + Protocol: k.protocol, + FromPort: k.fromPort, + ToPort: k.toPort, + } + result[groupKey] = ec2p + } + if k.group != nil { + ec2p.SourceGroups = append(ec2p.SourceGroups, + ec2.UserSecurityGroup{ + Id: k.group.id, + Name: k.group.name, + OwnerId: ownerId, + }) + } else { + ec2p.SourceIPs = append(ec2p.SourceIPs, k.ipAddr) + } + } + for _, ec2p := range result { + perms = append(perms, *ec2p) + } + return +} + +var actions = map[string]func(*Server, http.ResponseWriter, *http.Request, string) interface{}{ + "RunInstances": (*Server).runInstances, + "TerminateInstances": (*Server).terminateInstances, + "DescribeInstances": (*Server).describeInstances, + "CreateSecurityGroup": (*Server).createSecurityGroup, + "DescribeSecurityGroups": (*Server).describeSecurityGroups, + "DeleteSecurityGroup": (*Server).deleteSecurityGroup, + "AuthorizeSecurityGroupIngress": (*Server).authorizeSecurityGroupIngress, + "RevokeSecurityGroupIngress": (*Server).revokeSecurityGroupIngress, +} + +const ownerId = "9876" + +// newAction allocates a new action and adds it to the +// recorded list of server actions. +func (srv *Server) newAction() *Action { + srv.mu.Lock() + defer srv.mu.Unlock() + + a := new(Action) + srv.reqs = append(srv.reqs, a) + return a +} + +// NewServer returns a new server. +func NewServer() (*Server, error) { + srv := &Server{ + instances: make(map[string]*Instance), + groups: make(map[string]*securityGroup), + reservations: make(map[string]*reservation), + initialInstanceState: Pending, + } + + // Add default security group. + g := &securityGroup{ + name: "default", + description: "default group", + id: fmt.Sprintf("sg-%d", srv.groupId.next()), + } + g.perms = map[permKey]bool{ + permKey{ + protocol: "icmp", + fromPort: -1, + toPort: -1, + group: g, + }: true, + permKey{ + protocol: "tcp", + fromPort: 0, + toPort: 65535, + group: g, + }: true, + permKey{ + protocol: "udp", + fromPort: 0, + toPort: 65535, + group: g, + }: true, + } + srv.groups[g.id] = g + + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return nil, fmt.Errorf("cannot listen on localhost: %v", err) + } + srv.listener = l + + srv.url = "http://" + l.Addr().String() + + // we use HandlerFunc rather than *Server directly so that we + // can avoid exporting HandlerFunc from *Server. + go http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + srv.serveHTTP(w, req) + })) + return srv, nil +} + +// Quit closes down the server. +func (srv *Server) Quit() { + srv.listener.Close() +} + +// SetInitialInstanceState sets the state that any new instances will be started in. +func (srv *Server) SetInitialInstanceState(state ec2.InstanceState) { + srv.mu.Lock() + srv.initialInstanceState = state + srv.mu.Unlock() +} + +// URL returns the URL of the server. +func (srv *Server) URL() string { + return srv.url +} + +// serveHTTP serves the EC2 protocol. +func (srv *Server) serveHTTP(w http.ResponseWriter, req *http.Request) { + req.ParseForm() + + a := srv.newAction() + a.RequestId = fmt.Sprintf("req%d", srv.reqId.next()) + a.Request = req.Form + + // Methods on Server that deal with parsing user data + // may fail. To save on error handling code, we allow these + // methods to call fatalf, which will panic with an *ec2.Error + // which will be caught here and returned + // to the client as a properly formed EC2 error. + defer func() { + switch err := recover().(type) { + case *ec2.Error: + a.Err = err + err.RequestId = a.RequestId + writeError(w, err) + case nil: + default: + panic(err) + } + }() + + f := actions[req.Form.Get("Action")] + if f == nil { + fatalf(400, "InvalidParameterValue", "Unrecognized Action") + } + + response := f(srv, w, req, a.RequestId) + a.Response = response + + w.Header().Set("Content-Type", `xml version="1.0" encoding="UTF-8"`) + xmlMarshal(w, response) +} + +// Instance returns the instance for the given instance id. +// It returns nil if there is no such instance. +func (srv *Server) Instance(id string) *Instance { + srv.mu.Lock() + defer srv.mu.Unlock() + return srv.instances[id] +} + +// writeError writes an appropriate error response. +// TODO how should we deal with errors when the +// error itself is potentially generated by backend-agnostic +// code? +func writeError(w http.ResponseWriter, err *ec2.Error) { + // Error encapsulates an error returned by EC2. + // TODO merge with ec2.Error when xml supports ignoring a field. + type ec2error struct { + Code string // EC2 error code ("UnsupportedOperation", ...) + Message string // The human-oriented error message + RequestId string + } + + type Response struct { + RequestId string + Errors []ec2error `xml:"Errors>Error"` + } + + w.Header().Set("Content-Type", `xml version="1.0" encoding="UTF-8"`) + w.WriteHeader(err.StatusCode) + xmlMarshal(w, Response{ + RequestId: err.RequestId, + Errors: []ec2error{{ + Code: err.Code, + Message: err.Message, + }}, + }) +} + +// xmlMarshal is the same as xml.Marshal except that +// it panics on error. The marshalling should not fail, +// but we want to know if it does. +func xmlMarshal(w io.Writer, x interface{}) { + if err := xml.NewEncoder(w).Encode(x); err != nil { + panic(fmt.Errorf("error marshalling %#v: %v", x, err)) + } +} + +// formToGroups parses a set of SecurityGroup form values +// as found in a RunInstances request, and returns the resulting +// slice of security groups. +// It calls fatalf if a group is not found. +func (srv *Server) formToGroups(form url.Values) []*securityGroup { + var groups []*securityGroup + for name, values := range form { + switch { + case strings.HasPrefix(name, "SecurityGroupId."): + if g := srv.groups[values[0]]; g != nil { + groups = append(groups, g) + } else { + fatalf(400, "InvalidGroup.NotFound", "unknown group id %q", values[0]) + } + case strings.HasPrefix(name, "SecurityGroup."): + var found *securityGroup + for _, g := range srv.groups { + if g.name == values[0] { + found = g + } + } + if found == nil { + fatalf(400, "InvalidGroup.NotFound", "unknown group name %q", values[0]) + } + groups = append(groups, found) + } + } + return groups +} + +// runInstances implements the EC2 RunInstances entry point. +func (srv *Server) runInstances(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + min := atoi(req.Form.Get("MinCount")) + max := atoi(req.Form.Get("MaxCount")) + if min < 0 || max < 1 { + fatalf(400, "InvalidParameterValue", "bad values for MinCount or MaxCount") + } + if min > max { + fatalf(400, "InvalidParameterCombination", "MinCount is greater than MaxCount") + } + var userData []byte + if data := req.Form.Get("UserData"); data != "" { + var err error + userData, err = b64.DecodeString(data) + if err != nil { + fatalf(400, "InvalidParameterValue", "bad UserData value: %v", err) + } + } + + // TODO attributes still to consider: + // ImageId: accept anything, we can verify later + // KeyName ? + // InstanceType ? + // KernelId ? + // RamdiskId ? + // AvailZone ? + // GroupName tag + // Monitoring ignore? + // SubnetId ? + // DisableAPITermination bool + // ShutdownBehavior string + // PrivateIPAddress string + + srv.mu.Lock() + defer srv.mu.Unlock() + + // make sure that form fields are correct before creating the reservation. + instType := req.Form.Get("InstanceType") + imageId := req.Form.Get("ImageId") + + r := srv.newReservation(srv.formToGroups(req.Form)) + + var resp ec2.RunInstancesResp + resp.RequestId = reqId + resp.ReservationId = r.id + resp.OwnerId = ownerId + + for i := 0; i < max; i++ { + inst := srv.newInstance(r, instType, imageId, srv.initialInstanceState) + inst.UserData = userData + resp.Instances = append(resp.Instances, inst.ec2instance()) + } + return &resp +} + +func (srv *Server) group(group ec2.SecurityGroup) *securityGroup { + if group.Id != "" { + return srv.groups[group.Id] + } + for _, g := range srv.groups { + if g.name == group.Name { + return g + } + } + return nil +} + +// NewInstances creates n new instances in srv with the given instance type, +// image ID, initial state and security groups. If any group does not already +// exist, it will be created. NewInstances returns the ids of the new instances. +func (srv *Server) NewInstances(n int, instType string, imageId string, state ec2.InstanceState, groups []ec2.SecurityGroup) []string { + srv.mu.Lock() + defer srv.mu.Unlock() + + rgroups := make([]*securityGroup, len(groups)) + for i, group := range groups { + g := srv.group(group) + if g == nil { + fatalf(400, "InvalidGroup.NotFound", "no such group %v", g) + } + rgroups[i] = g + } + r := srv.newReservation(rgroups) + + ids := make([]string, n) + for i := 0; i < n; i++ { + inst := srv.newInstance(r, instType, imageId, state) + ids[i] = inst.id + } + return ids +} + +func (srv *Server) newInstance(r *reservation, instType string, imageId string, state ec2.InstanceState) *Instance { + inst := &Instance{ + id: fmt.Sprintf("i-%d", srv.maxId.next()), + instType: instType, + imageId: imageId, + state: state, + reservation: r, + } + srv.instances[inst.id] = inst + r.instances[inst.id] = inst + return inst +} + +func (srv *Server) newReservation(groups []*securityGroup) *reservation { + r := &reservation{ + id: fmt.Sprintf("r-%d", srv.reservationId.next()), + instances: make(map[string]*Instance), + groups: groups, + } + + srv.reservations[r.id] = r + return r +} + +func (srv *Server) terminateInstances(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + srv.mu.Lock() + defer srv.mu.Unlock() + var resp ec2.TerminateInstancesResp + resp.RequestId = reqId + var insts []*Instance + for attr, vals := range req.Form { + if strings.HasPrefix(attr, "InstanceId.") { + id := vals[0] + inst := srv.instances[id] + if inst == nil { + fatalf(400, "InvalidInstanceID.NotFound", "no such instance id %q", id) + } + insts = append(insts, inst) + } + } + for _, inst := range insts { + resp.StateChanges = append(resp.StateChanges, inst.terminate()) + } + return &resp +} + +func (inst *Instance) terminate() (d ec2.InstanceStateChange) { + d.PreviousState = inst.state + inst.state = ShuttingDown + d.CurrentState = inst.state + d.InstanceId = inst.id + return d +} + +func (inst *Instance) ec2instance() ec2.Instance { + return ec2.Instance{ + InstanceId: inst.id, + InstanceType: inst.instType, + ImageId: inst.imageId, + DNSName: fmt.Sprintf("%s.example.com", inst.id), + // TODO the rest + } +} + +func (inst *Instance) matchAttr(attr, value string) (ok bool, err error) { + switch attr { + case "architecture": + return value == "i386", nil + case "instance-id": + return inst.id == value, nil + case "group-id": + for _, g := range inst.reservation.groups { + if g.id == value { + return true, nil + } + } + return false, nil + case "group-name": + for _, g := range inst.reservation.groups { + if g.name == value { + return true, nil + } + } + return false, nil + case "image-id": + return value == inst.imageId, nil + case "instance-state-code": + code, err := strconv.Atoi(value) + if err != nil { + return false, err + } + return code&0xff == inst.state.Code, nil + case "instance-state-name": + return value == inst.state.Name, nil + } + return false, fmt.Errorf("unknown attribute %q", attr) +} + +var ( + Pending = ec2.InstanceState{0, "pending"} + Running = ec2.InstanceState{16, "running"} + ShuttingDown = ec2.InstanceState{32, "shutting-down"} + Terminated = ec2.InstanceState{16, "terminated"} + Stopped = ec2.InstanceState{16, "stopped"} +) + +func (srv *Server) createSecurityGroup(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + name := req.Form.Get("GroupName") + if name == "" { + fatalf(400, "InvalidParameterValue", "empty security group name") + } + srv.mu.Lock() + defer srv.mu.Unlock() + if srv.group(ec2.SecurityGroup{Name: name}) != nil { + fatalf(400, "InvalidGroup.Duplicate", "group %q already exists", name) + } + g := &securityGroup{ + name: name, + description: req.Form.Get("GroupDescription"), + id: fmt.Sprintf("sg-%d", srv.groupId.next()), + perms: make(map[permKey]bool), + } + srv.groups[g.id] = g + // we define a local type for this because ec2.CreateSecurityGroupResp + // contains SecurityGroup, but the response to this request + // should not contain the security group name. + type CreateSecurityGroupResponse struct { + RequestId string `xml:"requestId"` + Return bool `xml:"return"` + GroupId string `xml:"groupId"` + } + r := &CreateSecurityGroupResponse{ + RequestId: reqId, + Return: true, + GroupId: g.id, + } + return r +} + +func (srv *Server) notImplemented(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + fatalf(500, "InternalError", "not implemented") + panic("not reached") +} + +func (srv *Server) describeInstances(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + srv.mu.Lock() + defer srv.mu.Unlock() + insts := make(map[*Instance]bool) + for name, vals := range req.Form { + if !strings.HasPrefix(name, "InstanceId.") { + continue + } + inst := srv.instances[vals[0]] + if inst == nil { + fatalf(400, "InvalidInstanceID.NotFound", "instance %q not found", vals[0]) + } + insts[inst] = true + } + + f := newFilter(req.Form) + + var resp ec2.InstancesResp + resp.RequestId = reqId + for _, r := range srv.reservations { + var instances []ec2.Instance + for _, inst := range r.instances { + if len(insts) > 0 && !insts[inst] { + continue + } + ok, err := f.ok(inst) + if ok { + instances = append(instances, inst.ec2instance()) + } else if err != nil { + fatalf(400, "InvalidParameterValue", "describe instances: %v", err) + } + } + if len(instances) > 0 { + var groups []ec2.SecurityGroup + for _, g := range r.groups { + groups = append(groups, g.ec2SecurityGroup()) + } + resp.Reservations = append(resp.Reservations, ec2.Reservation{ + ReservationId: r.id, + OwnerId: ownerId, + Instances: instances, + SecurityGroups: groups, + }) + } + } + return &resp +} + +func (srv *Server) describeSecurityGroups(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + // BUG similar bug to describeInstances, but for GroupName and GroupId + srv.mu.Lock() + defer srv.mu.Unlock() + + var groups []*securityGroup + for name, vals := range req.Form { + var g ec2.SecurityGroup + switch { + case strings.HasPrefix(name, "GroupName."): + g.Name = vals[0] + case strings.HasPrefix(name, "GroupId."): + g.Id = vals[0] + default: + continue + } + sg := srv.group(g) + if sg == nil { + fatalf(400, "InvalidGroup.NotFound", "no such group %v", g) + } + groups = append(groups, sg) + } + if len(groups) == 0 { + for _, g := range srv.groups { + groups = append(groups, g) + } + } + + f := newFilter(req.Form) + var resp ec2.SecurityGroupsResp + resp.RequestId = reqId + for _, group := range groups { + ok, err := f.ok(group) + if ok { + resp.Groups = append(resp.Groups, ec2.SecurityGroupInfo{ + OwnerId: ownerId, + SecurityGroup: group.ec2SecurityGroup(), + Description: group.description, + IPPerms: group.ec2Perms(), + }) + } else if err != nil { + fatalf(400, "InvalidParameterValue", "describe security groups: %v", err) + } + } + return &resp +} + +func (srv *Server) authorizeSecurityGroupIngress(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + srv.mu.Lock() + defer srv.mu.Unlock() + g := srv.group(ec2.SecurityGroup{ + Name: req.Form.Get("GroupName"), + Id: req.Form.Get("GroupId"), + }) + if g == nil { + fatalf(400, "InvalidGroup.NotFound", "group not found") + } + perms := srv.parsePerms(req) + + for _, p := range perms { + if g.perms[p] { + fatalf(400, "InvalidPermission.Duplicate", "Permission has already been authorized on the specified group") + } + } + for _, p := range perms { + g.perms[p] = true + } + return &ec2.SimpleResp{ + XMLName: xml.Name{"", "AuthorizeSecurityGroupIngressResponse"}, + RequestId: reqId, + } +} + +func (srv *Server) revokeSecurityGroupIngress(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + srv.mu.Lock() + defer srv.mu.Unlock() + g := srv.group(ec2.SecurityGroup{ + Name: req.Form.Get("GroupName"), + Id: req.Form.Get("GroupId"), + }) + if g == nil { + fatalf(400, "InvalidGroup.NotFound", "group not found") + } + perms := srv.parsePerms(req) + + // Note EC2 does not give an error if asked to revoke an authorization + // that does not exist. + for _, p := range perms { + delete(g.perms, p) + } + return &ec2.SimpleResp{ + XMLName: xml.Name{"", "RevokeSecurityGroupIngressResponse"}, + RequestId: reqId, + } +} + +var secGroupPat = regexp.MustCompile(`^sg-[a-z0-9]+$`) +var ipPat = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$`) +var ownerIdPat = regexp.MustCompile(`^[0-9]+$`) + +// parsePerms returns a slice of permKey values extracted +// from the permission fields in req. +func (srv *Server) parsePerms(req *http.Request) []permKey { + // perms maps an index found in the form to its associated + // IPPerm. For instance, the form value with key + // "IpPermissions.3.FromPort" will be stored in perms[3].FromPort + perms := make(map[int]ec2.IPPerm) + + type subgroupKey struct { + id1, id2 int + } + // Each IPPerm can have many source security groups. The form key + // for a source security group contains two indices: the index + // of the IPPerm and the sub-index of the security group. The + // sourceGroups map maps from a subgroupKey containing these + // two indices to the associated security group. For instance, + // the form value with key "IPPermissions.3.Groups.2.GroupName" + // will be stored in sourceGroups[subgroupKey{3, 2}].Name. + sourceGroups := make(map[subgroupKey]ec2.UserSecurityGroup) + + // For each value in the form we store its associated information in the + // above maps. The maps are necessary because the form keys may + // arrive in any order, and the indices are not + // necessarily sequential or even small. + for name, vals := range req.Form { + val := vals[0] + var id1 int + var rest string + if x, _ := fmt.Sscanf(name, "IpPermissions.%d.%s", &id1, &rest); x != 2 { + continue + } + ec2p := perms[id1] + switch { + case rest == "FromPort": + ec2p.FromPort = atoi(val) + case rest == "ToPort": + ec2p.ToPort = atoi(val) + case rest == "IpProtocol": + switch val { + case "tcp", "udp", "icmp": + ec2p.Protocol = val + default: + // check it's a well formed number + atoi(val) + ec2p.Protocol = val + } + case strings.HasPrefix(rest, "Groups."): + k := subgroupKey{id1: id1} + if x, _ := fmt.Sscanf(rest[len("Groups."):], "%d.%s", &k.id2, &rest); x != 2 { + continue + } + g := sourceGroups[k] + switch rest { + case "UserId": + // BUG if the user id is blank, this does not conform to the + // way that EC2 handles it - a specified but blank owner id + // can cause RevokeSecurityGroupIngress to fail with + // "group not found" even if the security group id has been + // correctly specified. + // By failing here, we ensure that we fail early in this case. + if !ownerIdPat.MatchString(val) { + fatalf(400, "InvalidUserID.Malformed", "Invalid user ID: %q", val) + } + g.OwnerId = val + case "GroupName": + g.Name = val + case "GroupId": + if !secGroupPat.MatchString(val) { + fatalf(400, "InvalidGroupId.Malformed", "Invalid group ID: %q", val) + } + g.Id = val + default: + fatalf(400, "UnknownParameter", "unknown parameter %q", name) + } + sourceGroups[k] = g + case strings.HasPrefix(rest, "IpRanges."): + var id2 int + if x, _ := fmt.Sscanf(rest[len("IpRanges."):], "%d.%s", &id2, &rest); x != 2 { + continue + } + switch rest { + case "CidrIp": + if !ipPat.MatchString(val) { + fatalf(400, "InvalidPermission.Malformed", "Invalid IP range: %q", val) + } + ec2p.SourceIPs = append(ec2p.SourceIPs, val) + default: + fatalf(400, "UnknownParameter", "unknown parameter %q", name) + } + default: + fatalf(400, "UnknownParameter", "unknown parameter %q", name) + } + perms[id1] = ec2p + } + // Associate each set of source groups with its IPPerm. + for k, g := range sourceGroups { + p := perms[k.id1] + p.SourceGroups = append(p.SourceGroups, g) + perms[k.id1] = p + } + + // Now that we have built up the IPPerms we need, we check for + // parameter errors and build up a permKey for each permission, + // looking up security groups from srv as we do so. + var result []permKey + for _, p := range perms { + if p.FromPort > p.ToPort { + fatalf(400, "InvalidParameterValue", "invalid port range") + } + k := permKey{ + protocol: p.Protocol, + fromPort: p.FromPort, + toPort: p.ToPort, + } + for _, g := range p.SourceGroups { + if g.OwnerId != "" && g.OwnerId != ownerId { + fatalf(400, "InvalidGroup.NotFound", "group %q not found", g.Name) + } + var ec2g ec2.SecurityGroup + switch { + case g.Id != "": + ec2g.Id = g.Id + case g.Name != "": + ec2g.Name = g.Name + } + k.group = srv.group(ec2g) + if k.group == nil { + fatalf(400, "InvalidGroup.NotFound", "group %v not found", g) + } + result = append(result, k) + } + k.group = nil + for _, ip := range p.SourceIPs { + k.ipAddr = ip + result = append(result, k) + } + } + return result +} + +func (srv *Server) deleteSecurityGroup(w http.ResponseWriter, req *http.Request, reqId string) interface{} { + srv.mu.Lock() + defer srv.mu.Unlock() + g := srv.group(ec2.SecurityGroup{ + Name: req.Form.Get("GroupName"), + Id: req.Form.Get("GroupId"), + }) + if g == nil { + fatalf(400, "InvalidGroup.NotFound", "group not found") + } + for _, r := range srv.reservations { + for _, h := range r.groups { + if h == g && r.hasRunningMachine() { + fatalf(500, "InvalidGroup.InUse", "group is currently in use by a running instance") + } + } + } + for _, sg := range srv.groups { + // If a group refers to itself, it's ok to delete it. + if sg == g { + continue + } + for k := range sg.perms { + if k.group == g { + fatalf(500, "InvalidGroup.InUse", "group is currently in use by group %q", sg.id) + } + } + } + + delete(srv.groups, g.id) + return &ec2.SimpleResp{ + XMLName: xml.Name{"", "DeleteSecurityGroupResponse"}, + RequestId: reqId, + } +} + +func (r *reservation) hasRunningMachine() bool { + for _, inst := range r.instances { + if inst.state.Code != ShuttingDown.Code && inst.state.Code != Terminated.Code { + return true + } + } + return false +} + +type counter int + +func (c *counter) next() (i int) { + i = int(*c) + (*c)++ + return +} + +// atoi is like strconv.Atoi but is fatal if the +// string is not well formed. +func atoi(s string) int { + i, err := strconv.Atoi(s) + if err != nil { + fatalf(400, "InvalidParameterValue", "bad number: %v", err) + } + return i +} + +func fatalf(statusCode int, code string, f string, a ...interface{}) { + panic(&ec2.Error{ + StatusCode: statusCode, + Code: code, + Message: fmt.Sprintf(f, a...), + }) +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/export_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/export_test.go new file mode 100644 index 00000000000..1c24422129b --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/export_test.go @@ -0,0 +1,22 @@ +package ec2 + +import ( + "github.com/mitchellh/goamz/aws" + "time" +) + +func Sign(auth aws.Auth, method, path string, params map[string]string, host string) { + sign(auth, method, path, params, host) +} + +func fixedTime() time.Time { + return time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC) +} + +func FakeTime(fakeIt bool) { + if fakeIt { + timeNow = fixedTime + } else { + timeNow = time.Now + } +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/responses_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/responses_test.go new file mode 100644 index 00000000000..0a4dbb366bb --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/responses_test.go @@ -0,0 +1,854 @@ +package ec2_test + +var ErrorDump = ` + +UnsupportedOperation +AMIs with an instance-store root device are not supported for the instance type 't1.micro'. +0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4 +` + +// http://goo.gl/Mcm3b +var RunInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + r-47a5402e + 999988887777 + + + sg-67ad940e + default + + + + + i-2ba64342 + ami-60a54009 + + 0 + pending + + + + example-key-name + 0 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + paravirtual + + + xen + + + i-2bc64242 + ami-60a54009 + + 0 + pending + + + + example-key-name + 1 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + paravirtual + + + xen + + + i-2be64332 + ami-60a54009 + + 0 + pending + + + + example-key-name + 2 + m1.small + 2007-08-07T11:51:50.000Z + + us-east-1b + + + enabled + + paravirtual + + + xen + + + +` + +// http://goo.gl/GRZgCD +var RequestSpotInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + sir-1a2b3c4d + 0.5 + one-time + open + + pending-evaluation + 2008-05-07T12:51:50.000Z + Your Spot request has been submitted for review, and is pending evaluation. + + MyAzGroup + + ami-1a2b3c4d + gsg-keypair + + + sg-1a2b3c4d + websrv + + + m1.small + + + false + + false + + YYYY-MM-DDTHH:MM:SS.000Z + Linux/UNIX + + + +` + +// http://goo.gl/KsKJJk +var DescribeSpotRequestsExample = ` + + b1719f2a-5334-4479-b2f1-26926EXAMPLE + + + sir-1a2b3c4d + 0.5 + one-time + active + + fulfilled + 2008-05-07T12:51:50.000Z + Your Spot request is fulfilled. + + + ami-1a2b3c4d + gsg-keypair + + + sg-1a2b3c4d + websrv + + + m1.small + + false + + false + + i-1a2b3c4d + YYYY-MM-DDTHH:MM:SS.000Z + Linux/UNIX + us-east-1a + + + +` + +// http://goo.gl/DcfFgJ +var CancelSpotRequestsExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + sir-1a2b3c4d + cancelled + + + +` + +// http://goo.gl/3BKHj +var TerminateInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + i-3ea74257 + + 32 + shutting-down + + + 16 + running + + + + +` + +// http://goo.gl/mLbmw +var DescribeInstancesExample1 = ` + + 98e3c9a4-848c-4d6d-8e8a-b1bdEXAMPLE + + + r-b27e30d9 + 999988887777 + + + sg-67ad940e + default + + + + + i-c5cd56af + ami-1a2b3c4d + + 16 + running + + domU-12-31-39-10-56-34.compute-1.internal + ec2-174-129-165-232.compute-1.amazonaws.com + + GSG_Keypair + 0 + + m1.small + 2010-08-17T01:15:18.000Z + + us-east-1b + + + aki-94c527fd + ari-96c527ff + + disabled + + 10.198.85.190 + 174.129.165.232 + i386 + ebs + /dev/sda1 + + + /dev/sda1 + + vol-a082c1c9 + attached + 2010-08-17T01:15:21.000Z + false + + + + spot + sir-7a688402 + paravirtual + + + xen + + + 854251627541 + + + r-b67e30dd + 999988887777 + + + sg-67ad940e + default + + + + + i-d9cd56b3 + ami-1a2b3c4d + + 16 + running + + domU-12-31-39-10-54-E5.compute-1.internal + ec2-184-73-58-78.compute-1.amazonaws.com + + GSG_Keypair + 0 + + m1.large + 2010-08-17T01:15:19.000Z + + us-east-1b + + + aki-94c527fd + ari-96c527ff + + disabled + + 10.198.87.19 + 184.73.58.78 + i386 + ebs + /dev/sda1 + + + /dev/sda1 + + vol-a282c1cb + attached + 2010-08-17T01:15:23.000Z + false + + + + spot + sir-55a3aa02 + paravirtual + + + xen + + + 854251627541 + + + +` + +// http://goo.gl/mLbmw +var DescribeInstancesExample2 = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + r-bc7e30d7 + 999988887777 + + + sg-67ad940e + default + + + + + i-c7cd56ad + ami-b232d0db + + 16 + running + + domU-12-31-39-01-76-06.compute-1.internal + ec2-72-44-52-124.compute-1.amazonaws.com + GSG_Keypair + 0 + + m1.small + 2010-08-17T01:15:16.000Z + + us-east-1b + + aki-94c527fd + ari-96c527ff + + disabled + + 10.255.121.240 + 72.44.52.124 + i386 + ebs + /dev/sda1 + + + /dev/sda1 + + vol-a482c1cd + attached + 2010-08-17T01:15:26.000Z + true + + + + paravirtual + + + + webserver + + + + stack + Production + + + xen + + + + + +` + +// http://goo.gl/cxU41 +var CreateImageExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + ami-4fa54026 + +` + +// http://goo.gl/V0U25 +var DescribeImagesExample = ` + + 4a4a27a2-2e7c-475d-b35b-ca822EXAMPLE + + + ami-a2469acf + aws-marketplace/example-marketplace-amzn-ami.1 + available + 123456789999 + true + + + a1b2c3d4e5f6g7h8i9j10k11 + marketplace + + + i386 + machine + aki-805ea7e9 + aws-marketplace + example-marketplace-amzn-ami.1 + Amazon Linux AMI i386 EBS + ebs + /dev/sda1 + + + /dev/sda1 + + snap-787e9403 + 8 + true + + + + paravirtual + xen + + + +` + +// http://goo.gl/bHO3z +var ImageAttributeExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + ami-61a54008 + + + all + + + 495219933132 + + + +` + +// http://goo.gl/ttcda +var CreateSnapshotExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + snap-78a54011 + vol-4d826724 + pending + 2008-05-07T12:51:50.000Z + 60% + 111122223333 + 10 + Daily Backup + +` + +// http://goo.gl/vwU1y +var DeleteSnapshotExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/nkovs +var DescribeSnapshotsExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + snap-1a2b3c4d + vol-8875daef + pending + 2010-07-29T04:12:01.000Z + 30% + 111122223333 + 15 + Daily Backup + + + Purpose + demo_db_14_backup + + + + + +` + +// http://goo.gl/YUjO4G +var ModifyImageAttributeExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/hQwPCK +var CopyImageExample = ` + + 60bc441d-fa2c-494d-b155-5d6a3EXAMPLE + ami-4d3c2b1a + +` + +var CreateKeyPairExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + foo + + 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00 + + ---- BEGIN RSA PRIVATE KEY ---- +MIICiTCCAfICCQD6m7oRw0uXOjANBgkqhkiG9w0BAQUFADCBiDELMAkGA1UEBhMC +VVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6 +b24xFDASBgNVBAsTC0lBTSBDb25zb2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAd +BgkqhkiG9w0BCQEWEG5vb25lQGFtYXpvbi5jb20wHhcNMTEwNDI1MjA0NTIxWhcN +MTIwNDI0MjA0NTIxWjCBiDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYD +VQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZBbWF6b24xFDASBgNVBAsTC0lBTSBDb25z +b2xlMRIwEAYDVQQDEwlUZXN0Q2lsYWMxHzAdBgkqhkiG9w0BCQEWEG5vb25lQGFt +YXpvbi5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMaK0dn+a4GmWIWJ +21uUSfwfEvySWtC2XADZ4nB+BLYgVIk60CpiwsZ3G93vUEIO3IyNoH/f0wYK8m9T +rDHudUZg3qX4waLG5M43q7Wgc/MbQITxOUSQv7c7ugFFDzQGBzZswY6786m86gpE +Ibb3OhjZnzcvQAaRHhdlQWIMm2nrAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAtCu4 +nUhVVxYUntneD9+h8Mg9q6q+auNKyExzyLwaxlAoo7TJHidbtS4J5iNmZgXL0Fkb +FFBjvSfpJIlJ00zbhNYS5f6GuoEDmFJl0ZxBHjJnyp378OD8uTs7fLvjx79LjSTb +NYiytVbZPQUQ5Yaxu2jXnimvw3rrszlaEXAMPLE= +-----END RSA PRIVATE KEY----- + + +` + +var DeleteKeyPairExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/Eo7Yl +var CreateSecurityGroupExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + sg-67ad940e + +` + +// http://goo.gl/k12Uy +var DescribeSecurityGroupsExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + 999988887777 + WebServers + sg-67ad940e + Web Servers + + + tcp + 80 + 80 + + + + 0.0.0.0/0 + + + + + + + 999988887777 + RangedPortsBySource + sg-76abc467 + Group A + + + tcp + 6000 + 7000 + + + + + + + +` + +// A dump which includes groups within ip permissions. +var DescribeSecurityGroupsDump = ` + + + 87b92b57-cc6e-48b2-943f-f6f0e5c9f46c + + + 12345 + default + default group + + + icmp + -1 + -1 + + + 12345 + default + sg-67ad940e + + + + + + tcp + 0 + 65535 + + + 12345 + other + sg-76abc467 + + + + + + + + +` + +// http://goo.gl/QJJDO +var DeleteSecurityGroupExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/u2sDJ +var AuthorizeSecurityGroupIngressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/u2sDJ +var AuthorizeSecurityGroupEgressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/Mz7xr +var RevokeSecurityGroupIngressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/Vmkqc +var CreateTagsExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/awKeF +var StartInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + i-10a64379 + + 0 + pending + + + 80 + stopped + + + + +` + +// http://goo.gl/436dJ +var StopInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + + + i-10a64379 + + 64 + stopping + + + 16 + running + + + + +` + +// http://goo.gl/baoUf +var RebootInstancesExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/9rprDN +var AllocateAddressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + 198.51.100.1 + vpc + eipalloc-5723d13e + +` + +// http://goo.gl/3Q0oCc +var ReleaseAddressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/uOSQE +var AssociateAddressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + eipassoc-fc5ca095 + +` + +// http://goo.gl/LrOa0 +var DisassociateAddressExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +// http://goo.gl/icuXh5 +var ModifyInstanceExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` + +var CreateVpcExample = ` + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + vpc-1a2b3c4d + pending + 10.0.0.0/16 + dopt-1a2b3c4d2 + default + + + +` + +var DescribeVpcsExample = ` + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + + vpc-1a2b3c4d + available + 10.0.0.0/23 + dopt-7a8b9c2d + default + false + + + + +` + +var CreateSubnetExample = ` + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + + subnet-9d4a7b6c + pending + vpc-1a2b3c4d + 10.0.1.0/24 + 251 + us-east-1a + + + +` + +// http://goo.gl/r6ZCPm +var ResetImageAttributeExample = ` + + 59dbff89-35bd-4eac-99ed-be587EXAMPLE + true + +` diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign.go new file mode 100644 index 00000000000..bffc3c7e930 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign.go @@ -0,0 +1,45 @@ +package ec2 + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "github.com/mitchellh/goamz/aws" + "sort" + "strings" +) + +// ---------------------------------------------------------------------------- +// EC2 signing (http://goo.gl/fQmAN) + +var b64 = base64.StdEncoding + +func sign(auth aws.Auth, method, path string, params map[string]string, host string) { + params["AWSAccessKeyId"] = auth.AccessKey + params["SignatureVersion"] = "2" + params["SignatureMethod"] = "HmacSHA256" + if auth.Token != "" { + params["SecurityToken"] = auth.Token + } + + // AWS specifies that the parameters in a signed request must + // be provided in the natural order of the keys. This is distinct + // from the natural order of the encoded value of key=value. + // Percent and equals affect the sorting order. + var keys, sarray []string + for k, _ := range params { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + sarray = append(sarray, aws.Encode(k)+"="+aws.Encode(params[k])) + } + joined := strings.Join(sarray, "&") + payload := method + "\n" + host + "\n" + path + "\n" + joined + hash := hmac.New(sha256.New, []byte(auth.SecretKey)) + hash.Write([]byte(payload)) + signature := make([]byte, b64.EncodedLen(hash.Size())) + b64.Encode(signature, hash.Sum(nil)) + + params["Signature"] = string(signature) +} diff --git a/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign_test.go b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign_test.go new file mode 100644 index 00000000000..86d203e78c6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mitchellh/goamz/ec2/sign_test.go @@ -0,0 +1,68 @@ +package ec2_test + +import ( + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + . "github.com/motain/gocheck" +) + +// EC2 ReST authentication docs: http://goo.gl/fQmAN + +var testAuth = aws.Auth{"user", "secret", ""} + +func (s *S) TestBasicSignature(c *C) { + params := map[string]string{} + ec2.Sign(testAuth, "GET", "/path", params, "localhost") + c.Assert(params["SignatureVersion"], Equals, "2") + c.Assert(params["SignatureMethod"], Equals, "HmacSHA256") + expected := "6lSe5QyXum0jMVc7cOUz32/52ZnL7N5RyKRk/09yiK4=" + c.Assert(params["Signature"], Equals, expected) +} + +func (s *S) TestParamSignature(c *C) { + params := map[string]string{ + "param1": "value1", + "param2": "value2", + "param3": "value3", + } + ec2.Sign(testAuth, "GET", "/path", params, "localhost") + expected := "XWOR4+0lmK8bD8CGDGZ4kfuSPbb2JibLJiCl/OPu1oU=" + c.Assert(params["Signature"], Equals, expected) +} + +func (s *S) TestManyParams(c *C) { + params := map[string]string{ + "param1": "value10", + "param2": "value2", + "param3": "value3", + "param4": "value4", + "param5": "value5", + "param6": "value6", + "param7": "value7", + "param8": "value8", + "param9": "value9", + "param10": "value1", + } + ec2.Sign(testAuth, "GET", "/path", params, "localhost") + expected := "di0sjxIvezUgQ1SIL6i+C/H8lL+U0CQ9frLIak8jkVg=" + c.Assert(params["Signature"], Equals, expected) +} + +func (s *S) TestEscaping(c *C) { + params := map[string]string{"Nonce": "+ +"} + ec2.Sign(testAuth, "GET", "/path", params, "localhost") + c.Assert(params["Nonce"], Equals, "+ +") + expected := "bqffDELReIqwjg/W0DnsnVUmfLK4wXVLO4/LuG+1VFA=" + c.Assert(params["Signature"], Equals, expected) +} + +func (s *S) TestSignatureExample1(c *C) { + params := map[string]string{ + "Timestamp": "2009-02-01T12:53:20+00:00", + "Version": "2007-11-07", + "Action": "ListDomains", + } + ec2.Sign(aws.Auth{"access", "secret", ""}, "GET", "/", params, "sdb.amazonaws.com") + expected := "okj96/5ucWBSc1uR2zXVfm6mDHtgfNv657rRtt/aunQ=" + c.Assert(params["Signature"], Equals, expected) +} diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/LICENSE b/Godeps/_workspace/src/github.com/vaughan0/go-ini/LICENSE new file mode 100644 index 00000000000..968b45384d0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/LICENSE @@ -0,0 +1,14 @@ +Copyright (c) 2013 Vaughan Newton + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/README.md b/Godeps/_workspace/src/github.com/vaughan0/go-ini/README.md new file mode 100644 index 00000000000..d5cd4e74b00 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/README.md @@ -0,0 +1,70 @@ +go-ini +====== + +INI parsing library for Go (golang). + +View the API documentation [here](http://godoc.org/github.com/vaughan0/go-ini). + +Usage +----- + +Parse an INI file: + +```go +import "github.com/vaughan0/go-ini" + +file, err := ini.LoadFile("myfile.ini") +``` + +Get data from the parsed file: + +```go +name, ok := file.Get("person", "name") +if !ok { + panic("'name' variable missing from 'person' section") +} +``` + +Iterate through values in a section: + +```go +for key, value := range file["mysection"] { + fmt.Printf("%s => %s\n", key, value) +} +``` + +Iterate through sections in a file: + +```go +for name, section := range file { + fmt.Printf("Section name: %s\n", name) +} +``` + +File Format +----------- + +INI files are parsed by go-ini line-by-line. Each line may be one of the following: + + * A section definition: [section-name] + * A property: key = value + * A comment: #blahblah _or_ ;blahblah + * Blank. The line will be ignored. + +Properties defined before any section headers are placed in the default section, which has +the empty string as it's key. + +Example: + +```ini +# I am a comment +; So am I! + +[apples] +colour = red or green +shape = applish + +[oranges] +shape = square +colour = blue +``` diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini.go b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini.go new file mode 100644 index 00000000000..81aeb32f8b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini.go @@ -0,0 +1,123 @@ +// Package ini provides functions for parsing INI configuration files. +package ini + +import ( + "bufio" + "fmt" + "io" + "os" + "regexp" + "strings" +) + +var ( + sectionRegex = regexp.MustCompile(`^\[(.*)\]$`) + assignRegex = regexp.MustCompile(`^([^=]+)=(.*)$`) +) + +// ErrSyntax is returned when there is a syntax error in an INI file. +type ErrSyntax struct { + Line int + Source string // The contents of the erroneous line, without leading or trailing whitespace +} + +func (e ErrSyntax) Error() string { + return fmt.Sprintf("invalid INI syntax on line %d: %s", e.Line, e.Source) +} + +// A File represents a parsed INI file. +type File map[string]Section + +// A Section represents a single section of an INI file. +type Section map[string]string + +// Returns a named Section. A Section will be created if one does not already exist for the given name. +func (f File) Section(name string) Section { + section := f[name] + if section == nil { + section = make(Section) + f[name] = section + } + return section +} + +// Looks up a value for a key in a section and returns that value, along with a boolean result similar to a map lookup. +func (f File) Get(section, key string) (value string, ok bool) { + if s := f[section]; s != nil { + value, ok = s[key] + } + return +} + +// Loads INI data from a reader and stores the data in the File. +func (f File) Load(in io.Reader) (err error) { + bufin, ok := in.(*bufio.Reader) + if !ok { + bufin = bufio.NewReader(in) + } + return parseFile(bufin, f) +} + +// Loads INI data from a named file and stores the data in the File. +func (f File) LoadFile(file string) (err error) { + in, err := os.Open(file) + if err != nil { + return + } + defer in.Close() + return f.Load(in) +} + +func parseFile(in *bufio.Reader, file File) (err error) { + section := "" + lineNum := 0 + for done := false; !done; { + var line string + if line, err = in.ReadString('\n'); err != nil { + if err == io.EOF { + done = true + } else { + return + } + } + lineNum++ + line = strings.TrimSpace(line) + if len(line) == 0 { + // Skip blank lines + continue + } + if line[0] == ';' || line[0] == '#' { + // Skip comments + continue + } + + if groups := assignRegex.FindStringSubmatch(line); groups != nil { + key, val := groups[1], groups[2] + key, val = strings.TrimSpace(key), strings.TrimSpace(val) + file.Section(section)[key] = val + } else if groups := sectionRegex.FindStringSubmatch(line); groups != nil { + name := strings.TrimSpace(groups[1]) + section = name + // Create the section if it does not exist + file.Section(section) + } else { + return ErrSyntax{lineNum, line} + } + + } + return nil +} + +// Loads and returns a File from a reader. +func Load(in io.Reader) (File, error) { + file := make(File) + err := file.Load(in) + return file, err +} + +// Loads and returns an INI File from a file on disk. +func LoadFile(filename string) (File, error) { + file := make(File) + err := file.LoadFile(filename) + return file, err +} diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_linux_test.go b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_linux_test.go new file mode 100644 index 00000000000..38a6f0004cf --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_linux_test.go @@ -0,0 +1,43 @@ +package ini + +import ( + "reflect" + "syscall" + "testing" +) + +func TestLoadFile(t *testing.T) { + originalOpenFiles := numFilesOpen(t) + + file, err := LoadFile("test.ini") + if err != nil { + t.Fatal(err) + } + + if originalOpenFiles != numFilesOpen(t) { + t.Error("test.ini not closed") + } + + if !reflect.DeepEqual(file, File{"default": {"stuff": "things"}}) { + t.Error("file not read correctly") + } +} + +func numFilesOpen(t *testing.T) (num uint64) { + var rlimit syscall.Rlimit + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimit) + if err != nil { + t.Fatal(err) + } + maxFds := int(rlimit.Cur) + + var stat syscall.Stat_t + for i := 0; i < maxFds; i++ { + if syscall.Fstat(i, &stat) == nil { + num++ + } else { + return + } + } + return +} diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_test.go b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_test.go new file mode 100644 index 00000000000..06a4d05eaf0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/ini_test.go @@ -0,0 +1,89 @@ +package ini + +import ( + "reflect" + "strings" + "testing" +) + +func TestLoad(t *testing.T) { + src := ` + # Comments are ignored + + herp = derp + + [foo] + hello=world + whitespace should = not matter + ; sneaky semicolon-style comment + multiple = equals = signs + + [bar] + this = that` + + file, err := Load(strings.NewReader(src)) + if err != nil { + t.Fatal(err) + } + check := func(section, key, expect string) { + if value, _ := file.Get(section, key); value != expect { + t.Errorf("Get(%q, %q): expected %q, got %q", section, key, expect, value) + } + } + + check("", "herp", "derp") + check("foo", "hello", "world") + check("foo", "whitespace should", "not matter") + check("foo", "multiple", "equals = signs") + check("bar", "this", "that") +} + +func TestSyntaxError(t *testing.T) { + src := ` + # Line 2 + [foo] + bar = baz + # Here's an error on line 6: + wut? + herp = derp` + _, err := Load(strings.NewReader(src)) + t.Logf("%T: %v", err, err) + if err == nil { + t.Fatal("expected an error, got nil") + } + syntaxErr, ok := err.(ErrSyntax) + if !ok { + t.Fatal("expected an error of type ErrSyntax") + } + if syntaxErr.Line != 6 { + t.Fatal("incorrect line number") + } + if syntaxErr.Source != "wut?" { + t.Fatal("incorrect source") + } +} + +func TestDefinedSectionBehaviour(t *testing.T) { + check := func(src string, expect File) { + file, err := Load(strings.NewReader(src)) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(file, expect) { + t.Errorf("expected %v, got %v", expect, file) + } + } + // No sections for an empty file + check("", File{}) + // Default section only if there are actually values for it + check("foo=bar", File{"": {"foo": "bar"}}) + // User-defined sections should always be present, even if empty + check("[a]\n[b]\nfoo=bar", File{ + "a": {}, + "b": {"foo": "bar"}, + }) + check("foo=bar\n[a]\nthis=that", File{ + "": {"foo": "bar"}, + "a": {"this": "that"}, + }) +} diff --git a/Godeps/_workspace/src/github.com/vaughan0/go-ini/test.ini b/Godeps/_workspace/src/github.com/vaughan0/go-ini/test.ini new file mode 100644 index 00000000000..d13c999e254 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vaughan0/go-ini/test.ini @@ -0,0 +1,2 @@ +[default] +stuff = things From b548465adf11141799735ca3d4367f87dea0f101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ragnar=20Dahl=C3=A9n?= Date: Tue, 9 Sep 2014 22:25:35 +0100 Subject: [PATCH 2/2] Initial impl. of cloud provider interface for AWS --- cmd/apiserver/plugins.go | 1 + pkg/cloudprovider/aws/aws.go | 181 ++++++++++++++++++++++++++++++ pkg/cloudprovider/aws/aws_test.go | 157 ++++++++++++++++++++++++++ 3 files changed, 339 insertions(+) create mode 100644 pkg/cloudprovider/aws/aws.go create mode 100644 pkg/cloudprovider/aws/aws_test.go diff --git a/cmd/apiserver/plugins.go b/cmd/apiserver/plugins.go index 266b57d108a..3eddf5c0545 100644 --- a/cmd/apiserver/plugins.go +++ b/cmd/apiserver/plugins.go @@ -20,6 +20,7 @@ package main // This should probably be part of some configuration fed into the build for a // given binary target. import ( + _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/aws" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/gce" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/vagrant" _ "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider/ovirt" diff --git a/pkg/cloudprovider/aws/aws.go b/pkg/cloudprovider/aws/aws.go new file mode 100644 index 00000000000..438b263077f --- /dev/null +++ b/pkg/cloudprovider/aws/aws.go @@ -0,0 +1,181 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aws_cloud + +import ( + "fmt" + "io" + "net" + "regexp" + + "code.google.com/p/gcfg" + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" +) + +type EC2 interface { + Instances(instIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) +} + +// AWSCloud is an implementation of Interface, TCPLoadBalancer and Instances for Amazon Web Services. +type AWSCloud struct { + ec2 EC2 + cfg *AWSCloudConfig +} + +type AWSCloudConfig struct { + Global struct { + Region string + } +} + +type AuthFunc func() (auth aws.Auth, err error) + +func init() { + cloudprovider.RegisterCloudProvider("aws", func(config io.Reader) (cloudprovider.Interface, error) { + return newAWSCloud(config, getAuth) + }) +} + +func getAuth() (auth aws.Auth, err error) { + return aws.GetAuth("", "") +} + +// readAWSCloudConfig reads an instance of AWSCloudConfig from config reader. +func readAWSCloudConfig(config io.Reader) (*AWSCloudConfig, error) { + if config == nil { + return nil, fmt.Errorf("No AWS cloud provider config file given") + } + + var cfg AWSCloudConfig + err := gcfg.ReadInto(&cfg, config) + if err != nil { + return nil, err + } + + if cfg.Global.Region == "" { + return nil, fmt.Errorf("No region specified in configuration file") + } + + return &cfg, nil +} + +// newAWSCloud creates a new instance of AWSCloud. +func newAWSCloud(config io.Reader, authFunc AuthFunc) (*AWSCloud, error) { + cfg, err := readAWSCloudConfig(config) + if err != nil { + return nil, fmt.Errorf("Unable to read AWS cloud provider config file: %s", err) + } + + auth, err := authFunc() + if err != nil { + return nil, err + } + + region, ok := aws.Regions[cfg.Global.Region] + if !ok { + return nil, fmt.Errorf("Not a valid AWS region: %s", cfg.Global.Region) + } + + ec2 := ec2.New(auth, region) + return &AWSCloud{ + ec2: ec2, + cfg: cfg, + }, nil +} + +// TCPLoadBalancer returns an implementation of TCPLoadBalancer for Amazon Web Services. +func (aws *AWSCloud) TCPLoadBalancer() (cloudprovider.TCPLoadBalancer, bool) { + return nil, false +} + +// Instances returns an implementation of Instances for Amazon Web Services. +func (aws *AWSCloud) Instances() (cloudprovider.Instances, bool) { + return aws, true +} + +// Zones returns an implementation of Zones for Amazon Web Services. +func (aws *AWSCloud) Zones() (cloudprovider.Zones, bool) { + return nil, false +} + +// IPAddress is an implementation of Instances.IPAddress. +func (aws *AWSCloud) IPAddress(name string) (net.IP, error) { + f := ec2.NewFilter() + f.Add("private-dns-name", name) + + resp, err := aws.ec2.Instances(nil, f) + if err != nil { + return nil, err + } + if len(resp.Reservations) == 0 { + return nil, fmt.Errorf("No reservations found for host: %s", name) + } + if len(resp.Reservations) > 1 { + return nil, fmt.Errorf("Multiple reservations found for host: %s", name) + } + if len(resp.Reservations[0].Instances) == 0 { + return nil, fmt.Errorf("No instances found for host: %s", name) + } + if len(resp.Reservations[0].Instances) > 1 { + return nil, fmt.Errorf("Multiple instances found for host: %s", name) + } + + ipAddress := resp.Reservations[0].Instances[0].PrivateIpAddress + ip := net.ParseIP(ipAddress) + if ip == nil { + return nil, fmt.Errorf("Invalid network IP: %s", ipAddress) + } + return ip, nil +} + +// Return a list of instances matching regex string. +func (aws *AWSCloud) getInstancesByRegex(regex string) ([]string, error) { + resp, err := aws.ec2.Instances(nil, nil) + if err != nil { + return []string{}, err + } + if resp == nil { + return []string{}, fmt.Errorf("No InstanceResp returned") + } + + re, err := regexp.Compile(regex) + if err != nil { + return []string{}, err + } + + instances := []string{} + for _, reservation := range resp.Reservations { + for _, instance := range reservation.Instances { + for _, tag := range instance.Tags { + if tag.Key == "Name" && re.MatchString(tag.Value) { + instances = append(instances, instance.PrivateDNSName) + break + } + } + } + } + return instances, nil +} + +// List is an implementation of Instances.List. +func (aws *AWSCloud) List(filter string) ([]string, error) { + // TODO: Should really use tag query. No need to go regexp. + return aws.getInstancesByRegex(filter) +} diff --git a/pkg/cloudprovider/aws/aws_test.go b/pkg/cloudprovider/aws/aws_test.go new file mode 100644 index 00000000000..bc026db3d01 --- /dev/null +++ b/pkg/cloudprovider/aws/aws_test.go @@ -0,0 +1,157 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package aws_cloud + +import ( + "reflect" + "strings" + "testing" + + "github.com/mitchellh/goamz/aws" + "github.com/mitchellh/goamz/ec2" +) + +func TestReadAWSCloudConfig(t *testing.T) { + _, err1 := readAWSCloudConfig(nil) + if err1 == nil { + t.Errorf("Should error when no config reader is given") + } + + _, err2 := readAWSCloudConfig(strings.NewReader("")) + if err2 == nil { + t.Errorf("Should error when config is empty") + } + + _, err3 := readAWSCloudConfig(strings.NewReader("[global]\n")) + if err3 == nil { + t.Errorf("Should error when no region is specified") + } + + cfg, err4 := readAWSCloudConfig(strings.NewReader("[global]\nregion = eu-west-1")) + if err4 != nil { + t.Errorf("Should succeed when a region is specified: %s", err4) + } + if cfg.Global.Region != "eu-west-1" { + t.Errorf("Should read region from config") + } +} + +func TestNewAWSCloud(t *testing.T) { + fakeAuthFunc := func() (auth aws.Auth, err error) { + return aws.Auth{"", "", ""}, nil + } + + _, err1 := newAWSCloud(nil, fakeAuthFunc) + if err1 == nil { + t.Errorf("Should error when no config reader is given") + } + + _, err2 := newAWSCloud(strings.NewReader( + "[global]\nregion = blahonga"), + fakeAuthFunc) + if err2 == nil { + t.Errorf("Should error when config specifies invalid region") + } + + _, err3 := newAWSCloud( + strings.NewReader("[global]\nregion = eu-west-1"), + fakeAuthFunc) + if err3 != nil { + t.Errorf("Should succeed when a valid region is specified: %s", err3) + } +} + +type FakeEC2 struct { + instances func(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) +} + +func (ec2 *FakeEC2) Instances(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) { + return ec2.instances(instanceIds, filter) +} + +func mockInstancesResp(instances []ec2.Instance) (aws *AWSCloud) { + return &AWSCloud{ + &FakeEC2{ + func(instanceIds []string, filter *ec2.Filter) (resp *ec2.InstancesResp, err error) { + return &ec2.InstancesResp{"", + []ec2.Reservation{ + ec2.Reservation{"", "", "", nil, instances}}}, nil + }}, + nil} +} + +func TestList(t *testing.T) { + instances := make([]ec2.Instance, 4) + instances[0].Tags = []ec2.Tag{ec2.Tag{"Name", "foo"}} + instances[0].PrivateDNSName = "instance1" + instances[1].Tags = []ec2.Tag{ec2.Tag{"Name", "bar"}} + instances[1].PrivateDNSName = "instance2" + instances[2].Tags = []ec2.Tag{ec2.Tag{"Name", "baz"}} + instances[2].PrivateDNSName = "instance3" + instances[3].Tags = []ec2.Tag{ec2.Tag{"Name", "quux"}} + instances[3].PrivateDNSName = "instance4" + + aws := mockInstancesResp(instances) + + table := []struct { + input string + expect []string + }{ + {"blahonga", []string{}}, + {"quux", []string{"instance4"}}, + {"a", []string{"instance2", "instance3"}}, + } + + for _, item := range table { + result, err := aws.List(item.input) + if err != nil { + t.Errorf("Expected call with %v to succeed, failed with %s", item.input, err) + } + if e, a := item.expect, result; !reflect.DeepEqual(e, a) { + t.Errorf("Expected %v, got %v", e, a) + } + } +} + +func TestIPAddress(t *testing.T) { + instances := make([]ec2.Instance, 2) + instances[0].PrivateDNSName = "instance1" + instances[0].PrivateIpAddress = "192.168.0.1" + instances[1].PrivateDNSName = "instance2" + instances[1].PrivateIpAddress = "192.168.0.2" + + aws1 := mockInstancesResp([]ec2.Instance{}) + _, err1 := aws1.IPAddress("instance") + if err1 == nil { + t.Errorf("Should error when no instance found") + } + + aws2 := mockInstancesResp(instances) + _, err2 := aws2.IPAddress("instance1") + if err2 == nil { + t.Errorf("Should error when multiple instances found") + } + + aws3 := mockInstancesResp(instances[0:1]) + ip3, err3 := aws3.IPAddress("instance1") + if err3 != nil { + t.Errorf("Should not error when instance found") + } + if e, a := instances[0].PrivateIpAddress, ip3.String(); e != a { + t.Errorf("Expected %v, got %v", e, a) + } +}