bosh_openstack_cpi 0.0.4 → 0.0.5

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Bosh::OpenStackCloud
4
4
  ##
5
- #
5
+ # Represents OpenStack dynamic network: where IaaS sets VM's IP
6
6
  class DynamicNetwork < Network
7
7
 
8
8
  ##
@@ -17,8 +17,10 @@ module Bosh::OpenStackCloud
17
17
  ##
18
18
  # Configures OpenStack dynamic network. Right now it's a no-op,
19
19
  # as dynamic networks are completely managed by OpenStack
20
+ #
20
21
  # @param [Fog::Compute::OpenStack] openstack Fog OpenStack Compute client
21
- # @param [Fog::Compute::OpenStack::Server] server OpenStack server to configure
22
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server to
23
+ # configure
22
24
  def configure(openstack, server)
23
25
  end
24
26
 
@@ -4,44 +4,74 @@ module Bosh::OpenStackCloud
4
4
 
5
5
  module Helpers
6
6
 
7
- DEFAULT_TIMEOUT = 3600
7
+ DEFAULT_TIMEOUT = 600 # Default timeout for target state (in seconds)
8
8
 
9
- #
9
+ ##
10
10
  # Raises CloudError exception
11
11
  #
12
+ # @param [String] message Message about what went wrong
12
13
  def cloud_error(message)
13
14
  @logger.error(message) if @logger
14
15
  raise Bosh::Clouds::CloudError, message
15
16
  end
16
17
 
17
- #
18
+ ##
18
19
  # Waits for a resource to be on a target state
19
20
  #
20
- def wait_resource(resource, start_state, target_state, state_method = :status, timeout = DEFAULT_TIMEOUT)
21
+ # @param [Fog::Model] resource Resource to query
22
+ # @param [Symbol] target_state Resource's state desired
23
+ # @param [Symbol] state_method Resource's method to fetch state
24
+ # @param [Boolean] allow_notfound true if resource could be not found
25
+ # @param [Integer] timeout Timeout for target state (in seconds)
26
+ def wait_resource(resource, target_state, state_method = :status,
27
+ allow_notfound = false, timeout = DEFAULT_TIMEOUT)
21
28
 
22
29
  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
30
+ desc = resource.class.name.split("::").last.to_s + " `" +
31
+ resource.id.to_s + "'"
32
+
33
+ loop do
34
+ task_checkpoint
25
35
 
26
- while state.to_sym != target_state
27
36
  duration = Time.now - started_at
28
37
 
29
38
  if duration > timeout
30
39
  cloud_error("Timed out waiting for #{desc} to be #{target_state}")
31
40
  end
32
41
 
33
- @logger.debug("Waiting for #{desc} to be #{target_state} (#{duration})") if @logger
34
-
35
- sleep(1)
42
+ if @logger
43
+ @logger.debug("Waiting for #{desc} to be #{target_state} " \
44
+ "(#{duration}s)")
45
+ end
36
46
 
47
+ # If resource reload is nil, perhaps it's because resource went away
48
+ # (ie: a destroy operation). Don't raise an exception if this is
49
+ # expected (allow_notfound).
37
50
  if resource.reload.nil?
38
- state = target_state
51
+ break if allow_notfound
52
+ cloud_error("#{desc}: Resource not found")
39
53
  else
40
- state = resource.send(state_method).downcase rescue state_method
54
+ state = resource.send(state_method).downcase.to_sym
41
55
  end
56
+
57
+ # This is not a very strong convention, but some resources
58
+ # have 'error' and 'failed' states, we probably don't want to keep
59
+ # waiting if we're in these states. Alternatively we could introduce a
60
+ # set of 'loop breaker' states but that doesn't seem very helpful
61
+ # at the moment
62
+ if state == :error || state == :failed
63
+ cloud_error("#{desc} state is #{state}, expected #{target_state}")
64
+ end
65
+
66
+ break if state == target_state
67
+
68
+ sleep(1)
42
69
  end
43
70
 
44
- @logger.info("#{desc} is #{target_state} after #{Time.now - started_at}s") if @logger
71
+ if @logger
72
+ total = Time.now - started_at
73
+ @logger.info("#{desc} is now #{target_state}, took #{total}s")
74
+ end
45
75
  end
