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