Merge pull request #1000 from ehazlett/spot-instance

EC2 spot instance support
This commit is contained in:
Evan Hazlett 2015-04-14 17:08:37 -04:00
commit 65f4a24916
4 changed files with 188 additions and 54 deletions

View File

@ -10,6 +10,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"time"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
@ -66,6 +67,8 @@ type Driver struct {
SwarmDiscovery string SwarmDiscovery string
storePath string storePath string
keyPath string keyPath string
RequestSpotInstance bool
SpotPrice string
} }
func init() { func init() {
@ -152,6 +155,15 @@ func GetCreateFlags() []cli.Flag {
Value: "ubuntu", Value: "ubuntu",
EnvVar: "AWS_SSH_USER", 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.SessionToken = flags.String("amazonec2-session-token")
d.Region = region d.Region = region
d.AMI = image 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.InstanceType = flags.String("amazonec2-instance-type")
d.VpcId = flags.String("amazonec2-vpc-id") d.VpcId = flags.String("amazonec2-vpc-id")
d.SubnetId = flags.String("amazonec2-subnet-id") d.SubnetId = flags.String("amazonec2-subnet-id")
@ -335,11 +349,35 @@ func (d *Driver) Create() error {
} }
log.Debugf("launching instance in subnet %s", d.SubnetId) 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) 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.Info("Waiting for spot instance...")
// 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 { if err != nil {
return fmt.Errorf("Error launching instance: %s", err) return fmt.Errorf("Error launching instance: %s", err)
} }
instance = inst
}
d.InstanceId = instance.InstanceId d.InstanceId = instance.InstanceId
@ -371,7 +409,7 @@ func (d *Driver) Create() error {
"Name": d.MachineName, "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 return err
} }

View File

@ -78,6 +78,8 @@ func getDefaultTestDriverFlags() *DriverOptionsMock {
"amazonec2-root-size": 10, "amazonec2-root-size": 10,
"amazonec2-iam-instance-profile": "", "amazonec2-iam-instance-profile": "",
"amazonec2-ssh-user": "ubuntu", "amazonec2-ssh-user": "ubuntu",
"amazonec2-request-spot-instance": false,
"amazonec2-spot-price": "",
}, },
} }
} }

View File

@ -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"`
}

View File

@ -103,6 +103,14 @@ type (
OwnerId string `xml:"ownerId"` OwnerId string `xml:"ownerId"`
Instances []EC2Instance `xml:"instancesSet>item"` 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 { 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 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 { func (e *EC2) DeleteKeyPair(name string) error {
v := url.Values{} v := url.Values{}
v.Set("Action", "DeleteKeyPair") v.Set("Action", "DeleteKeyPair")