46
76
 
47
77
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Bosh::OpenStackCloud
4
4
  ##
5
- #
5
+ # Represents OpenStack network.
6
6
  class Network
7
7
  include Helpers
8
8
 
@@ -8,15 +8,13 @@ module Bosh::OpenStackCloud
8
8
  # a number of sanity checks for the network spec provided by director
9
9
  # to make sure we don't apply something OpenStack doesn't understand how to
10
10
  # deal with.
11
- #
12
11
  class NetworkConfigurator
13
12
  include Helpers
14
13
 
15
14
  ##
16
15
  # Creates new network spec
17
16
  #
18
- # @param [Hash] spec raw network spec passed by director
19
- # TODO Add network configuration examples
17
+ # @param [Hash] spec Raw network spec passed by director
20
18
  def initialize(spec)
21
19
  unless spec.is_a?(Hash)
22
20
  raise ArgumentError, "Invalid spec, Hash expected, " \
@@ -37,20 +35,19 @@ module Bosh::OpenStackCloud
37
35
  cloud_error("More than one dynamic network for `#{name}'")
38
36
  else
39
37
  @dynamic_network = DynamicNetwork.new(name, spec)
40
- # only extract security groups for dynamic networks
41
- extract_security_groups(spec)
38
+ @security_groups += extract_security_groups(spec)
42
39
  end
43
40
  when "vip"
44
41
  if @vip_network
45
42
  cloud_error("More than one vip network for `#{name}'")
46
43
  else
47
44
  @vip_network = VipNetwork.new(name, spec)
45
+ @security_groups += extract_security_groups(spec)
48
46
  end
49
47
  else
50
- cloud_error("Invalid network type `#{network_type}': OpenStack CPI " \
51
- "can only handle `dynamic' and `vip' network types")
48
+ cloud_error("Invalid network type `#{network_type}': OpenStack " \
49
+ "CPI can only handle `dynamic' and `vip' network types")
52
50
  end
53
-
54
51
  end
55
52
 
56
53
  if @dynamic_network.nil?
@@ -58,6 +55,12 @@ module Bosh::OpenStackCloud
58
55
  end
59
56
  end
60
57
 
58
+ ##
59
+ # Applies network configuration to the vm
60
+ #
61
+ # @param [Fog::Compute::OpenStack] openstack Fog OpenStack Compute client
62
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server to
63
+ # configure
61
64
  def configure(openstack, server)
62
65
  @dynamic_network.configure(openstack, server)
63
66
 
@@ -81,33 +84,38 @@ module Bosh::OpenStackCloud
81
84
  # Returns the security groups for this network configuration, or
82
85
  # the default security groups if the configuration does not contain
83
86
  # security groups
87
+ #
84
88
  # @param [Array] default Default security groups
85
89
  # @return [Array] security groups
86
90
  def security_groups(default)
87
91
  if @security_groups.empty? && default
88
- return default
92
+ default
89
93
  else
90
- return @security_groups
94
+ @security_groups.sort
91
95
  end
92
96
  end
93
97
 
98
+ private
99
+
94
100
  ##
95
101
  # Extracts the security groups from the network configuration
102
+ #
96
103
  # @param [Hash] network_spec Network specification
104
+ # @return [Array] security groups
97
105
  # @raise [ArgumentError] if the security groups in the network_spec
98
106
  # is not an Array
99
- def extract_security_groups(spec)
100
- if spec && spec["cloud_properties"]
101
- cloud_properties = spec["cloud_properties"]
107
+ def extract_security_groups(network_spec)
108
+ if network_spec && network_spec["cloud_properties"]
109
+ cloud_properties = network_spec["cloud_properties"]
102
110
  if cloud_properties && cloud_properties["security_groups"]
103
111
  unless cloud_properties["security_groups"].is_a?(Array)
104
112
  raise ArgumentError, "security groups must be an Array"
105
113
  end
106
- @security_groups += cloud_properties["security_groups"]
114
+ return cloud_properties["security_groups"]
107
115
  end
108
116
  end
117
+ []
109
118
  end
110
119
 
111
120
  end
112
-
113
121
  end
@@ -1,6 +1,23 @@
1
1
  # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
