bosh_openstack_cpi 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +40 -0
- data/Rakefile +50 -0
- data/bin/bosh_openstack_console +74 -0
- data/lib/bosh_openstack_cpi.rb +3 -0
- data/lib/cloud/openstack.rb +34 -0
- data/lib/cloud/openstack/cloud.rb +550 -0
- data/lib/cloud/openstack/dynamic_network.rb +26 -0
- data/lib/cloud/openstack/helpers.rb +49 -0
- data/lib/cloud/openstack/network.rb +37 -0
- data/lib/cloud/openstack/network_configurator.rb +113 -0
- data/lib/cloud/openstack/registry_client.rb +109 -0
- data/lib/cloud/openstack/version.rb +7 -0
- data/lib/cloud/openstack/vip_network.rb +49 -0
- data/spec/spec_helper.rb +137 -0
- data/spec/unit/attach_disk_spec.rb +95 -0
- data/spec/unit/cloud_spec.rb +18 -0
- data/spec/unit/configure_networks_spec.rb +83 -0
- data/spec/unit/create_disk_spec.rb +82 -0
- data/spec/unit/create_stemcell_spec.rb +50 -0
- data/spec/unit/create_vm_spec.rb +142 -0
- data/spec/unit/delete_disk_spec.rb +35 -0
- data/spec/unit/delete_stemcell_spec.rb +19 -0
- data/spec/unit/delete_vm_spec.rb +26 -0
- data/spec/unit/detach_disk_spec.rb +67 -0
- data/spec/unit/helpers_spec.rb +34 -0
- data/spec/unit/network_configurator_spec.rb +57 -0
- data/spec/unit/reboot_vm_spec.rb +34 -0
- data/spec/unit/validate_deployment_spec.rb +16 -0
- metadata +190 -0
@@ -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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|