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.
@@ -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,7 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh
4
+ module AwsCloud
5
+ VERSION = "0.3.3"
6
+ end
7
+ 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
@@ -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
+