2
 
3
3
  module Bosh::OpenStackCloud
4
+ ##
5
+ # Represents OpenStack Registry Client. It performs CRUD operations against
6
+ # the OpenStack Registry.
7
+ #
8
+ # Settings example:
9
+ # settings = {
10
+ # "vm" => {
11
+ # "name" => server_name
12
+ # },
13
+ # "agent_id" => agent_id,
14
+ # "networks" => network_spec,
15
+ # "disks" => {
16
+ # "system" => "/dev/vda",
17
+ # "ephemeral" => "/dev/vdb",
18
+ # "persistent" => {"volume_id" => device_name}
19
+ # }
20
+ # }
4
21
  class RegistryClient
5
22
  include Helpers
6
23
 
@@ -8,6 +25,12 @@ module Bosh::OpenStackCloud
8
25
  attr_reader :user
9
26
  attr_reader :password
10
27
 
28
+ ##
29
+ # Creates a new Registry client
30
+ #
31
+ # @param [String] endpoint Registry endpoint URL
32
+ # @param [String] user Registry user
33
+ # @param [String] password Registry password
11
34
  def initialize(endpoint, user, password)
12
35
  @endpoint = endpoint
13
36
 
@@ -17,7 +40,6 @@ module Bosh::OpenStackCloud
17
40
 
18
41
  @user = user
19
42
  @password = password
20
-
21
43
  auth = Base64.encode64("#{@user}:#{@password}").gsub("\n", "")
22
44
 
