2014-09-09 21:25:35 +00:00
|
|
|
/*
|
|
|
|
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 (
|
2015-03-06 14:26:39 +00:00
|
|
|
"errors"
|
2014-09-09 21:25:35 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"net"
|
2015-03-06 14:26:39 +00:00
|
|
|
"net/url"
|
2014-09-09 21:25:35 +00:00
|
|
|
"regexp"
|
2015-03-04 21:52:49 +00:00
|
|
|
"strings"
|
2015-03-06 14:26:39 +00:00
|
|
|
"sync"
|
|
|
|
"time"
|
2014-09-09 21:25:35 +00:00
|
|
|
|
|
|
|
"code.google.com/p/gcfg"
|
|
|
|
"github.com/mitchellh/goamz/aws"
|
|
|
|
"github.com/mitchellh/goamz/ec2"
|
|
|
|
|
2014-09-26 23:28:30 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
2015-03-05 18:10:56 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource"
|
2014-09-09 21:25:35 +00:00
|
|
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
|
2015-03-04 21:52:49 +00:00
|
|
|
|
|
|
|
"github.com/golang/glog"
|
2014-09-09 21:25:35 +00:00
|
|
|
)
|
|
|
|
|
2015-03-05 14:05:11 +00:00
|
|
|
// Abstraction over EC2, to allow mocking/other implementations
|
2014-09-09 21:25:35 +00:00
|
|
|
type EC2 interface {
|
2015-03-05 14:05:11 +00:00
|
|
|
// Query EC2 for instances matching the filter
|
|
|
|
Instances(instIds []string, filter *ec2InstanceFilter) (resp *ec2.InstancesResp, err error)
|
2015-03-06 14:26:39 +00:00
|
|
|
|
|
|
|
// Attach a volume to an instance
|
2015-04-09 13:34:16 +00:00
|
|
|
AttachVolume(volumeID string, instanceId string, mountDevice string) (resp *ec2.AttachVolumeResp, err error)
|
2015-03-06 14:26:39 +00:00
|
|
|
// Detach a volume from whatever instance it is attached to
|
|
|
|
// TODO: We should specify the InstanceID and the Device, for safety
|
2015-04-09 13:34:16 +00:00
|
|
|
DetachVolume(volumeID string) (resp *ec2.SimpleResp, err error)
|
2015-03-06 14:26:39 +00:00
|
|
|
// Lists volumes
|
2015-04-09 13:34:16 +00:00
|
|
|
Volumes(volumeIDs []string, filter *ec2.Filter) (resp *ec2.VolumesResp, err error)
|
2015-03-06 14:26:39 +00:00
|
|
|
// Create an EBS volume
|
|
|
|
CreateVolume(request *ec2.CreateVolume) (resp *ec2.CreateVolumeResp, err error)
|
|
|
|
// Delete an EBS volume
|
2015-04-09 13:34:16 +00:00
|
|
|
DeleteVolume(volumeID string) (resp *ec2.SimpleResp, err error)
|
2015-03-26 19:47:49 +00:00
|
|
|
}
|
2015-03-10 04:15:53 +00:00
|
|
|
|
2015-03-26 19:47:49 +00:00
|
|
|
// Abstraction over the AWS metadata service
|
|
|
|
type AWSMetadata interface {
|
2015-03-10 04:15:53 +00:00
|
|
|
// Query the EC2 metadata service (used to discover instance-id etc)
|
|
|
|
GetMetaData(key string) ([]byte, error)
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
|
|
|
|
2015-03-06 14:26:39 +00:00
|
|
|
type VolumeOptions struct {
|
|
|
|
CapacityMB int
|
|
|
|
}
|
|
|
|
|
|
|
|
// Volumes is an interface for managing cloud-provisioned volumes
|
|
|
|
type Volumes interface {
|
|
|
|
// Attach the disk to the specified instance
|
|
|
|
// instanceName can be empty to mean "the instance on which we are running"
|
2015-04-02 18:56:11 +00:00
|
|
|
// Returns the device (e.g. /dev/xvdf) where we attached the volume
|
|
|
|
AttachDisk(instanceName string, volumeName string, readOnly bool) (string, error)
|
2015-03-06 14:26:39 +00:00
|
|
|
// Detach the disk from the specified instance
|
|
|
|
// instanceName can be empty to mean "the instance on which we are running"
|
|
|
|
DetachDisk(instanceName string, volumeName string) error
|
|
|
|
|
|
|
|
// Create a volume with the specified options
|
|
|
|
CreateVolume(volumeOptions *VolumeOptions) (volumeName string, err error)
|
|
|
|
DeleteVolume(volumeName string) error
|
|
|
|
}
|
|
|
|
|
2014-09-09 21:25:35 +00:00
|
|
|
// AWSCloud is an implementation of Interface, TCPLoadBalancer and Instances for Amazon Web Services.
|
|
|
|
type AWSCloud struct {
|
2015-03-10 04:15:53 +00:00
|
|
|
ec2 EC2
|
2015-04-08 13:25:33 +00:00
|
|
|
metadata AWSMetadata
|
2015-03-10 04:15:53 +00:00
|
|
|
cfg *AWSCloudConfig
|
|
|
|
availabilityZone string
|
|
|
|
region aws.Region
|
2015-03-06 14:26:39 +00:00
|
|
|
|
|
|
|
// The AWS instance that we are running on
|
2015-04-09 13:29:57 +00:00
|
|
|
selfAWSInstance *awsInstance
|
2015-04-08 13:25:33 +00:00
|
|
|
|
|
|
|
mutex sync.Mutex
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
type AWSCloudConfig struct {
|
|
|
|
Global struct {
|
2015-03-10 04:15:53 +00:00
|
|
|
// TODO: Is there any use for this? We can get it from the instance metadata service
|
2015-03-26 19:47:49 +00:00
|
|
|
Zone string
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-03-05 14:05:11 +00:00
|
|
|
// Similar to ec2.Filter, but the filter values can be read from tests
|
|
|
|
// (ec2.Filter only has private members)
|
|
|
|
type ec2InstanceFilter struct {
|
|
|
|
PrivateDNSName string
|
|
|
|
}
|
|
|
|
|
|
|
|
// True if the passed instance matches the filter
|
|
|
|
func (f *ec2InstanceFilter) Matches(instance ec2.Instance) bool {
|
|
|
|
if f.PrivateDNSName != "" && instance.PrivateDNSName != f.PrivateDNSName {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// goamzEC2 is an implementation of the EC2 interface, backed by goamz
|
2015-03-06 14:26:39 +00:00
|
|
|
type goamzEC2 struct {
|
2015-03-05 14:05:11 +00:00
|
|
|
ec2 *ec2.EC2
|
|
|
|
}
|
|
|
|
|
|
|
|
// Implementation of EC2.Instances
|
2015-03-06 14:26:39 +00:00
|
|
|
func (self *goamzEC2) Instances(instanceIds []string, filter *ec2InstanceFilter) (resp *ec2.InstancesResp, err error) {
|
2015-03-05 14:05:11 +00:00
|
|
|
var goamzFilter *ec2.Filter
|
|
|
|
if filter != nil {
|
|
|
|
goamzFilter = ec2.NewFilter()
|
|
|
|
if filter.PrivateDNSName != "" {
|
|
|
|
goamzFilter.Add("private-dns-name", filter.PrivateDNSName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return self.ec2.Instances(instanceIds, goamzFilter)
|
|
|
|
}
|
|
|
|
|
2015-03-26 19:47:49 +00:00
|
|
|
type goamzMetadata struct {
|
|
|
|
}
|
|
|
|
|
|
|
|
// Implements AWSMetadata.GetMetaData
|
|
|
|
func (self *goamzMetadata) GetMetaData(key string) ([]byte, error) {
|
2015-03-10 04:15:53 +00:00
|
|
|
v, err := aws.GetMetaData(key)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Error querying AWS metadata for key %s: %v", key, err)
|
|
|
|
}
|
|
|
|
return v, nil
|
|
|
|
}
|
|
|
|
|
2014-09-09 21:25:35 +00:00
|
|
|
type AuthFunc func() (auth aws.Auth, err error)
|
|
|
|
|
2015-04-09 13:34:16 +00:00
|
|
|
func (s *goamzEC2) AttachVolume(volumeID string, instanceId string, device string) (resp *ec2.AttachVolumeResp, err error) {
|
|
|
|
return s.ec2.AttachVolume(volumeID, instanceId, device)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
2015-04-09 13:34:16 +00:00
|
|
|
func (s *goamzEC2) DetachVolume(volumeID string) (resp *ec2.SimpleResp, err error) {
|
|
|
|
return s.ec2.DetachVolume(volumeID)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
2015-04-09 13:34:16 +00:00
|
|
|
func (s *goamzEC2) Volumes(volumeIDs []string, filter *ec2.Filter) (resp *ec2.VolumesResp, err error) {
|
|
|
|
return s.ec2.Volumes(volumeIDs, filter)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *goamzEC2) CreateVolume(request *ec2.CreateVolume) (resp *ec2.CreateVolumeResp, err error) {
|
|
|
|
return s.ec2.CreateVolume(request)
|
|
|
|
}
|
|
|
|
|
2015-04-09 13:34:16 +00:00
|
|
|
func (s *goamzEC2) DeleteVolume(volumeID string) (resp *ec2.SimpleResp, err error) {
|
|
|
|
return s.ec2.DeleteVolume(volumeID)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
2014-09-09 21:25:35 +00:00
|
|
|
func init() {
|
|
|
|
cloudprovider.RegisterCloudProvider("aws", func(config io.Reader) (cloudprovider.Interface, error) {
|
2015-03-26 19:47:49 +00:00
|
|
|
metadata := &goamzMetadata{}
|
2015-04-08 13:35:09 +00:00
|
|
|
return newAWSCloud(config, getAuth, metadata)
|
2014-09-09 21:25:35 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func getAuth() (auth aws.Auth, err error) {
|
|
|
|
return aws.GetAuth("", "")
|
|
|
|
}
|
|
|
|
|
|
|
|
// readAWSCloudConfig reads an instance of AWSCloudConfig from config reader.
|
2015-03-26 19:47:49 +00:00
|
|
|
func readAWSCloudConfig(config io.Reader, metadata AWSMetadata) (*AWSCloudConfig, error) {
|
2014-09-09 21:25:35 +00:00
|
|
|
var cfg AWSCloudConfig
|
2015-04-02 17:14:06 +00:00
|
|
|
var err error
|
|
|
|
|
|
|
|
if config != nil {
|
|
|
|
err = gcfg.ReadInto(&cfg, config)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
|
|
|
|
2015-03-26 19:47:49 +00:00
|
|
|
if cfg.Global.Zone == "" {
|
|
|
|
if metadata != nil {
|
|
|
|
glog.Info("Zone not specified in configuration file; querying AWS metadata service")
|
|
|
|
cfg.Global.Zone, err = getAvailabilityZone(metadata)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if cfg.Global.Zone == "" {
|
|
|
|
return nil, fmt.Errorf("no zone specified in configuration file")
|
|
|
|
}
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &cfg, nil
|
|
|
|
}
|
|
|
|
|
2015-03-26 19:47:49 +00:00
|
|
|
func getAvailabilityZone(metadata AWSMetadata) (string, error) {
|
|
|
|
availabilityZoneBytes, err := metadata.GetMetaData("placement/availability-zone")
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
if availabilityZoneBytes == nil || len(availabilityZoneBytes) == 0 {
|
|
|
|
return "", fmt.Errorf("Unable to determine availability-zone from instance metadata")
|
|
|
|
}
|
|
|
|
return string(availabilityZoneBytes), nil
|
|
|
|
}
|
|
|
|
|
2014-09-09 21:25:35 +00:00
|
|
|
// newAWSCloud creates a new instance of AWSCloud.
|
2015-03-26 17:04:10 +00:00
|
|
|
// authFunc and instanceId are primarily for tests
|
2015-04-08 13:35:09 +00:00
|
|
|
func newAWSCloud(config io.Reader, authFunc AuthFunc, metadata AWSMetadata) (*AWSCloud, error) {
|
2015-03-26 19:47:49 +00:00
|
|
|
cfg, err := readAWSCloudConfig(config, metadata)
|
2014-09-09 21:25:35 +00:00
|
|
|
if err != nil {
|
2014-11-20 10:00:36 +00:00
|
|
|
return nil, fmt.Errorf("unable to read AWS cloud provider config file: %v", err)
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
auth, err := authFunc()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2015-03-26 19:47:49 +00:00
|
|
|
zone := cfg.Global.Zone
|
|
|
|
if len(zone) <= 1 {
|
|
|
|
return nil, fmt.Errorf("invalid AWS zone in config file: %s", zone)
|
|
|
|
}
|
|
|
|
regionName := zone[:len(zone)-1]
|
|
|
|
|
|
|
|
region, ok := aws.Regions[regionName]
|
2014-09-09 21:25:35 +00:00
|
|
|
if !ok {
|
2015-03-26 19:47:49 +00:00
|
|
|
return nil, fmt.Errorf("not a valid AWS zone (unknown region): %s", zone)
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
|
|
|
|
2015-03-06 14:26:39 +00:00
|
|
|
ec2 := &goamzEC2{ec2: ec2.New(auth, region)}
|
|
|
|
|
|
|
|
awsCloud := &AWSCloud{
|
2015-03-27 15:59:49 +00:00
|
|
|
ec2: ec2,
|
|
|
|
cfg: cfg,
|
|
|
|
region: region,
|
2015-03-26 19:47:49 +00:00
|
|
|
availabilityZone: zone,
|
2015-04-08 13:25:33 +00:00
|
|
|
metadata: metadata,
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return awsCloud, nil
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
|
|
|
|
2014-11-13 20:35:03 +00:00
|
|
|
func (aws *AWSCloud) Clusters() (cloudprovider.Clusters, bool) {
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
|
2014-09-09 21:25:35 +00:00
|
|
|
// 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) {
|
2015-03-10 04:15:53 +00:00
|
|
|
return aws, true
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
|
|
|
|
2015-03-11 23:37:11 +00:00
|
|
|
// NodeAddresses is an implementation of Instances.NodeAddresses.
|
|
|
|
func (aws *AWSCloud) NodeAddresses(name string) ([]api.NodeAddress, error) {
|
2015-02-16 16:54:04 +00:00
|
|
|
inst, err := aws.getInstancesByDnsName(name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
ip := net.ParseIP(inst.PrivateIpAddress)
|
|
|
|
if ip == nil {
|
|
|
|
return nil, fmt.Errorf("invalid network IP: %s", inst.PrivateIpAddress)
|
|
|
|
}
|
2015-03-11 23:37:11 +00:00
|
|
|
|
|
|
|
return []api.NodeAddress{{Type: api.NodeLegacyHostIP, Address: ip.String()}}, nil
|
2015-02-16 16:54:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ExternalID returns the cloud provider ID of the specified instance.
|
|
|
|
func (aws *AWSCloud) ExternalID(name string) (string, error) {
|
|
|
|
inst, err := aws.getInstancesByDnsName(name)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return inst.InstanceId, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the instances matching the relevant private dns name.
|
|
|
|
func (aws *AWSCloud) getInstancesByDnsName(name string) (*ec2.Instance, error) {
|
2015-03-05 14:05:11 +00:00
|
|
|
f := &ec2InstanceFilter{}
|
|
|
|
f.PrivateDNSName = name
|
2014-09-09 21:25:35 +00:00
|
|
|
|
|
|
|
resp, err := aws.ec2.Instances(nil, f)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-03-04 22:58:36 +00:00
|
|
|
|
|
|
|
instances := []*ec2.Instance{}
|
|
|
|
for _, reservation := range resp.Reservations {
|
|
|
|
for _, instance := range reservation.Instances {
|
|
|
|
// TODO: Push running logic down into filter?
|
|
|
|
if !isAlive(&instance) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if instance.PrivateDNSName != name {
|
|
|
|
// TODO: Should we warn here? - the filter should have caught this
|
|
|
|
// (this will happen in the tests if they don't fully mock the EC2 API)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
instances = append(instances, &instance)
|
|
|
|
}
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
2015-03-04 22:58:36 +00:00
|
|
|
|
|
|
|
if len(instances) == 0 {
|
2014-11-20 10:00:36 +00:00
|
|
|
return nil, fmt.Errorf("no instances found for host: %s", name)
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
2015-03-04 22:58:36 +00:00
|
|
|
if len(instances) > 1 {
|
2014-11-20 10:00:36 +00:00
|
|
|
return nil, fmt.Errorf("multiple instances found for host: %s", name)
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
2015-03-04 22:58:36 +00:00
|
|
|
return instances[0], nil
|
|
|
|
}
|
2014-09-09 21:25:35 +00:00
|
|
|
|
2015-03-04 22:58:36 +00:00
|
|
|
// Check if the instance is alive (running or pending)
|
|
|
|
// We typically ignore instances that are not alive
|
|
|
|
func isAlive(instance *ec2.Instance) bool {
|
|
|
|
switch instance.State.Name {
|
|
|
|
case "shutting-down", "terminated", "stopping", "stopped":
|
|
|
|
return false
|
|
|
|
case "pending", "running":
|
|
|
|
return true
|
|
|
|
default:
|
|
|
|
glog.Errorf("unknown EC2 instance state: %s", instance.State)
|
|
|
|
return false
|
|
|
|
}
|
2015-02-11 22:37:27 +00:00
|
|
|
}
|
|
|
|
|
2014-09-09 21:25:35 +00:00
|
|
|
// 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 {
|
2014-11-20 10:00:36 +00:00
|
|
|
return []string{}, fmt.Errorf("no InstanceResp returned")
|
2014-09-09 21:25:35 +00:00
|
|
|
}
|
|
|
|
|
2015-03-04 21:52:49 +00:00
|
|
|
if strings.HasPrefix(regex, "'") && strings.HasSuffix(regex, "'") {
|
|
|
|
glog.Infof("Stripping quotes around regex (%s)", regex)
|
|
|
|
regex = regex[1 : len(regex)-1]
|
|
|
|
}
|
|
|
|
|
2014-09-09 21:25:35 +00:00
|
|
|
re, err := regexp.Compile(regex)
|
|
|
|
if err != nil {
|
|
|
|
return []string{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
instances := []string{}
|
|
|
|
for _, reservation := range resp.Reservations {
|
|
|
|
for _, instance := range reservation.Instances {
|
2015-03-04 22:58:36 +00:00
|
|
|
// TODO: Push filtering down into EC2 API filter?
|
|
|
|
if !isAlive(&instance) {
|
2015-03-30 15:05:24 +00:00
|
|
|
glog.V(2).Infof("skipping EC2 instance (%s): %s",
|
|
|
|
instance.State.Name, instance.InstanceId)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only return fully-ready instances when listing instances
|
|
|
|
// (vs a query by name, where we will return it if we find it)
|
|
|
|
if instance.State.Name == "pending" {
|
|
|
|
glog.V(2).Infof("skipping EC2 instance (pending): %s", instance.InstanceId)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if instance.PrivateDNSName == "" {
|
|
|
|
glog.V(2).Infof("skipping EC2 instance (no PrivateDNSName): %s",
|
|
|
|
instance.InstanceId)
|
2015-03-04 22:58:36 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2014-09-09 21:25:35 +00:00
|
|
|
for _, tag := range instance.Tags {
|
|
|
|
if tag.Key == "Name" && re.MatchString(tag.Value) {
|
|
|
|
instances = append(instances, instance.PrivateDNSName)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2015-03-04 22:58:36 +00:00
|
|
|
glog.V(2).Infof("Matched EC2 instances: %s", instances)
|
2014-09-09 21:25:35 +00:00
|
|
|
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)
|
|
|
|
}
|
2014-09-26 23:28:30 +00:00
|
|
|
|
2015-03-05 18:10:56 +00:00
|
|
|
// GetNodeResources implements Instances.GetNodeResources
|
|
|
|
func (aws *AWSCloud) GetNodeResources(name string) (*api.NodeResources, error) {
|
|
|
|
instance, err := aws.getInstancesByDnsName(name)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resources, err := getResourcesByInstanceType(instance.InstanceType)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return resources, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Builds an api.NodeResources
|
|
|
|
// cpu is in ecus, memory is in GiB
|
|
|
|
// We pass the family in so that we could provide more info (e.g. GPU or not)
|
|
|
|
func makeNodeResources(family string, cpu float64, memory float64) (*api.NodeResources, error) {
|
|
|
|
return &api.NodeResources{
|
|
|
|
Capacity: api.ResourceList{
|
|
|
|
api.ResourceCPU: *resource.NewMilliQuantity(int64(cpu*1000), resource.DecimalSI),
|
|
|
|
api.ResourceMemory: *resource.NewQuantity(int64(memory*1024*1024*1024), resource.BinarySI),
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Maps an EC2 instance type to k8s resource information
|
|
|
|
func getResourcesByInstanceType(instanceType string) (*api.NodeResources, error) {
|
|
|
|
// There is no API for this (that I know of)
|
|
|
|
switch instanceType {
|
|
|
|
// t2: Burstable
|
|
|
|
// TODO: The ECUs are fake values (because they are burstable), so this is just a guess...
|
|
|
|
case "t1.micro":
|
|
|
|
return makeNodeResources("t1", 0.125, 0.615)
|
|
|
|
|
|
|
|
// t2: Burstable
|
2015-03-06 14:26:39 +00:00
|
|
|
// TODO: The ECUs are fake values (because they are burstable), so this is just a guess...
|
2015-03-05 18:10:56 +00:00
|
|
|
case "t2.micro":
|
|
|
|
return makeNodeResources("t2", 0.25, 1)
|
|
|
|
case "t2.small":
|
|
|
|
return makeNodeResources("t2", 0.5, 2)
|
|
|
|
case "t2.medium":
|
|
|
|
return makeNodeResources("t2", 1, 4)
|
|
|
|
|
|
|
|
// c1: Compute optimized
|
|
|
|
case "c1.medium":
|
|
|
|
return makeNodeResources("c1", 5, 1.7)
|
|
|
|
case "c1.xlarge":
|
|
|
|
return makeNodeResources("c1", 20, 7)
|
|
|
|
|
|
|
|
// cc2: Compute optimized
|
|
|
|
case "cc2.8xlarge":
|
|
|
|
return makeNodeResources("cc2", 88, 60.5)
|
|
|
|
|
|
|
|
// cg1: GPU instances
|
|
|
|
case "cg1.4xlarge":
|
|
|
|
return makeNodeResources("cg1", 33.5, 22.5)
|
|
|
|
|
|
|
|
// cr1: Memory optimized
|
|
|
|
case "cr1.8xlarge":
|
|
|
|
return makeNodeResources("cr1", 88, 244)
|
|
|
|
|
|
|
|
// c3: Compute optimized
|
|
|
|
case "c3.large":
|
|
|
|
return makeNodeResources("c3", 7, 3.75)
|
|
|
|
case "c3.xlarge":
|
|
|
|
return makeNodeResources("c3", 14, 7.5)
|
|
|
|
case "c3.2xlarge":
|
|
|
|
return makeNodeResources("c3", 28, 15)
|
|
|
|
case "c3.4xlarge":
|
|
|
|
return makeNodeResources("c3", 55, 30)
|
|
|
|
case "c3.8xlarge":
|
|
|
|
return makeNodeResources("c3", 108, 60)
|
|
|
|
|
|
|
|
// c4: Compute optimized
|
|
|
|
case "c4.large":
|
|
|
|
return makeNodeResources("c4", 8, 3.75)
|
|
|
|
case "c4.xlarge":
|
|
|
|
return makeNodeResources("c4", 16, 7.5)
|
|
|
|
case "c4.2xlarge":
|
|
|
|
return makeNodeResources("c4", 31, 15)
|
|
|
|
case "c4.4xlarge":
|
|
|
|
return makeNodeResources("c4", 62, 30)
|
|
|
|
case "c4.8xlarge":
|
|
|
|
return makeNodeResources("c4", 132, 60)
|
|
|
|
|
|
|
|
// g2: GPU instances
|
|
|
|
case "g2.2xlarge":
|
|
|
|
return makeNodeResources("g2", 26, 15)
|
|
|
|
|
|
|
|
// hi1: Storage optimized (SSD)
|
|
|
|
case "hi1.4xlarge":
|
|
|
|
return makeNodeResources("hs1", 35, 60.5)
|
|
|
|
|
|
|
|
// hs1: Storage optimized (HDD)
|
|
|
|
case "hs1.8xlarge":
|
|
|
|
return makeNodeResources("hs1", 35, 117)
|
|
|
|
|
|
|
|
// m1: General purpose
|
|
|
|
case "m1.small":
|
|
|
|
return makeNodeResources("m1", 1, 1.7)
|
|
|
|
case "m1.medium":
|
|
|
|
return makeNodeResources("m1", 2, 3.75)
|
|
|
|
case "m1.large":
|
|
|
|
return makeNodeResources("m1", 4, 7.5)
|
|
|
|
case "m1.xlarge":
|
|
|
|
return makeNodeResources("m1", 8, 15)
|
|
|
|
|
|
|
|
// m2: Memory optimized
|
|
|
|
case "m2.xlarge":
|
|
|
|
return makeNodeResources("m2", 6.5, 17.1)
|
|
|
|
case "m2.2xlarge":
|
|
|
|
return makeNodeResources("m2", 13, 34.2)
|
|
|
|
case "m2.4xlarge":
|
|
|
|
return makeNodeResources("m2", 26, 68.4)
|
|
|
|
|
|
|
|
// m3: General purpose
|
|
|
|
case "m3.medium":
|
|
|
|
return makeNodeResources("m3", 3, 3.75)
|
|
|
|
case "m3.large":
|
|
|
|
return makeNodeResources("m3", 6.5, 7.5)
|
|
|
|
case "m3.xlarge":
|
|
|
|
return makeNodeResources("m3", 13, 15)
|
|
|
|
case "m3.2xlarge":
|
|
|
|
return makeNodeResources("m3", 26, 30)
|
|
|
|
|
|
|
|
// i2: Storage optimized (SSD)
|
|
|
|
case "i2.xlarge":
|
|
|
|
return makeNodeResources("i2", 14, 30.5)
|
|
|
|
case "i2.2xlarge":
|
|
|
|
return makeNodeResources("i2", 27, 61)
|
|
|
|
case "i2.4xlarge":
|
|
|
|
return makeNodeResources("i2", 53, 122)
|
|
|
|
case "i2.8xlarge":
|
|
|
|
return makeNodeResources("i2", 104, 244)
|
|
|
|
|
|
|
|
// r3: Memory optimized
|
|
|
|
case "r3.large":
|
|
|
|
return makeNodeResources("r3", 6.5, 15)
|
|
|
|
case "r3.xlarge":
|
|
|
|
return makeNodeResources("r3", 13, 30.5)
|
|
|
|
case "r3.2xlarge":
|
|
|
|
return makeNodeResources("r3", 26, 61)
|
|
|
|
case "r3.4xlarge":
|
|
|
|
return makeNodeResources("r3", 52, 122)
|
|
|
|
case "r3.8xlarge":
|
|
|
|
return makeNodeResources("r3", 104, 244)
|
|
|
|
|
|
|
|
default:
|
|
|
|
glog.Errorf("unknown instanceType: %s", instanceType)
|
|
|
|
return nil, nil
|
|
|
|
}
|
2014-09-26 23:28:30 +00:00
|
|
|
}
|
2015-03-10 04:15:53 +00:00
|
|
|
|
|
|
|
// GetZone implements Zones.GetZone
|
|
|
|
func (self *AWSCloud) GetZone() (cloudprovider.Zone, error) {
|
2015-03-26 19:47:49 +00:00
|
|
|
if self.availabilityZone == "" {
|
|
|
|
// Should be unreachable
|
|
|
|
panic("availabilityZone not set")
|
2015-03-10 04:15:53 +00:00
|
|
|
}
|
|
|
|
return cloudprovider.Zone{
|
2015-03-26 19:47:49 +00:00
|
|
|
FailureDomain: self.availabilityZone,
|
2015-03-10 04:15:53 +00:00
|
|
|
Region: self.region.Name,
|
|
|
|
}, nil
|
|
|
|
}
|
2015-03-06 14:26:39 +00:00
|
|
|
|
|
|
|
// Abstraction around AWS Instance Types
|
|
|
|
// There isn't an API to get information for a particular instance type (that I know of)
|
|
|
|
type awsInstanceType struct {
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Also return number of mounts allowed?
|
2015-04-09 13:28:01 +00:00
|
|
|
func (self *awsInstanceType) getEBSMountDevices() []string {
|
2015-03-06 14:26:39 +00:00
|
|
|
// See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html
|
|
|
|
devices := []string{}
|
|
|
|
for c := 'f'; c <= 'p'; c++ {
|
2015-04-02 18:00:50 +00:00
|
|
|
devices = append(devices, fmt.Sprintf("/dev/sd%c", c))
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
return devices
|
|
|
|
}
|
|
|
|
|
|
|
|
type awsInstance struct {
|
|
|
|
ec2 EC2
|
|
|
|
|
|
|
|
// id in AWS
|
2015-04-09 13:29:14 +00:00
|
|
|
awsID string
|
2015-03-06 14:26:39 +00:00
|
|
|
|
|
|
|
mutex sync.Mutex
|
|
|
|
|
|
|
|
// We must cache because otherwise there is a race condition,
|
|
|
|
// where we assign a device mapping and then get a second request before we attach the volume
|
|
|
|
deviceMappings map[string]string
|
|
|
|
}
|
|
|
|
|
2015-04-09 13:29:57 +00:00
|
|
|
func newAWSInstance(ec2 EC2, awsID string) *awsInstance {
|
2015-04-09 13:29:14 +00:00
|
|
|
self := &awsInstance{ec2: ec2, awsID: awsID}
|
2015-03-06 14:26:39 +00:00
|
|
|
|
|
|
|
// We lazy-init deviceMappings
|
|
|
|
self.deviceMappings = nil
|
|
|
|
|
|
|
|
return self
|
|
|
|
}
|
|
|
|
|
|
|
|
// Gets the awsInstanceType that models the instance type of this instance
|
|
|
|
func (self *awsInstance) getInstanceType() *awsInstanceType {
|
|
|
|
// TODO: Make this real
|
|
|
|
awsInstanceType := &awsInstanceType{}
|
|
|
|
return awsInstanceType
|
|
|
|
}
|
|
|
|
|
|
|
|
// Gets the full information about this instance from the EC2 API
|
|
|
|
func (self *awsInstance) getInfo() (*ec2.Instance, error) {
|
2015-04-09 13:29:14 +00:00
|
|
|
resp, err := self.ec2.Instances([]string{self.awsID}, nil)
|
2015-03-06 14:26:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error querying ec2 for instance info: %v", err)
|
|
|
|
}
|
|
|
|
if len(resp.Reservations) == 0 {
|
2015-04-09 13:29:14 +00:00
|
|
|
return nil, fmt.Errorf("no reservations found for instance: %s", self.awsID)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
if len(resp.Reservations) > 1 {
|
2015-04-09 13:29:14 +00:00
|
|
|
return nil, fmt.Errorf("multiple reservations found for instance: %s", self.awsID)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
if len(resp.Reservations[0].Instances) == 0 {
|
2015-04-09 13:29:14 +00:00
|
|
|
return nil, fmt.Errorf("no instances found for instance: %s", self.awsID)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
if len(resp.Reservations[0].Instances) > 1 {
|
2015-04-09 13:29:14 +00:00
|
|
|
return nil, fmt.Errorf("multiple instances found for instance: %s", self.awsID)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
return &resp.Reservations[0].Instances[0], nil
|
|
|
|
}
|
|
|
|
|
2015-04-02 18:10:55 +00:00
|
|
|
// Assigns an unused mount device for the specified volume.
|
|
|
|
// If the volume is already assigned, this will return the existing mount device and true
|
2015-04-09 13:34:16 +00:00
|
|
|
func (self *awsInstance) assignMountDevice(volumeID string) (mountDevice string, alreadyAttached bool, err error) {
|
2015-03-06 14:26:39 +00:00
|
|
|
instanceType := self.getInstanceType()
|
|
|
|
if instanceType == nil {
|
2015-04-09 13:29:14 +00:00
|
|
|
return "", false, fmt.Errorf("could not get instance type for instance: %s", self.awsID)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// We lock to prevent concurrent mounts from conflicting
|
|
|
|
// We may still conflict if someone calls the API concurrently,
|
|
|
|
// but the AWS API will then fail one of the two attach operations
|
|
|
|
self.mutex.Lock()
|
|
|
|
defer self.mutex.Unlock()
|
|
|
|
|
|
|
|
// We cache both for efficiency and correctness
|
|
|
|
if self.deviceMappings == nil {
|
|
|
|
info, err := self.getInfo()
|
|
|
|
if err != nil {
|
2015-04-02 18:10:55 +00:00
|
|
|
return "", false, err
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
deviceMappings := map[string]string{}
|
|
|
|
for _, blockDevice := range info.BlockDevices {
|
2015-04-09 14:48:03 +00:00
|
|
|
deviceMappings[blockDevice.DeviceName] = blockDevice.VolumeId
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
self.deviceMappings = deviceMappings
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check to see if this volume is already assigned a device on this machine
|
2015-04-09 13:34:16 +00:00
|
|
|
for deviceName, mappingVolumeID := range self.deviceMappings {
|
|
|
|
if volumeID == mappingVolumeID {
|
|
|
|
glog.Warningf("Got assignment call for already-assigned volume: %s@%s", deviceName, mappingVolumeID)
|
2015-04-02 18:10:55 +00:00
|
|
|
return deviceName, true, nil
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check all the valid mountpoints to see if any of them are free
|
2015-04-09 13:28:01 +00:00
|
|
|
valid := instanceType.getEBSMountDevices()
|
2015-03-06 14:26:39 +00:00
|
|
|
chosen := ""
|
|
|
|
for _, device := range valid {
|
|
|
|
_, found := self.deviceMappings[device]
|
|
|
|
if !found {
|
|
|
|
chosen = device
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if chosen == "" {
|
|
|
|
glog.Warningf("Could not assign a mount device (all in use?). mappings=%v, valid=%v", self.deviceMappings, valid)
|
2015-04-02 18:10:55 +00:00
|
|
|
return "", false, nil
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
2015-04-09 13:34:16 +00:00
|
|
|
self.deviceMappings[chosen] = volumeID
|
|
|
|
glog.V(2).Infof("Assigned mount device %s -> volume %s", chosen, volumeID)
|
2015-03-06 14:26:39 +00:00
|
|
|
|
2015-04-02 18:10:55 +00:00
|
|
|
return chosen, false, nil
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
2015-04-09 13:34:16 +00:00
|
|
|
func (self *awsInstance) releaseMountDevice(volumeID string, mountDevice string) {
|
2015-03-06 14:26:39 +00:00
|
|
|
self.mutex.Lock()
|
|
|
|
defer self.mutex.Unlock()
|
|
|
|
|
2015-04-09 13:34:16 +00:00
|
|
|
existingVolumeID, found := self.deviceMappings[mountDevice]
|
2015-03-06 14:26:39 +00:00
|
|
|
if !found {
|
|
|
|
glog.Errorf("releaseMountDevice on non-allocated device")
|
|
|
|
return
|
|
|
|
}
|
2015-04-09 13:34:16 +00:00
|
|
|
if volumeID != existingVolumeID {
|
2015-03-06 14:26:39 +00:00
|
|
|
glog.Errorf("releaseMountDevice on device assigned to different volume")
|
|
|
|
return
|
|
|
|
}
|
2015-04-09 13:34:16 +00:00
|
|
|
glog.V(2).Infof("Releasing mount device mapping: %s -> volume %s", mountDevice, volumeID)
|
2015-03-06 14:26:39 +00:00
|
|
|
delete(self.deviceMappings, mountDevice)
|
|
|
|
}
|
|
|
|
|
|
|
|
type awsDisk struct {
|
|
|
|
ec2 EC2
|
|
|
|
|
|
|
|
// Name in k8s
|
|
|
|
name string
|
|
|
|
// id in AWS
|
2015-04-09 13:29:14 +00:00
|
|
|
awsID string
|
2015-03-06 14:26:39 +00:00
|
|
|
// az which holds the volume
|
|
|
|
az string
|
|
|
|
}
|
|
|
|
|
2015-04-09 13:29:57 +00:00
|
|
|
func newAWSDisk(ec2 EC2, name string) (*awsDisk, error) {
|
2015-03-06 14:26:39 +00:00
|
|
|
// name looks like aws://availability-zone/id
|
|
|
|
url, err := url.Parse(name)
|
|
|
|
if err != nil {
|
|
|
|
// TODO: Maybe we should pass a URL into the Volume functions
|
|
|
|
return nil, fmt.Errorf("Invalid disk name (%s): %v", name, err)
|
|
|
|
}
|
|
|
|
if url.Scheme != "aws" {
|
|
|
|
return nil, fmt.Errorf("Invalid scheme for AWS volume (%s)", name)
|
|
|
|
}
|
2015-04-02 17:23:41 +00:00
|
|
|
|
2015-04-09 13:29:14 +00:00
|
|
|
awsID := url.Path
|
|
|
|
if len(awsID) > 1 && awsID[0] == '/' {
|
|
|
|
awsID = awsID[1:]
|
2015-04-02 17:23:41 +00:00
|
|
|
}
|
|
|
|
|
2015-03-06 14:26:39 +00:00
|
|
|
// TODO: Regex match?
|
2015-04-09 13:29:14 +00:00
|
|
|
if strings.Contains(awsID, "/") || !strings.HasPrefix(awsID, "vol-") {
|
2015-03-06 14:26:39 +00:00
|
|
|
return nil, fmt.Errorf("Invalid format for AWS volume (%s)", name)
|
|
|
|
}
|
|
|
|
az := url.Host
|
|
|
|
// TODO: Better validation?
|
|
|
|
// TODO: Default to our AZ? Look it up?
|
|
|
|
// TODO: Should this be a region or an AZ?
|
|
|
|
if az == "" {
|
|
|
|
return nil, fmt.Errorf("Invalid format for AWS volume (%s)", name)
|
|
|
|
}
|
2015-04-09 13:29:14 +00:00
|
|
|
disk := &awsDisk{ec2: ec2, name: name, awsID: awsID, az: az}
|
2015-03-06 14:26:39 +00:00
|
|
|
return disk, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Gets the full information about this volume from the EC2 API
|
|
|
|
func (self *awsDisk) getInfo() (*ec2.Volume, error) {
|
2015-04-09 13:29:14 +00:00
|
|
|
resp, err := self.ec2.Volumes([]string{self.awsID}, nil)
|
2015-03-06 14:26:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error querying ec2 for volume info: %v", err)
|
|
|
|
}
|
|
|
|
if len(resp.Volumes) == 0 {
|
2015-04-09 13:29:14 +00:00
|
|
|
return nil, fmt.Errorf("no volumes found for volume: %s", self.awsID)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
if len(resp.Volumes) > 1 {
|
2015-04-09 13:29:14 +00:00
|
|
|
return nil, fmt.Errorf("multiple volumes found for volume: %s", self.awsID)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
return &resp.Volumes[0], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (self *awsDisk) waitForAttachmentStatus(status string) error {
|
2015-04-03 19:27:33 +00:00
|
|
|
// TODO: There may be a faster way to get this when we're attaching locally
|
2015-03-06 14:26:39 +00:00
|
|
|
attempt := 0
|
|
|
|
maxAttempts := 60
|
|
|
|
|
|
|
|
for {
|
|
|
|
info, err := self.getInfo()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if len(info.Attachments) > 1 {
|
|
|
|
glog.Warningf("Found multiple attachments for volume: %v", info)
|
|
|
|
}
|
2015-04-03 18:10:55 +00:00
|
|
|
attachmentStatus := ""
|
2015-03-06 14:26:39 +00:00
|
|
|
for _, attachment := range info.Attachments {
|
2015-04-03 18:10:55 +00:00
|
|
|
if attachmentStatus != "" {
|
|
|
|
glog.Warning("Found multiple attachments: ", info)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
attachmentStatus = attachment.Status
|
|
|
|
}
|
2015-04-03 18:10:55 +00:00
|
|
|
if attachmentStatus == "" {
|
|
|
|
attachmentStatus = "detached"
|
|
|
|
}
|
|
|
|
if attachmentStatus == status {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2015-03-06 14:26:39 +00:00
|
|
|
glog.V(2).Infof("Waiting for volume state: actual=%s, desired=%s", attachmentStatus, status)
|
|
|
|
|
|
|
|
attempt++
|
|
|
|
if attempt > maxAttempts {
|
|
|
|
glog.Warningf("Timeout waiting for volume state: actual=%s, desired=%s", attachmentStatus, status)
|
|
|
|
return errors.New("Timeout waiting for volume state")
|
|
|
|
}
|
|
|
|
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Deletes the EBS disk
|
|
|
|
func (self *awsDisk) delete() error {
|
2015-04-09 13:29:14 +00:00
|
|
|
_, err := self.ec2.DeleteVolume(self.awsID)
|
2015-03-06 14:26:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error delete EBS volumes: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Gets the awsInstance for the EC2 instance on which we are running
|
|
|
|
// may return nil in case of error
|
2015-04-09 13:29:57 +00:00
|
|
|
func (aws *AWSCloud) getSelfAWSInstance() (*awsInstance, error) {
|
2015-03-06 14:26:39 +00:00
|
|
|
// Note that we cache some state in awsInstance (mountpoints), so we must preserve the instance
|
2015-04-08 13:25:33 +00:00
|
|
|
|
|
|
|
aws.mutex.Lock()
|
|
|
|
defer aws.mutex.Unlock()
|
|
|
|
|
2015-04-09 13:29:57 +00:00
|
|
|
i := aws.selfAWSInstance
|
2015-04-08 13:25:33 +00:00
|
|
|
if i == nil {
|
|
|
|
instanceIdBytes, err := aws.metadata.GetMetaData("instance-id")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error fetching instance-id from ec2 metadata service: %v", err)
|
|
|
|
}
|
2015-04-09 13:29:57 +00:00
|
|
|
i = newAWSInstance(aws.ec2, string(instanceIdBytes))
|
|
|
|
aws.selfAWSInstance = i
|
2015-04-08 13:25:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return i, nil
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Implements Volumes.AttachDisk
|
2015-04-02 18:56:11 +00:00
|
|
|
func (aws *AWSCloud) AttachDisk(instanceName string, diskName string, readOnly bool) (string, error) {
|
2015-04-09 13:29:57 +00:00
|
|
|
disk, err := newAWSDisk(aws.ec2, diskName)
|
2015-03-06 14:26:39 +00:00
|
|
|
if err != nil {
|
2015-04-02 18:56:11 +00:00
|
|
|
return "", err
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var awsInstance *awsInstance
|
|
|
|
if instanceName == "" {
|
2015-04-09 13:29:57 +00:00
|
|
|
awsInstance, err = aws.getSelfAWSInstance()
|
2015-04-08 13:25:33 +00:00
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("Error getting self-instance: %v", err)
|
|
|
|
}
|
2015-03-06 14:26:39 +00:00
|
|
|
} else {
|
|
|
|
instance, err := aws.getInstancesByDnsName(instanceName)
|
|
|
|
if err != nil {
|
2015-04-02 18:56:11 +00:00
|
|
|
return "", fmt.Errorf("Error finding instance: %v", err)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
2015-04-09 13:29:57 +00:00
|
|
|
awsInstance = newAWSInstance(aws.ec2, instance.InstanceId)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if readOnly {
|
|
|
|
// TODO: We could enforce this when we mount the volume (?)
|
|
|
|
// TODO: We could also snapshot the volume and attach copies of it
|
2015-04-02 18:56:11 +00:00
|
|
|
return "", errors.New("AWS volumes cannot be mounted read-only")
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
2015-04-09 13:29:14 +00:00
|
|
|
mountDevice, alreadyAttached, err := awsInstance.assignMountDevice(disk.awsID)
|
2015-03-06 14:26:39 +00:00
|
|
|
if err != nil {
|
2015-04-02 18:56:11 +00:00
|
|
|
return "", err
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
attached := false
|
|
|
|
defer func() {
|
|
|
|
if !attached {
|
2015-04-09 13:29:14 +00:00
|
|
|
awsInstance.releaseMountDevice(disk.awsID, mountDevice)
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2015-04-02 18:10:55 +00:00
|
|
|
if !alreadyAttached {
|
2015-04-09 13:29:14 +00:00
|
|
|
attachResponse, err := aws.ec2.AttachVolume(disk.awsID, awsInstance.awsID, mountDevice)
|
2015-04-02 18:10:55 +00:00
|
|
|
if err != nil {
|
2015-04-02 18:56:11 +00:00
|
|
|
// TODO: Check if the volume was concurrently attached?
|
|
|
|
return "", fmt.Errorf("Error attaching EBS volume: %v", err)
|
2015-04-02 18:10:55 +00:00
|
|
|
}
|
2015-03-06 14:26:39 +00:00
|
|
|
|
2015-04-02 18:10:55 +00:00
|
|
|
glog.V(2).Info("AttachVolume request returned %v", attachResponse)
|
|
|
|
}
|
2015-03-06 14:26:39 +00:00
|
|
|
|
|
|
|
err = disk.waitForAttachmentStatus("attached")
|
|
|
|
if err != nil {
|
2015-04-02 18:56:11 +00:00
|
|
|
return "", err
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
attached = true
|
|
|
|
|
2015-04-02 18:56:11 +00:00
|
|
|
hostDevice := mountDevice
|
|
|
|
if strings.HasPrefix(hostDevice, "/dev/sd") {
|
|
|
|
// Inside the instance, the mountpoint /dev/sdf looks like /dev/xvdf
|
|
|
|
hostDevice = "/dev/xvd" + hostDevice[7:]
|
|
|
|
}
|
|
|
|
return hostDevice, nil
|
2015-03-06 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Implements Volumes.DetachDisk
|
|
|
|
func (aws *AWSCloud) DetachDisk(instanceName string, diskName string) error {
|
2015-04-09 13:29:57 +00:00
|
|
|
disk, err := newAWSDisk(aws.ec2, diskName)
|
2015-03-06 14:26:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: We should specify the InstanceID and the Device, for safety
|
2015-04-09 13:29:14 +00:00
|
|
|
response, err := aws.ec2.DetachVolume(disk.awsID)
|
2015-03-06 14:26:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error detaching EBS volume: %v", err)
|
|
|
|
}
|
|
|
|
if response == nil {
|
|
|
|
return errors.New("no response from DetachVolume")
|
|
|
|
}
|
|
|
|
err = disk.waitForAttachmentStatus("detached")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Implements Volumes.CreateVolume
|
|
|
|
func (aws *AWSCloud) CreateVolume(volumeOptions *VolumeOptions) (string, error) {
|
|
|
|
request := &ec2.CreateVolume{}
|
2015-03-27 15:59:07 +00:00
|
|
|
request.AvailZone = aws.availabilityZone
|
2015-03-06 14:26:39 +00:00
|
|
|
request.Size = (int64(volumeOptions.CapacityMB) + 1023) / 1024
|
|
|
|
response, err := aws.ec2.CreateVolume(request)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
az := response.AvailZone
|
2015-04-09 14:48:03 +00:00
|
|
|
awsID := response.VolumeId
|
2015-03-06 14:26:39 +00:00
|
|
|
|
2015-04-09 13:29:14 +00:00
|
|
|
volumeName := "aws://" + az + "/" + awsID
|
2015-03-06 14:26:39 +00:00
|
|
|
|
|
|
|
return volumeName, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Implements Volumes.DeleteVolume
|
|
|
|
func (aws *AWSCloud) DeleteVolume(volumeName string) error {
|
2015-04-09 13:29:57 +00:00
|
|
|
awsDisk, err := newAWSDisk(aws.ec2, volumeName)
|
2015-03-06 14:26:39 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return awsDisk.delete()
|
|
|
|
}
|