bosh_openstack_cpi 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,26 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ module Bosh::OpenStackCloud
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 OpenStack dynamic network. Right now it's a no-op,
19
+ # as dynamic networks are completely managed by OpenStack
20
+ # @param [Fog::Compute::OpenStack] openstack Fog OpenStack Compute client
21
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server to configure
22
+ def configure(openstack, server)
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ module Bosh::OpenStackCloud
4
+
5
+ module Helpers
6
+
7
+ DEFAULT_TIMEOUT = 3600
8
+
9
+ #
10
+ # Raises CloudError exception
11
+ #
12
+ def cloud_error(message)
13
+ @logger.error(message) if @logger
14
+ raise Bosh::Clouds::CloudError, message
15
+ end
16
+
17
+ #
18
+ # Waits for a resource to be on a target state
19
+ #
20
+ def wait_resource(resource, start_state, target_state, state_method = :status, timeout = DEFAULT_TIMEOUT)
21
+
22
+ started_at = Time.now
23
+ state = resource.send(state_method).downcase rescue state_method
24
+ desc = resource.class.name.split("::").last.to_s + " " + resource.id.to_s
25
+
26
+ while state.to_sym != target_state
27
+ duration = Time.now - started_at
28
+
29
+ if duration > timeout
30
+ cloud_error("Timed out waiting for #{desc} to be #{target_state}")
31
+ end
32
+
33
+ @logger.debug("Waiting for #{desc} to be #{target_state} (#{duration})") if @logger
34
+
35
+ sleep(1)
36
+
37
+ if resource.reload.nil?
38
+ state = target_state
39
+ else
40
+ state = resource.send(state_method).downcase rescue state_method
41
+ end
42
+ end
43
+
44
+ @logger.info("#{desc} is #{target_state} after #{Time.now - started_at}s") if @logger
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,37 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ module Bosh::OpenStackCloud
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 server
29
+ #
30
+ # @param [Fog::Compute::OpenStack] openstack Fog OpenStack Compute client
31
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server to configure
32
+ def configure(openstack, server)
33
+ cloud_error("`configure' not implemented by #{self.class}")
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,113 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ module Bosh::OpenStackCloud
4
+ ##
5
+ # Represents OpenStack server network config. OpenStack server has single NIC
6
+ # with dynamic IP address and (optionally) a single floating IP address
7
+ # which server 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 OpenStack 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}': OpenStack 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(openstack, server)
62
+ @dynamic_network.configure(openstack, server)
63
+
64
+ if @vip_network
65
+ @vip_network.configure(openstack, server)
66
+ else
67
+ # If there is no vip network we should disassociate any floating IP
68
+ # currently held by server (as it might have had floating IP before)
69
+ addresses = openstack.addresses
70
+ addresses.each do |address|
71
+ if address.instance_id == server.id
72
+ @logger.info("Disassociating floating IP `#{address.ip}' " \
73
+ "from server `#{server.id}'")
74
+ address.server = nil
75
+ end
76
+ end
77
+ end
78
+ end
79
+
80
+ ##
81
+ # Returns the security groups for this network configuration, or
82
+ # the default security groups if the configuration does not contain
83
+ # security groups
84
+ # @param [Array] default Default security groups
85
+ # @return [Array] security groups
86
+ def security_groups(default)
87
+ if @security_groups.empty? && default
88
+ return default
89
+ else
90
+ return @security_groups
91
+ end
92
+ end
93
+
94
+ ##
95
+ # Extracts the security groups from the network configuration
96
+ # @param [Hash] network_spec Network specification
97
+ # @raise [ArgumentError] if the security groups in the network_spec
98
+ # is not an Array
99
+ def extract_security_groups(spec)
100
+ if spec && spec["cloud_properties"]
101
+ cloud_properties = spec["cloud_properties"]
102
+ if cloud_properties && cloud_properties["security_groups"]
103
+ unless cloud_properties["security_groups"].is_a?(Array)
104
+ raise ArgumentError, "security groups must be an Array"
105
+ end
106
+ @security_groups += cloud_properties["security_groups"]
107
+ end
108
+ end
109
+ end
110
+
111
+ end
112
+
113
+ end
@@ -0,0 +1,109 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ module Bosh::OpenStackCloud
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 server settings in the registry
33
+ # @param [String] server_id OpenStack server id
34
+ # @param [Hash] settings New agent settings
35
+ # @return [Boolean]
36
+ def update_settings(server_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}/servers/#{server_id}/settings"
44
+
45
+ response = @client.put(url, payload, @headers)
46
+
47
+ if response.status != 200
48
+ cloud_error("Cannot update settings for `#{server_id}', " \
49
+ "got HTTP #{response.status}")
50
+ end
51
+
52
+ true
53
+ end
54
+
55
+ ##
56
+ # Read server settings from the registry
57
+ # @param [String] server_id OpenStack server id
58
+ # @return [Hash] Agent settings
59
+ def read_settings(server_id)
60
+ url = "#{@endpoint}/servers/#{server_id}/settings"
61
+
62
+ response = @client.get(url, {}, @headers)
63
+
64
+ if response.status != 200
65
+ cloud_error("Cannot read settings for `#{server_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 `#{server_id}'")
88
+ end
89
+
90
+ ##
91
+ # Delete server settings from the registry
92
+ # @param [String] server_id OpenStack server id
93
+ # @return [Boolean]
94
+ def delete_settings(server_id)
95
+ url = "#{@endpoint}/servers/#{server_id}/settings"
96
+
97
+ response = @client.delete(url, @headers)
98
+
99
+ if response.status != 200
100
+ cloud_error("Cannot delete settings for `#{server_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) 2012 Piston Cloud Computing, Inc.
2
+
3
+ module Bosh
4
+ module OpenStackCloud
5
+ VERSION = "0.0.2"
6
+ end
7
+ end
@@ -0,0 +1,49 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ module Bosh::OpenStackCloud
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 [Fog::Compute::OpenStack] openstack Fog OpenStack Compute client
21
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server to configure
22
+ def configure(openstack, server)
23
+ if @ip.nil?
24
+ cloud_error("No IP provided for vip network `#{@name}'")
25
+ end
26
+
27
+ @logger.info("Associating server `#{server.id}' " \
28
+ "with floating IP `#{@ip}'")
29
+
30
+ # Check if the OpenStack floating IP is allocated. If true, check
31
+ # if it is associated to any server, so we can disassociate it
32
+ # before associating it to the new server.
33
+ address_id = nil
34
+ addresses = openstack.addresses
35
+ addresses.each do |address|
36
+ if address.ip == @ip
37
+ address.server = nil unless address.instance_id.nil?
38
+ address.server = server
39
+ address_id = address.id
40
+ break
41
+ end
42
+ end
43
+ if address_id.nil?
44
+ cloud_error("OpenStack CPI: floating IP #{@ip} not allocated")
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,137 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__)
4
+
5
+ require "rubygems"
6
+ require "bundler"
7
+
8
+ Bundler.setup(:default, :test)
9
+
10
+ require "rspec"
11
+ require "tmpdir"
12
+
13
+ require "cloud/openstack"
14
+
15
+ class OpenStackConfig
16
+ attr_accessor :db, :logger, :uuid
17
+ end
18
+
19
+ os_config = OpenStackConfig.new
20
+ os_config.db = nil # OpenStack CPI doesn't need DB
21
+ os_config.logger = Logger.new(StringIO.new)
22
+ os_config.logger.level = Logger::DEBUG
23
+
24
+ Bosh::Clouds::Config.configure(os_config)
25
+
26
+ def internal_to(*args, &block)
27
+ example = describe *args, &block
28
+ klass = args[0]
29
+ if klass.is_a? Class
30
+ saved_private_instance_methods = klass.private_instance_methods
31
+ example.before do
32
+ klass.class_eval { public *saved_private_instance_methods }
33
+ end
34
+ example.after do
35
+ klass.class_eval { private *saved_private_instance_methods }
36
+ end
37
+ end
38
+ end
39
+
40
+ def mock_cloud_options
41
+ {
42
+ "openstack" => {
43
+ "auth_url" => "http://127.0.0.1:5000/v2.0/tokens",
44
+ "username" => "admin",
45
+ "api_key" => "nova",
46
+ "tenant" => "admin"
47
+ },
48
+ "registry" => {
49
+ "endpoint" => "localhost:42288",
50
+ "user" => "admin",
51
+ "password" => "admin"
52
+ },
53
+ "agent" => {
54
+ "foo" => "bar",
55
+ "baz" => "zaz"
56
+ }
57
+ }
58
+ end
59
+
60
+ def make_cloud(options = nil)
61
+ Bosh::OpenStackCloud::Cloud.new(options || mock_cloud_options)
62
+ end
63
+
64
+ def mock_registry(endpoint = "http://registry:3333")
65
+ registry = mock("registry", :endpoint => endpoint)
66
+ Bosh::OpenStackCloud::RegistryClient.stub!(:new).and_return(registry)
67
+ registry
68
+ end
69
+
70
+ def mock_cloud(options = nil)
71
+ servers = double("servers")
72
+ images = double("images")
73
+ flavors = double("flavors")
74
+ volumes = double("volumes")
75
+ addresses = double("addresses")
76
+ snapshots = double("snapshots")
77
+
78
+ glance = double(Fog::Image)
79
+ Fog::Image.stub(:new).and_return(glance)
80
+
81
+ openstack = double(Fog::Compute)
82
+
83
+ openstack.stub(:servers).and_return(servers)
84
+ openstack.stub(:images).and_return(images)
85
+ openstack.stub(:flavors).and_return(flavors)
86
+ openstack.stub(:volumes).and_return(volumes)
87
+ openstack.stub(:addresses).and_return(addresses)
88
+ openstack.stub(:snapshots).and_return(snapshots)
89
+
90
+ Fog::Compute.stub(:new).and_return(openstack)
91
+
92
+ yield openstack if block_given?
93
+
94
+ Bosh::OpenStackCloud::Cloud.new(options || mock_cloud_options)
95
+ end
96
+
97
+ def mock_glance(options = nil)
98
+ images = double("images")
99
+
100
+ openstack = double(Fog::Compute)
101
+ Fog::Compute.stub(:new).and_return(openstack)
102
+
103
+ glance = double(Fog::Image)
104
+ glance.stub(:images).and_return(images)
105
+
106
+ Fog::Image.stub(:new).and_return(glance)
107
+
108
+ yield glance if block_given?
109
+
110
+ Bosh::OpenStackCloud::Cloud.new(options || mock_cloud_options)
111
+ end
112
+
113
+ def dynamic_network_spec
114
+ { "type" => "dynamic" }
115
+ end
116
+
117
+ def vip_network_spec
118
+ {
119
+ "type" => "vip",
120
+ "ip" => "10.0.0.1"
121
+ }
122
+ end
123
+
124
+ def combined_network_spec
125
+ {
126
+ "network_a" => dynamic_network_spec,
127
+ "network_b" => vip_network_spec
128
+ }
129
+ end
130
+
131
+ def resource_pool_spec
132
+ {
133
+ "key_name" => "test_key",
134
+ "availability_zone" => "foobar-1a",
135
+ "instance_type" => "m1.tiny"
136
+ }
137
+ end