23
45
  @headers = {
@@ -30,6 +52,7 @@ module Bosh::OpenStackCloud
30
52
 
31
53
  ##
32
54
  # Update server settings in the registry
55
+ #
33
56
  # @param [String] server_id OpenStack server id
34
57
  # @param [Hash] settings New agent settings
35
58
  # @return [Boolean]
@@ -42,8 +65,7 @@ module Bosh::OpenStackCloud
42
65
  payload = Yajl::Encoder.encode(settings)
43
66
  url = "#{@endpoint}/servers/#{server_id}/settings"
44
67
 
45
- response = @client.put(url, payload, @headers)
46
-
68
+ response = @client.put(url, {:body => payload, :header => @headers})
47
69
  if response.status != 200
48
70
  cloud_error("Cannot update settings for `#{server_id}', " \
49
71
  "got HTTP #{response.status}")
@@ -54,27 +76,25 @@ module Bosh::OpenStackCloud
54
76
 
55
77
  ##
56
78
  # Read server settings from the registry
79
+ #
57
80
  # @param [String] server_id OpenStack server id
58
81
  # @return [Hash] Agent settings
59
82
  def read_settings(server_id)
60
83
  url = "#{@endpoint}/servers/#{server_id}/settings"
61
84
 
62
- response = @client.get(url, {}, @headers)
63
-
85
+ response = @client.get(url, {:header => @headers})
64
86
  if response.status != 200
65
87
  cloud_error("Cannot read settings for `#{server_id}', " \
66
88
  "got HTTP #{response.status}")
67
89
  end
68
90
 
69
91
  body = Yajl::Parser.parse(response.body)
70
-
71
92
  unless body.is_a?(Hash)
72
93
  cloud_error("Invalid registry response, Hash expected, " \
73
94
  "got #{body.class}: #{body}")
74
95
  end
75
96
 
76
97
  settings = Yajl::Parser.parse(body["settings"])
77
-
78
98
  unless settings.is_a?(Hash)
79
99
  cloud_error("Invalid settings format, " \
80
100
  "Hash expected, got #{settings.class}: " \
@@ -82,20 +102,19 @@ module Bosh::OpenStackCloud
82
102
  end
83
103
 
84
104
  settings
85
-
86
105
  rescue Yajl::ParseError
87
106
  cloud_error("Cannot parse settings for `#{server_id}'")
88
107
  end
89
108
 
90
109
  ##
91
110
  # Delete server settings from the registry
111
+ #
92
112
  # @param [String] server_id OpenStack server id
93
113
  # @return [Boolean]
94
114
  def delete_settings(server_id)
95
115
  url = "#{@endpoint}/servers/#{server_id}/settings"
96
116
 
97
- response = @client.delete(url, @headers)
98
-
117
+ response = @client.delete(url, {:header => @headers})
99
118
  if response.status != 200
100
119
  cloud_error("Cannot delete settings for `#{server_id}', " \
101
120
  "got HTTP #{response.status}")
@@ -105,5 +124,4 @@ module Bosh::OpenStackCloud
105
124
  end
106
125
 
107
126
  end
108
-
109
127
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Bosh
4
4
  module OpenStackCloud
5
- VERSION = "0.0.4"
5
+ VERSION = "0.0.5"
6
6
  end
7
7
  end
@@ -2,7 +2,8 @@
2
2
 
3
3
  module Bosh::OpenStackCloud
4
4
  ##
5
- #
5
+ # Represents OpenStack vip network: where users sets VM's IP (floating IP's
6
+ # in OpenStack)
6
7
  class VipNetwork < Network
7
8
 
8
9
  ##
@@ -15,33 +16,31 @@ module Bosh::OpenStackCloud
15
16
  end
16
17
 
17
18
  ##
18
- # Configures vip network
19
+ # Configures OpenStack vip network
19
20
  #
20
21
  # @param [Fog::Compute::OpenStack] openstack Fog OpenStack Compute client
21
- # @param [Fog::Compute::OpenStack::Server] server OpenStack server to configure
22
+ # @param [Fog::Compute::OpenStack::Server] server OpenStack server to
23
+ # configure
22
24
  def configure(openstack, server)
23
25
  if @ip.nil?
24
26
  cloud_error("No IP provided for vip network `#{@name}'")
25
27
  end
26
28
 
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
29
+ # Check if the OpenStack floating IP is allocated. If true, disassociate
30
+ # it from any server before associating it to the new server
31
+ address = openstack.addresses.find { |a| a.ip == @ip }
32
+ if address
33
+ unless address.instance_id.nil?
34
+ @logger.info("Disassociating floating IP `#{@ip}' " \
35
+ "from server `#{address.instance_id}'")
36
+ address.server = nil
41
37
  end
42
- end
43
- if address_id.nil?
44
- cloud_error("OpenStack CPI: floating IP #{@ip} not allocated")
38
+
39
+ @logger.info("Associating server `#{server.id}' " \
40
+ "with floating IP `#{@ip}'")
41
+ address.server = server
42
+ else
43
+ cloud_error("Floating IP #{@ip} not allocated")
45
44
  end
46
45
  end
47
46
 
@@ -0,0 +1,14 @@
1
+ ---
2
+ openstack:
3
+ auth_url: http://127.0.0.1:5000/v2.0/tokens
4
+ username: foo
5
+ api_key: bar
6
+ tenant: bosh
7
+ region:
8
+ default_key_name: default
9
+ default_security_groups: ["default"]
10
+
11
+ registry:
12
+ endpoint: http://127.0.0.1:25695
13
+ user: admin
14
+ password: admin
@@ -0,0 +1,119 @@
1
+ # Copyright (c) 2012 Piston Cloud Computing, Inc.
2
+
3
+ require File.expand_path("../../spec_helper", __FILE__)
4
+ require "tempfile"
5
+
6
+ ##
7
+ # BOSH OpenStack CPI Integration tests
8
+ #
9
+ # To run integration test:
10
+ # 1. Prepare OpenStack CPI configuration file (a sample can be found at
11
+ # spec/assets/sample_config.yml);
12
+ # 2. Set CPI_CONFIG_FILE env variable to point to OpenStack config file;
13
+ # 3. Download a public OpenStack stemcell: 'bosh download public stemcell ...';
14
+ # 4. Untar the OpenStack stemcell, you'll find and image file;
15
+ # 5. Set STEMCELL_FILE env variable to point to OpenStack image file;
16
+ # 6. Optional: Set FLOATING_IP env variable with an allocated OpenStack
17
+ # Floating IP if you want to test vip networks;
18
+ # 7. Start OpenStack Registry manually (see bosh/openstack_registry);
19
+ # 8. Run 'bundle exec rspec --color spec/integration/cpi_test.rb'.
20
+ describe Bosh::OpenStackCloud::Cloud do
21
+
22
+ before(:each) do
23
+ unless ENV["CPI_CONFIG_FILE"]
24
+ raise "Please provide CPI_CONFIG_FILE environment variable"
25
+ end
26
+ unless ENV["STEMCELL_FILE"]
27
+ raise "Please provide STEMCELL_UUID environment variable"
28
+ end
29
+ @config = YAML.load_file(ENV["CPI_CONFIG_FILE"])
30
+ @logger = Logger.new(STDOUT)
31
+ Bosh::Clouds::Config.stub(:task_checkpoint)
32
+ end
33
+
34
+ let(:cpi) do
35
+ cpi = Bosh::OpenStackCloud::Cloud.new(@config)
36
+ cpi.logger = @logger
37
+
38
+ # As we inject the configuration file from the outside, we don't care
39
+ # about spinning up the registry ourselves. However we don't want to bother
40
+ # OpenStack at all if registry is not working, so just in case we perform
41
+ # a test health check against whatever has been provided.
42
+ cpi.registry.update_settings("foo", { "bar" => "baz" })
43
+ cpi.registry.read_settings("foo").should == { "bar" => "baz"}
44
+ cpi.registry.delete_settings("foo")
45
+
46
+ cpi
47
+ end
48
+
49
+ it "exercises a VM lifecycle" do
50
+ unique_name = UUIDTools::UUID.random_create.to_s
51
+ cpi.stub(:generate_unique_name).and_return(unique_name)
52
+
53
+ stemcell_id = cpi.create_stemcell(
54
+ ENV["STEMCELL_FILE"],
55
+ {
56
+ "name" => "bosh-stemcell",
57
+ "version" => "0.6.7",
58
+ "infrastructure" => "openstack",
59
+ "disk_format" => "qcow2",
60
+ "container_format" => "bare"
61
+ })
62
+
63
+ server_id = cpi.create_vm(
64
+ "agent-007", stemcell_id,
65
+ { "instance_type" => "m1.small" },
66
+ { "default" => { "type" => "dynamic" }},
67
+ [], { "key" => "value" })
68
+
69
+ server_id.should_not be_nil
70
+
71
+ settings = cpi.registry.read_settings("vm-#{unique_name}")
72
+ settings["vm"].should be_a(Hash)
73
+ settings["vm"]["name"].should == "vm-#{unique_name}"
74
+ settings["agent_id"].should == "agent-007"
75
+ settings["networks"].should == { "default" => { "type" => "dynamic" }}
76
+ settings["disks"].should == {
77
+ "system" => "/dev/vda",
78
+ "ephemeral" => "/dev/vdb",
79
+ "persistent" => {}
80
+ }
81
+ settings["env"].should == { "key" => "value" }
82
+
83
+ volume_id = cpi.create_disk(1024)
84
+ volume_id.should_not be_nil
85
+
86
+ cpi.attach_disk(server_id, volume_id)
87
+ settings = cpi.registry.read_settings("vm-#{unique_name}")
88
+ settings["disks"]["persistent"].should == { volume_id => "/dev/vdc" }
89
+
90
+ cpi.reboot_vm(server_id)
91
+
92
+ if ENV["FLOATING_IP"]
93
+ cpi.configure_networks(server_id,
94
+ { "default" => { "type" => "dynamic" },
95
+ "floating" => { "type" => "vip",
96
+ "ip" => ENV["FLOATING_IP"] }
97
+ })
98
+ settings = cpi.registry.read_settings("vm-#{unique_name}")
99
+ settings["networks"].should == {
100
+ "default" => { "type" => "dynamic" },
101
+ "floating" => { "type" => "vip",
102
+ "ip" => ENV["FLOATING_IP"] }
103
+ }
104
+ end
105
+
106
+ cpi.detach_disk(server_id, volume_id)
107
+ settings = cpi.registry.read_settings("vm-#{unique_name}")
108
+ settings["disks"]["persistent"].should == {}
109
+
110
+ cpi.delete_vm(server_id)
111
+ cpi.delete_disk(volume_id)
112
+ cpi.delete_stemcell(stemcell_id)
113
+
114
+ expect {
115
+ cpi.registry.read_settings("vm-#{unique_name}")
116
+ }.to raise_error(/HTTP 404/)
117
+ end
118
+
119
+ end