bosh_aws_cpi 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +3 -0
- data/Rakefile +50 -0
- data/lib/bosh_aws_cpi.rb +3 -0
- data/lib/cloud/aws/cloud.rb +596 -0
- data/lib/cloud/aws/dynamic_network.rb +28 -0
- data/lib/cloud/aws/helpers.rb +58 -0
- data/lib/cloud/aws/network.rb +37 -0
- data/lib/cloud/aws/network_configurator.rb +112 -0
- data/lib/cloud/aws/registry_client.rb +109 -0
- data/lib/cloud/aws/version.rb +7 -0
- data/lib/cloud/aws/vip_network.rb +42 -0
- data/lib/cloud/aws.rb +33 -0
- data/spec/spec_helper.rb +100 -0
- data/spec/unit/attach_disk_spec.rb +146 -0
- data/spec/unit/cloud_spec.rb +16 -0
- data/spec/unit/configure_networks_spec.rb +92 -0
- data/spec/unit/create_disk_spec.rb +76 -0
- data/spec/unit/create_stemcell_spec.rb +107 -0
- data/spec/unit/create_vm_spec.rb +134 -0
- data/spec/unit/delete_disk_spec.rb +34 -0
- data/spec/unit/delete_stemcell_spec.rb +18 -0
- data/spec/unit/delete_vm_spec.rb +26 -0
- data/spec/unit/detach_disk_spec.rb +64 -0
- data/spec/unit/helpers_spec.rb +42 -0
- data/spec/unit/network_configurator_spec.rb +57 -0
- data/spec/unit/reboot_vm_spec.rb +40 -0
- data/spec/unit/validate_deployment_spec.rb +16 -0
- metadata +188 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh::AwsCloud
|
4
|
+
##
|
5
|
+
#
|
6
|
+
class DynamicNetwork < Network
|
7
|
+
|
8
|
+
##
|
9
|
+
# Creates a new dynamic network
|
10
|
+
#
|
11
|
+
# @param [String] name Network name
|
12
|
+
# @param [Hash] spec Raw network spec
|
13
|
+
def initialize(name, spec)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Configures EC2 dynamic network. Right now it's a no-op,
|
19
|
+
# as dynamic networks are completely managed by EC2
|
20
|
+
# @param [AWS:EC2] instance EC2 client
|
21
|
+
# @param [AWS::EC2::Instance] EC2 instance to configure
|
22
|
+
def configure(ec2, instance)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh::AwsCloud
|
4
|
+
|
5
|
+
module Helpers
|
6
|
+
|
7
|
+
DEFAULT_TIMEOUT = 3600
|
8
|
+
|
9
|
+
##
|
10
|
+
# Raises CloudError exception
|
11
|
+
#
|
12
|
+
def cloud_error(message)
|
13
|
+
if @logger
|
14
|
+
@logger.error(message)
|
15
|
+
end
|
16
|
+
raise Bosh::Clouds::CloudError, message
|
17
|
+
end
|
18
|
+
|
19
|
+
def wait_resource(resource, start_state,
|
20
|
+
target_state, state_method = :status,
|
21
|
+
timeout = DEFAULT_TIMEOUT)
|
22
|
+
|
23
|
+
started_at = Time.now
|
24
|
+
state = resource.send(state_method)
|
25
|
+
desc = resource.to_s
|
26
|
+
|
27
|
+
while state == start_state && state != target_state
|
28
|
+
duration = Time.now - started_at
|
29
|
+
|
30
|
+
if duration > timeout
|
31
|
+
cloud_error("Timed out waiting for #{desc} " \
|
32
|
+
"to be #{target_state}")
|
33
|
+
end
|
34
|
+
|
35
|
+
if @logger
|
36
|
+
@logger.debug("Waiting for #{desc} " \
|
37
|
+
"to be #{target_state} (#{duration})")
|
38
|
+
end
|
39
|
+
|
40
|
+
sleep(1)
|
41
|
+
|
42
|
+
state = resource.send(state_method)
|
43
|
+
end
|
44
|
+
|
45
|
+
if state == target_state
|
46
|
+
if @logger
|
47
|
+
@logger.info("#{desc} is #{target_state} " \
|
48
|
+
"after #{Time.now - started_at}s")
|
49
|
+
end
|
50
|
+
else
|
51
|
+
cloud_error("#{desc} is #{state}, " \
|
52
|
+
"expected to be #{target_state}")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh::AwsCloud
|
4
|
+
##
|
5
|
+
#
|
6
|
+
class Network
|
7
|
+
include Helpers
|
8
|
+
|
9
|
+
##
|
10
|
+
# Creates a new network
|
11
|
+
#
|
12
|
+
# @param [String] name Network name
|
13
|
+
# @param [Hash] spec Raw network spec
|
14
|
+
def initialize(name, spec)
|
15
|
+
unless spec.is_a?(Hash)
|
16
|
+
raise ArgumentError, "Invalid spec, Hash expected, " \
|
17
|
+
"#{spec.class} provided"
|
18
|
+
end
|
19
|
+
|
20
|
+
@logger = Bosh::Clouds::Config.logger
|
21
|
+
|
22
|
+
@name = name
|
23
|
+
@ip = spec["ip"]
|
24
|
+
@cloud_properties = spec["cloud_properties"]
|
25
|
+
end
|
26
|
+
|
27
|
+
##
|
28
|
+
# Configures given instance
|
29
|
+
#
|
30
|
+
# @param [AWS:EC2] instance EC2 client
|
31
|
+
# @param [AWS::EC2::Instance] EC2 instance to configure
|
32
|
+
def configure(ec2, instance)
|
33
|
+
cloud_error("`configure' not implemented by #{self.class}")
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh::AwsCloud
|
4
|
+
##
|
5
|
+
# Represents AWS instance network config. EC2 instance has single NIC
|
6
|
+
# with dynamic IP address and (optionally) a single elastic IP address
|
7
|
+
# which instance itself is not aware of (vip). Thus we should perform
|
8
|
+
# a number of sanity checks for the network spec provided by director
|
9
|
+
# to make sure we don't apply something EC2 doesn't understand how to
|
10
|
+
# deal with.
|
11
|
+
#
|
12
|
+
class NetworkConfigurator
|
13
|
+
include Helpers
|
14
|
+
|
15
|
+
##
|
16
|
+
# Creates new network spec
|
17
|
+
#
|
18
|
+
# @param [Hash] spec raw network spec passed by director
|
19
|
+
# TODO Add network configuration examples
|
20
|
+
def initialize(spec)
|
21
|
+
unless spec.is_a?(Hash)
|
22
|
+
raise ArgumentError, "Invalid spec, Hash expected, " \
|
23
|
+
"#{spec.class} provided"
|
24
|
+
end
|
25
|
+
|
26
|
+
@logger = Bosh::Clouds::Config.logger
|
27
|
+
@dynamic_network = nil
|
28
|
+
@vip_network = nil
|
29
|
+
@security_groups = []
|
30
|
+
|
31
|
+
spec.each_pair do |name, spec|
|
32
|
+
network_type = spec["type"]
|
33
|
+
|
34
|
+
case network_type
|
35
|
+
when "dynamic"
|
36
|
+
if @dynamic_network
|
37
|
+
cloud_error("More than one dynamic network for `#{name}'")
|
38
|
+
else
|
39
|
+
@dynamic_network = DynamicNetwork.new(name, spec)
|
40
|
+
# only extract security groups for dynamic networks
|
41
|
+
extract_security_groups(spec)
|
42
|
+
end
|
43
|
+
when "vip"
|
44
|
+
if @vip_network
|
45
|
+
cloud_error("More than one vip network for `#{name}'")
|
46
|
+
else
|
47
|
+
@vip_network = VipNetwork.new(name, spec)
|
48
|
+
end
|
49
|
+
else
|
50
|
+
cloud_error("Invalid network type `#{network_type}': AWS CPI " \
|
51
|
+
"can only handle `dynamic' and `vip' network types")
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
if @dynamic_network.nil?
|
57
|
+
cloud_error("At least one dynamic network should be defined")
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def configure(ec2, instance)
|
62
|
+
@dynamic_network.configure(ec2, instance)
|
63
|
+
|
64
|
+
if @vip_network
|
65
|
+
@vip_network.configure(ec2, instance)
|
66
|
+
else
|
67
|
+
# If there is no vip network we should disassociate any elastic IP
|
68
|
+
# currently held by instance (as it might have had elastic IP before)
|
69
|
+
elastic_ip = instance.elastic_ip
|
70
|
+
|
71
|
+
if elastic_ip
|
72
|
+
@logger.info("Disassociating elastic IP `#{elastic_ip}' " \
|
73
|
+
"from instance `#{instance.id}'")
|
74
|
+
instance.disassociate_elastic_ip
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Returns the security groups for this network configuration, or
|
81
|
+
# the default security groups if the configuration does not contain
|
82
|
+
# security groups
|
83
|
+
# @param [Array] default Default security groups
|
84
|
+
# @return [Array] security groups
|
85
|
+
def security_groups(default)
|
86
|
+
if @security_groups.empty? && default
|
87
|
+
return default
|
88
|
+
else
|
89
|
+
return @security_groups
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Extracts the security groups from the network configuration
|
95
|
+
# @param [Hash] network_spec Network specification
|
96
|
+
# @raise [ArgumentError] if the security groups in the network_spec
|
97
|
+
# is not an Array
|
98
|
+
def extract_security_groups(spec)
|
99
|
+
if spec && spec["cloud_properties"]
|
100
|
+
cloud_properties = spec["cloud_properties"]
|
101
|
+
if cloud_properties && cloud_properties["security_groups"]
|
102
|
+
unless cloud_properties["security_groups"].is_a?(Array)
|
103
|
+
raise ArgumentError, "security groups must be an Array"
|
104
|
+
end
|
105
|
+
@security_groups += cloud_properties["security_groups"]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh::AwsCloud
|
4
|
+
class RegistryClient
|
5
|
+
include Helpers
|
6
|
+
|
7
|
+
attr_reader :endpoint
|
8
|
+
attr_reader :user
|
9
|
+
attr_reader :password
|
10
|
+
|
11
|
+
def initialize(endpoint, user, password)
|
12
|
+
@endpoint = endpoint
|
13
|
+
|
14
|
+
unless @endpoint =~ /^http:\/\//
|
15
|
+
@endpoint = "http://#{@endpoint}"
|
16
|
+
end
|
17
|
+
|
18
|
+
@user = user
|
19
|
+
@password = password
|
20
|
+
|
21
|
+
auth = Base64.encode64("#{@user}:#{@password}").gsub("\n", "")
|
22
|
+
|
23
|
+
@headers = {
|
24
|
+
"Accept" => "application/json",
|
25
|
+
"Authorization" => "Basic #{auth}"
|
26
|
+
}
|
27
|
+
|
28
|
+
@client = HTTPClient.new
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Update instance settings in the registry
|
33
|
+
# @param [String] instance_id EC2 instance id
|
34
|
+
# @param [Hash] settings New agent settings
|
35
|
+
# @return [Boolean]
|
36
|
+
def update_settings(instance_id, settings)
|
37
|
+
unless settings.is_a?(Hash)
|
38
|
+
raise ArgumentError, "Invalid settings format, " \
|
39
|
+
"Hash expected, #{settings.class} given"
|
40
|
+
end
|
41
|
+
|
42
|
+
payload = Yajl::Encoder.encode(settings)
|
43
|
+
url = "#{@endpoint}/instances/#{instance_id}/settings"
|
44
|
+
|
45
|
+
response = @client.put(url, payload, @headers)
|
46
|
+
|
47
|
+
if response.status != 200
|
48
|
+
cloud_error("Cannot update settings for `#{instance_id}', " \
|
49
|
+
"got HTTP #{response.status}")
|
50
|
+
end
|
51
|
+
|
52
|
+
true
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Read instance settings from the registry
|
57
|
+
# @param [String] instance_id EC2 instance id
|
58
|
+
# @return [Hash] Agent settings
|
59
|
+
def read_settings(instance_id)
|
60
|
+
url = "#{@endpoint}/instances/#{instance_id}/settings"
|
61
|
+
|
62
|
+
response = @client.get(url, {}, @headers)
|
63
|
+
|
64
|
+
if response.status != 200
|
65
|
+
cloud_error("Cannot read settings for `#{instance_id}', " \
|
66
|
+
"got HTTP #{response.status}")
|
67
|
+
end
|
68
|
+
|
69
|
+
body = Yajl::Parser.parse(response.body)
|
70
|
+
|
71
|
+
unless body.is_a?(Hash)
|
72
|
+
cloud_error("Invalid registry response, Hash expected, " \
|
73
|
+
"got #{body.class}: #{body}")
|
74
|
+
end
|
75
|
+
|
76
|
+
settings = Yajl::Parser.parse(body["settings"])
|
77
|
+
|
78
|
+
unless settings.is_a?(Hash)
|
79
|
+
cloud_error("Invalid settings format, " \
|
80
|
+
"Hash expected, got #{settings.class}: " \
|
81
|
+
"#{settings}")
|
82
|
+
end
|
83
|
+
|
84
|
+
settings
|
85
|
+
|
86
|
+
rescue Yajl::ParseError
|
87
|
+
cloud_error("Cannot parse settings for `#{instance_id}'")
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Delete instance settings from the registry
|
92
|
+
# @param [String] instance_id EC2 instance id
|
93
|
+
# @return [Boolean]
|
94
|
+
def delete_settings(instance_id)
|
95
|
+
url = "#{@endpoint}/instances/#{instance_id}/settings"
|
96
|
+
|
97
|
+
response = @client.delete(url, @headers)
|
98
|
+
|
99
|
+
if response.status != 200
|
100
|
+
cloud_error("Cannot delete settings for `#{instance_id}', " \
|
101
|
+
"got HTTP #{response.status}")
|
102
|
+
end
|
103
|
+
|
104
|
+
true
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh::AwsCloud
|
4
|
+
##
|
5
|
+
#
|
6
|
+
class VipNetwork < Network
|
7
|
+
|
8
|
+
##
|
9
|
+
# Creates a new vip network
|
10
|
+
#
|
11
|
+
# @param [String] name Network name
|
12
|
+
# @param [Hash] spec Raw network spec
|
13
|
+
def initialize(name, spec)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Configures vip network
|
19
|
+
#
|
20
|
+
# @param [AWS:EC2] ec2 EC2 client
|
21
|
+
# @param [AWS::EC2::Instance] instance EC2 instance to configure
|
22
|
+
def configure(ec2, instance)
|
23
|
+
if @ip.nil?
|
24
|
+
cloud_error("No IP provided for vip network `#{@name}'")
|
25
|
+
end
|
26
|
+
|
27
|
+
@logger.info("Associating instance `#{instance.id}' " \
|
28
|
+
"with elastic IP `#{@ip}'")
|
29
|
+
|
30
|
+
# New elastic IP reservation supposed to clear the old one,
|
31
|
+
# so no need to disassociate manually. Also, we don't check
|
32
|
+
# if this IP is actually an allocated EC2 elastic IP, as
|
33
|
+
# API call will fail in that case.
|
34
|
+
# TODO: wrap error for non-existing elastic IP?
|
35
|
+
# TODO: poll instance until this IP is returned as its public IP?
|
36
|
+
instance.associate_elastic_ip(@ip)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
data/lib/cloud/aws.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
module Bosh
|
4
|
+
module AwsCloud; end
|
5
|
+
end
|
6
|
+
|
7
|
+
require "aws-sdk"
|
8
|
+
require "httpclient"
|
9
|
+
require "pp"
|
10
|
+
require "set"
|
11
|
+
require "tmpdir"
|
12
|
+
require "uuidtools"
|
13
|
+
require "yajl"
|
14
|
+
|
15
|
+
require "common/thread_pool"
|
16
|
+
require "common/thread_formatter"
|
17
|
+
|
18
|
+
require "cloud"
|
19
|
+
require "cloud/aws/helpers"
|
20
|
+
require "cloud/aws/cloud"
|
21
|
+
require "cloud/aws/registry_client"
|
22
|
+
require "cloud/aws/version"
|
23
|
+
|
24
|
+
require "cloud/aws/network_configurator"
|
25
|
+
require "cloud/aws/network"
|
26
|
+
require "cloud/aws/dynamic_network"
|
27
|
+
require "cloud/aws/vip_network"
|
28
|
+
|
29
|
+
module Bosh
|
30
|
+
module Clouds
|
31
|
+
Aws = Bosh::AwsCloud::Cloud
|
32
|
+
end
|
33
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# Copyright (c) 2009-2012 VMware, Inc.
|
2
|
+
|
3
|
+
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
|
4
|
+
|
5
|
+
require "rubygems"
|
6
|
+
require "bundler"
|
7
|
+
Bundler.setup(:default, :test)
|
8
|
+
|
9
|
+
require "rspec"
|
10
|
+
require "tmpdir"
|
11
|
+
|
12
|
+
require "cloud/aws"
|
13
|
+
|
14
|
+
class AwsConfig
|
15
|
+
attr_accessor :db, :logger, :uuid
|
16
|
+
end
|
17
|
+
|
18
|
+
aws_config = AwsConfig.new
|
19
|
+
aws_config.db = nil # AWS CPI doesn't need DB
|
20
|
+
aws_config.logger = Logger.new(StringIO.new)
|
21
|
+
aws_config.logger.level = Logger::DEBUG
|
22
|
+
|
23
|
+
Bosh::Clouds::Config.configure(aws_config)
|
24
|
+
|
25
|
+
MOCK_AWS_ACCESS_KEY_ID = "foo"
|
26
|
+
MOCK_AWS_SECRET_ACCESS_KEY = "bar"
|
27
|
+
|
28
|
+
def mock_cloud_options
|
29
|
+
{
|
30
|
+
"aws" => {
|
31
|
+
"access_key_id" => MOCK_AWS_ACCESS_KEY_ID,
|
32
|
+
"secret_access_key" => MOCK_AWS_SECRET_ACCESS_KEY
|
33
|
+
},
|
34
|
+
"registry" => {
|
35
|
+
"endpoint" => "localhost:42288",
|
36
|
+
"user" => "admin",
|
37
|
+
"password" => "admin"
|
38
|
+
},
|
39
|
+
"agent" => {
|
40
|
+
"foo" => "bar",
|
41
|
+
"baz" => "zaz"
|
42
|
+
}
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def make_cloud(options = nil)
|
47
|
+
Bosh::AwsCloud::Cloud.new(options || mock_cloud_options)
|
48
|
+
end
|
49
|
+
|
50
|
+
def mock_registry(endpoint = "http://registry:3333")
|
51
|
+
registry = mock("registry", :endpoint => endpoint)
|
52
|
+
Bosh::AwsCloud::RegistryClient.stub!(:new).and_return(registry)
|
53
|
+
registry
|
54
|
+
end
|
55
|
+
|
56
|
+
def mock_cloud(options = nil)
|
57
|
+
instances = double("instances")
|
58
|
+
volumes = double("volumes")
|
59
|
+
images = double("images")
|
60
|
+
|
61
|
+
ec2 = double(AWS::EC2)
|
62
|
+
|
63
|
+
ec2.stub(:instances).and_return(instances)
|
64
|
+
ec2.stub(:volumes).and_return(volumes)
|
65
|
+
ec2.stub(:images).and_return(images)
|
66
|
+
|
67
|
+
AWS::EC2.stub(:new).and_return(ec2)
|
68
|
+
|
69
|
+
yield ec2 if block_given?
|
70
|
+
|
71
|
+
Bosh::AwsCloud::Cloud.new(options || mock_cloud_options)
|
72
|
+
end
|
73
|
+
|
74
|
+
def dynamic_network_spec
|
75
|
+
{ "type" => "dynamic" }
|
76
|
+
end
|
77
|
+
|
78
|
+
def vip_network_spec
|
79
|
+
{
|
80
|
+
"type" => "vip",
|
81
|
+
"ip" => "10.0.0.1"
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def combined_network_spec
|
86
|
+
{
|
87
|
+
"network_a" => dynamic_network_spec,
|
88
|
+
"network_b" => vip_network_spec
|
89
|
+
}
|
90
|
+
end
|
91
|
+
|
92
|
+
def resource_pool_spec
|
93
|
+
{
|
94
|
+
"key_name" => "test_key",
|
95
|
+
"availability_zone" => "foobar-1a",
|
96
|
+
"instance_type" => "m3.zb"
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
|