From 9b577aabb631c83104828d4a9ff444c494320352 Mon Sep 17 00:00:00 2001 From: Qicong Chen Date: Wed, 1 Apr 2015 19:00:04 -0400 Subject: [PATCH 1/2] Supports #226 Signed-off-by: Qicong Chen Fix logging problem Signed-off-by: Qicong Chen --- drivers/amazonec2/amazonec2.go | 108 ++++++++++++------ drivers/amazonec2/amazonec2_test.go | 40 ++++--- .../amz/describe_spot_instance_requests.go | 11 ++ drivers/amazonec2/amz/ec2.go | 83 ++++++++++++++ 4 files changed, 188 insertions(+), 54 deletions(-) create mode 100644 drivers/amazonec2/amz/describe_spot_instance_requests.go diff --git a/drivers/amazonec2/amazonec2.go b/drivers/amazonec2/amazonec2.go index 371fad10f1..ac64ad0333 100644 --- a/drivers/amazonec2/amazonec2.go +++ b/drivers/amazonec2/amazonec2.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strconv" "strings" + "time" log "github.com/Sirupsen/logrus" "github.com/codegangsta/cli" @@ -36,36 +37,38 @@ var ( ) type Driver struct { - Id string - AccessKey string - SecretKey string - SessionToken string - Region string - AMI string - SSHKeyID int - SSHUser string - SSHPort int - KeyName string - InstanceId string - InstanceType string - IPAddress string - PrivateIPAddress string - MachineName string - SecurityGroupId string - SecurityGroupName string - ReservationId string - RootSize int64 - IamInstanceProfile string - VpcId string - SubnetId string - Zone string - CaCertPath string - PrivateKeyPath string - SwarmMaster bool - SwarmHost string - SwarmDiscovery string - storePath string - keyPath string + Id string + AccessKey string + SecretKey string + SessionToken string + Region string + AMI string + SSHKeyID int + SSHUser string + SSHPort int + KeyName string + InstanceId string + InstanceType string + IPAddress string + PrivateIPAddress string + MachineName string + SecurityGroupId string + SecurityGroupName string + ReservationId string + RootSize int64 + IamInstanceProfile string + VpcId string + SubnetId string + Zone string + CaCertPath string + PrivateKeyPath string + SwarmMaster bool + SwarmHost string + SwarmDiscovery string + storePath string + keyPath string + RequestSpotInstance bool + SpotPrice string } func init() { @@ -152,6 +155,15 @@ func GetCreateFlags() []cli.Flag { Value: "ubuntu", EnvVar: "AWS_SSH_USER", }, + cli.BoolFlag{ + Name: "amazonec2-request-spot-instance", + Usage: "Set this flag to request spot instance", + }, + cli.StringFlag{ + Name: "amazonec2-spot-price", + Usage: "AWS spot instance bid price (in dollar)", + Value: "0.50", + }, } } @@ -194,6 +206,8 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { d.SessionToken = flags.String("amazonec2-session-token") d.Region = region d.AMI = image + d.RequestSpotInstance = flags.Bool("amazonec2-request-spot-instance") + d.SpotPrice = flags.String("amazonec2-spot-price") d.InstanceType = flags.String("amazonec2-instance-type") d.VpcId = flags.String("amazonec2-vpc-id") d.SubnetId = flags.String("amazonec2-subnet-id") @@ -335,10 +349,34 @@ func (d *Driver) Create() error { } log.Debugf("launching instance in subnet %s", d.SubnetId) - instance, err := d.getClient().RunInstance(d.AMI, d.InstanceType, d.Zone, 1, 1, d.SecurityGroupId, d.KeyName, d.SubnetId, bdm, d.IamInstanceProfile) - - if err != nil { - return fmt.Errorf("Error launching instance: %s", err) + var instance amz.EC2Instance + if d.RequestSpotInstance { + spotInstanceRequestId, err := d.getClient().RequestSpotInstances(d.AMI, d.InstanceType, d.Zone, 1, d.SecurityGroupId, d.KeyName, d.SubnetId, bdm, d.IamInstanceProfile, d.SpotPrice) + if err != nil { + return fmt.Errorf("Error request spot instance: %s", err) + } + var instanceId string + var spotInstanceRequestStatus string + log.Debug("waiting for spot instance fulfillment...") + // check until fulfilled + for instanceId == "" { + time.Sleep(time.Second * 5) + spotInstanceRequestStatus, instanceId, err = d.getClient().DescribeSpotInstanceRequests(spotInstanceRequestId) + if err != nil { + return fmt.Errorf("Error describe spot instance request: %s", err) + } + log.Debugf("spot instance request status: %s", spotInstanceRequestStatus) + } + instance, err = d.getClient().GetInstance(instanceId) + if err != nil { + return fmt.Errorf("Error get instance: %s", err) + } + } else { + inst, err := d.getClient().RunInstance(d.AMI, d.InstanceType, d.Zone, 1, 1, d.SecurityGroupId, d.KeyName, d.SubnetId, bdm, d.IamInstanceProfile) + if err != nil { + return fmt.Errorf("Error launching instance: %s", err) + } + instance = inst } d.InstanceId = instance.InstanceId @@ -371,7 +409,7 @@ func (d *Driver) Create() error { "Name": d.MachineName, } - if err = d.getClient().CreateTags(d.InstanceId, tags); err != nil { + if err := d.getClient().CreateTags(d.InstanceId, tags); err != nil { return err } diff --git a/drivers/amazonec2/amazonec2_test.go b/drivers/amazonec2/amazonec2_test.go index f108a40f63..a2fcf2f404 100644 --- a/drivers/amazonec2/amazonec2_test.go +++ b/drivers/amazonec2/amazonec2_test.go @@ -59,25 +59,27 @@ func getTestStorePath() (string, error) { func getDefaultTestDriverFlags() *DriverOptionsMock { return &DriverOptionsMock{ Data: map[string]interface{}{ - "name": "test", - "url": "unix:///var/run/docker.sock", - "swarm": false, - "swarm-host": "", - "swarm-master": false, - "swarm-discovery": "", - "amazonec2-ami": "ami-12345", - "amazonec2-access-key": "abcdefg", - "amazonec2-secret-key": "12345", - "amazonec2-session-token": "", - "amazonec2-instance-type": "t1.micro", - "amazonec2-vpc-id": "vpc-12345", - "amazonec2-subnet-id": "subnet-12345", - "amazonec2-security-group": "docker-machine-test", - "amazonec2-region": "us-east-1", - "amazonec2-zone": "e", - "amazonec2-root-size": 10, - "amazonec2-iam-instance-profile": "", - "amazonec2-ssh-user": "ubuntu", + "name": "test", + "url": "unix:///var/run/docker.sock", + "swarm": false, + "swarm-host": "", + "swarm-master": false, + "swarm-discovery": "", + "amazonec2-ami": "ami-12345", + "amazonec2-access-key": "abcdefg", + "amazonec2-secret-key": "12345", + "amazonec2-session-token": "", + "amazonec2-instance-type": "t1.micro", + "amazonec2-vpc-id": "vpc-12345", + "amazonec2-subnet-id": "subnet-12345", + "amazonec2-security-group": "docker-machine-test", + "amazonec2-region": "us-east-1", + "amazonec2-zone": "e", + "amazonec2-root-size": 10, + "amazonec2-iam-instance-profile": "", + "amazonec2-ssh-user": "ubuntu", + "amazonec2-request-spot-instance": false, + "amazonec2-spot-price": "", }, } } diff --git a/drivers/amazonec2/amz/describe_spot_instance_requests.go b/drivers/amazonec2/amz/describe_spot_instance_requests.go new file mode 100644 index 0000000000..4682c562c5 --- /dev/null +++ b/drivers/amazonec2/amz/describe_spot_instance_requests.go @@ -0,0 +1,11 @@ +package amz + +type DescribeSpotInstanceRequestsResponse struct { + RequestId string `xml:"requestId"` + SpotInstanceRequestSet []struct { + Status struct { + Code string `xml:"code"` + } `xml:"status"` + InstanceId string `xml:"instanceId"` + } `xml:"spotInstanceRequestSet>item"` +} diff --git a/drivers/amazonec2/amz/ec2.go b/drivers/amazonec2/amz/ec2.go index 3bcde2a78f..3250dcecb0 100644 --- a/drivers/amazonec2/amz/ec2.go +++ b/drivers/amazonec2/amz/ec2.go @@ -103,6 +103,14 @@ type ( OwnerId string `xml:"ownerId"` Instances []EC2Instance `xml:"instancesSet>item"` } + + RequestSpotInstancesResponse struct { + RequestId string `xml:"requestId"` + SpotInstanceRequestSet []struct { + SpotInstanceRequestId string `xml:"spotInstanceRequestId"` + State string `xml:"state"` + } `xml:"spotInstanceRequestSet>item"` + } ) func newAwsApiResponseError(r http.Response) error { @@ -217,6 +225,81 @@ func (e *EC2) RunInstance(amiId string, instanceType string, zone string, minCou return instance.info, nil } +func (e *EC2) RequestSpotInstances(amiId string, instanceType string, zone string, instanceCount int, securityGroup string, keyName string, subnetId string, bdm *BlockDeviceMapping, role string, spotPrice string) (string, error) { + v := url.Values{} + v.Set("Action", "RequestSpotInstances") + v.Set("LaunchSpecification.ImageId", amiId) + v.Set("LaunchSpecification.Placement.AvailabilityZone", e.Region+zone) + v.Set("InstanceCount", strconv.Itoa(instanceCount)) + v.Set("SpotPrice", spotPrice) + v.Set("LaunchSpecification.KeyName", keyName) + v.Set("LaunchSpecification.InstanceType", instanceType) + v.Set("LaunchSpecification.NetworkInterface.0.DeviceIndex", "0") + v.Set("LaunchSpecification.NetworkInterface.0.SecurityGroupId.0", securityGroup) + v.Set("LaunchSpecification.NetworkInterface.0.SubnetId", subnetId) + v.Set("LaunchSpecification.NetworkInterface.0.AssociatePublicIpAddress", "1") + + if len(role) > 0 { + v.Set("LaunchSpecification.IamInstanceProfile.Name", role) + } + + if bdm != nil { + v.Set("LaunchSpecification.BlockDeviceMapping.0.DeviceName", bdm.DeviceName) + v.Set("LaunchSpecification.BlockDeviceMapping.0.VirtualName", bdm.VirtualName) + v.Set("LaunchSpecification.BlockDeviceMapping.0.Ebs.VolumeSize", strconv.FormatInt(bdm.VolumeSize, 10)) + v.Set("LaunchSpecification.BlockDeviceMapping.0.Ebs.VolumeType", bdm.VolumeType) + deleteOnTerm := 0 + if bdm.DeleteOnTermination { + deleteOnTerm = 1 + } + v.Set("LaunchSpecification.BlockDeviceMapping.0.Ebs.DeleteOnTermination", strconv.Itoa(deleteOnTerm)) + } + + resp, err := e.awsApiCall(v) + + if err != nil { + return "", newAwsApiCallError(err) + } + defer resp.Body.Close() + contents, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", fmt.Errorf("Error reading AWS response body") + } + unmarshalledResponse := RequestSpotInstancesResponse{} + err = xml.Unmarshal(contents, &unmarshalledResponse) + if err != nil { + return "", fmt.Errorf("Error unmarshalling AWS response XML: %s", err) + } + return unmarshalledResponse.SpotInstanceRequestSet[0].SpotInstanceRequestId, nil +} + +func (e *EC2) DescribeSpotInstanceRequests(spotInstanceRequestId string) (string, string, error) { + v := url.Values{} + v.Set("Action", "DescribeSpotInstanceRequests") + v.Set("SpotInstanceRequestId.0", spotInstanceRequestId) + + resp, err := e.awsApiCall(v) + + if err != nil { + return "", "", newAwsApiCallError(err) + } + defer resp.Body.Close() + + contents, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", "", fmt.Errorf("Error reading AWS response body") + } + unmarshalledResponse := DescribeSpotInstanceRequestsResponse{} + err = xml.Unmarshal(contents, &unmarshalledResponse) + if err != nil { + return "", "", fmt.Errorf("Error unmarshalling AWS response XML: %s", err) + } + if code := unmarshalledResponse.SpotInstanceRequestSet[0].Status.Code; code != "fulfilled" { + return code, "", nil + } + return "fulfilled", unmarshalledResponse.SpotInstanceRequestSet[0].InstanceId, nil +} + func (e *EC2) DeleteKeyPair(name string) error { v := url.Values{} v.Set("Action", "DeleteKeyPair") From 460cc02ea5ed60df820903c9d31377b3ac301671 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Tue, 14 Apr 2015 17:03:38 -0400 Subject: [PATCH 2/2] ec2: update spot instance message Signed-off-by: Evan Hazlett --- drivers/amazonec2/amazonec2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/amazonec2/amazonec2.go b/drivers/amazonec2/amazonec2.go index ac64ad0333..2866f3bfd8 100644 --- a/drivers/amazonec2/amazonec2.go +++ b/drivers/amazonec2/amazonec2.go @@ -357,7 +357,7 @@ func (d *Driver) Create() error { } var instanceId string var spotInstanceRequestStatus string - log.Debug("waiting for spot instance fulfillment...") + log.Info("Waiting for spot instance...") // check until fulfilled for instanceId == "" { time.Sleep(time.Second * 5)