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

View File

@ -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": "",
},
}
}

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"`
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")