bosh_openstack_cpi 0.0.2

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,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