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"
@ -36,36 +37,38 @@ var (
) )
type Driver struct { type Driver struct {
Id string Id string
AccessKey string AccessKey string
SecretKey string SecretKey string
SessionToken string SessionToken string
Region string Region string
AMI string AMI string
SSHKeyID int SSHKeyID int
SSHUser string SSHUser string
SSHPort int SSHPort int
KeyName string KeyName string
InstanceId string InstanceId string
InstanceType string InstanceType string
IPAddress string IPAddress string
PrivateIPAddress string PrivateIPAddress string
MachineName string MachineName string
SecurityGroupId string SecurityGroupId string
SecurityGroupName string SecurityGroupName string
ReservationId string ReservationId string
RootSize int64 RootSize int64
IamInstanceProfile string IamInstanceProfile string
VpcId string VpcId string
SubnetId string SubnetId string
Zone string Zone string
CaCertPath string CaCertPath string
PrivateKeyPath string PrivateKeyPath string
SwarmMaster bool SwarmMaster bool
SwarmHost string SwarmHost string
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,10 +349,34 @@ 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 {
if err != nil { spotInstanceRequestId, err := d.getClient().RequestSpotInstances(d.AMI, d.InstanceType, d.Zone, 1, d.SecurityGroupId, d.KeyName, d.SubnetId, bdm, d.IamInstanceProfile, d.SpotPrice)
return fmt.Errorf("Error launching instance: %s", err) 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 {
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

@ -59,25 +59,27 @@ func getTestStorePath() (string, error) {
func getDefaultTestDriverFlags() *DriverOptionsMock { func getDefaultTestDriverFlags() *DriverOptionsMock {
return &DriverOptionsMock{ return &DriverOptionsMock{
Data: map[string]interface{}{ Data: map[string]interface{}{
"name": "test", "name": "test",
"url": "unix:///var/run/docker.sock", "url": "unix:///var/run/docker.sock",
"swarm": false, "swarm": false,
"swarm-host": "", "swarm-host": "",
"swarm-master": false, "swarm-master": false,
"swarm-discovery": "", "swarm-discovery": "",
"amazonec2-ami": "ami-12345", "amazonec2-ami": "ami-12345",
"amazonec2-access-key": "abcdefg", "amazonec2-access-key": "abcdefg",
"amazonec2-secret-key": "12345", "amazonec2-secret-key": "12345",
"amazonec2-session-token": "", "amazonec2-session-token": "",
"amazonec2-instance-type": "t1.micro", "amazonec2-instance-type": "t1.micro",
"amazonec2-vpc-id": "vpc-12345", "amazonec2-vpc-id": "vpc-12345",
"amazonec2-subnet-id": "subnet-12345", "amazonec2-subnet-id": "subnet-12345",
"amazonec2-security-group": "docker-machine-test", "amazonec2-security-group": "docker-machine-test",
"amazonec2-region": "us-east-1", "amazonec2-region": "us-east-1",
"amazonec2-zone": "e", "amazonec2-zone": "e",
"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")