kitchen-openstack 7.0.1 → 8.0.0
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.
- checksums.yaml +4 -4
- data/README.md +477 -81
- data/lib/kitchen/driver/openstack/clouds.rb +124 -23
- data/lib/kitchen/driver/openstack/config.rb +97 -30
- data/lib/kitchen/driver/openstack/helpers.rb +44 -5
- data/lib/kitchen/driver/openstack/networking.rb +127 -32
- data/lib/kitchen/driver/openstack/server_helper.rb +92 -20
- data/lib/kitchen/driver/openstack/volume.rb +121 -25
- data/lib/kitchen/driver/openstack.rb +99 -15
- data/lib/kitchen/driver/openstack_version.rb +7 -1
- metadata +2 -2
|
@@ -28,47 +28,100 @@ module Kitchen
|
|
|
28
28
|
class Openstack < Kitchen::Driver::Base
|
|
29
29
|
# Floating IP allocation and IP address resolution
|
|
30
30
|
module Networking
|
|
31
|
+
# Serializes floating IP selection across the driver instances that
|
|
32
|
+
# Test Kitchen runs in parallel. Without it, two concurrent converges
|
|
33
|
+
# can pick the same free address out of the pool and one of them fails
|
|
34
|
+
# to associate it.
|
|
35
|
+
#
|
|
36
|
+
# @return [Mutex]
|
|
31
37
|
IP_POOL_LOCK = Mutex.new
|
|
32
38
|
|
|
33
39
|
private
|
|
34
40
|
|
|
41
|
+
# Attaches a floating IP from the named pool to the server, allocating
|
|
42
|
+
# a fresh one first when `:allocate_floating_ip` is set.
|
|
43
|
+
#
|
|
44
|
+
# Held under {IP_POOL_LOCK} for the whole select-then-attach sequence,
|
|
45
|
+
# so a concurrent converge cannot claim the same address in between.
|
|
46
|
+
#
|
|
47
|
+
# @param server [Fog::OpenStack::Compute::Server] the server to attach to
|
|
48
|
+
# @param pool [String] name of the floating IP pool / external network
|
|
49
|
+
# @return [void]
|
|
50
|
+
# @raise [Kitchen::ActionFailed] if the pool does not exist, or holds no
|
|
51
|
+
# free addresses
|
|
35
52
|
def attach_ip_from_pool(server, pool)
|
|
36
53
|
IP_POOL_LOCK.synchronize do
|
|
37
54
|
info "Attaching floating IP from <#{pool}> pool"
|
|
38
|
-
if config[:allocate_floating_ip]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
resp = network.create_floating_ip(network_id)
|
|
44
|
-
ip = resp.body["floatingip"]["floating_ip_address"]
|
|
45
|
-
info "Created floating IP <#{ip}> from <#{pool}> pool"
|
|
46
|
-
config[:floating_ip] = ip
|
|
47
|
-
else
|
|
48
|
-
free_addrs = compute.addresses.map do |i|
|
|
49
|
-
i.ip if i.fixed_ip.nil? && i.instance_id.nil? && i.pool == pool
|
|
50
|
-
end.compact
|
|
51
|
-
if free_addrs.empty?
|
|
52
|
-
raise ActionFailed, "No available IPs in pool <#{pool}>"
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
config[:floating_ip] = free_addrs[0]
|
|
56
|
-
end
|
|
55
|
+
config[:floating_ip] = if config[:allocate_floating_ip]
|
|
56
|
+
allocate_ip_from_pool(pool)
|
|
57
|
+
else
|
|
58
|
+
free_ip_from_pool(pool)
|
|
59
|
+
end
|
|
57
60
|
attach_ip(server, config[:floating_ip])
|
|
58
61
|
end
|
|
59
62
|
end
|
|
60
63
|
|
|
64
|
+
# Asks Neutron for a brand new floating IP on the named external
|
|
65
|
+
# network.
|
|
66
|
+
#
|
|
67
|
+
# @param pool [String] name of the external network
|
|
68
|
+
# @return [String] the newly allocated floating IP
|
|
69
|
+
# @raise [Kitchen::ActionFailed] if no network matches `pool`
|
|
70
|
+
def allocate_ip_from_pool(pool)
|
|
71
|
+
net = network
|
|
72
|
+
networks = net.list_networks(name: pool).body["networks"]
|
|
73
|
+
if networks.nil? || networks.empty?
|
|
74
|
+
raise ActionFailed, "Floating IP pool <#{pool}> not found"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
resp = net.create_floating_ip(networks[0]["id"])
|
|
78
|
+
ip = resp.body["floatingip"]["floating_ip_address"]
|
|
79
|
+
info "Created floating IP <#{ip}> from <#{pool}> pool"
|
|
80
|
+
ip
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Picks an already-allocated but unattached floating IP out of the pool.
|
|
84
|
+
#
|
|
85
|
+
# @param pool [String] name of the floating IP pool
|
|
86
|
+
# @return [String] a free floating IP
|
|
87
|
+
# @raise [Kitchen::ActionFailed] if every address in the pool is in use
|
|
88
|
+
def free_ip_from_pool(pool)
|
|
89
|
+
# `find`, not `map`+`compact`: this runs while holding IP_POOL_LOCK,
|
|
90
|
+
# which serializes parallel converges, so it should stop at the first
|
|
91
|
+
# usable address rather than building a throwaway list of all of them.
|
|
92
|
+
free = compute.addresses.find do |i|
|
|
93
|
+
i.fixed_ip.nil? && i.instance_id.nil? && i.pool == pool
|
|
94
|
+
end
|
|
95
|
+
raise ActionFailed, "No available IPs in pool <#{pool}>" if free.nil?
|
|
96
|
+
|
|
97
|
+
free.ip
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Associates a floating IP with a server.
|
|
101
|
+
#
|
|
102
|
+
# @param server [Fog::OpenStack::Compute::Server] the server
|
|
103
|
+
# @param ip [String] the floating IP to attach
|
|
104
|
+
# @return [void]
|
|
61
105
|
def attach_ip(server, ip)
|
|
62
106
|
info "Attaching floating IP <#{ip}>"
|
|
63
107
|
server.associate_address ip
|
|
64
108
|
end
|
|
65
109
|
|
|
110
|
+
# Reads the server's public and private addresses.
|
|
111
|
+
#
|
|
112
|
+
# Deployments without the floating IP extension answer the dedicated
|
|
113
|
+
# accessors with 404/403, so fall back to picking the lists out of the
|
|
114
|
+
# generic addresses hash.
|
|
115
|
+
#
|
|
116
|
+
# @see https://github.com/fog/fog/issues/2160
|
|
117
|
+
# @param server [Fog::OpenStack::Compute::Server] the server
|
|
118
|
+
# @return [Array(Array<String>, Array<String>)] public and private
|
|
119
|
+
# addresses, either of which may be nil
|
|
66
120
|
def get_public_private_ips(server)
|
|
67
121
|
begin
|
|
68
122
|
pub = server.public_ip_addresses
|
|
69
123
|
priv = server.private_ip_addresses
|
|
70
124
|
rescue Fog::OpenStack::Compute::NotFound, Excon::Errors::Forbidden
|
|
71
|
-
# See Fog issue: https://github.com/fog/fog/issues/2160
|
|
72
125
|
addrs = server.addresses
|
|
73
126
|
addrs["public"] && pub = addrs["public"].map { |i| i["addr"] }
|
|
74
127
|
addrs["private"] && priv = addrs["private"].map { |i| i["addr"] }
|
|
@@ -76,6 +129,19 @@ module Kitchen
|
|
|
76
129
|
[pub, priv]
|
|
77
130
|
end
|
|
78
131
|
|
|
132
|
+
# Determines the address Test Kitchen should connect to.
|
|
133
|
+
#
|
|
134
|
+
# Resolution order:
|
|
135
|
+
#
|
|
136
|
+
# 1. an explicitly configured `:floating_ip`
|
|
137
|
+
# 2. the first address on `:openstack_network_name`, if configured
|
|
138
|
+
# 3. `:public_ip_order` into the public addresses
|
|
139
|
+
# 4. `:private_ip_order` into the private addresses
|
|
140
|
+
#
|
|
141
|
+
# @param server [Fog::OpenStack::Compute::Server] the server
|
|
142
|
+
# @return [String] the address to connect to
|
|
143
|
+
# @raise [Kitchen::ActionFailed] if network information never arrives,
|
|
144
|
+
# or no address of the requested family can be found
|
|
79
145
|
def get_ip(server)
|
|
80
146
|
if config[:floating_ip]
|
|
81
147
|
debug "Using floating ip: #{config[:floating_ip]}"
|
|
@@ -91,11 +157,7 @@ module Kitchen
|
|
|
91
157
|
raise ActionFailed, "Could not get network information (timed out)"
|
|
92
158
|
end
|
|
93
159
|
|
|
94
|
-
|
|
95
|
-
if config[:openstack_network_name]
|
|
96
|
-
debug "Using configured net: #{config[:openstack_network_name]}"
|
|
97
|
-
return filter_ips(server.addresses[config[:openstack_network_name]]).first["addr"]
|
|
98
|
-
end
|
|
160
|
+
return ip_from_named_network(server) if config[:openstack_network_name]
|
|
99
161
|
|
|
100
162
|
pub, priv = get_public_private_ips(server)
|
|
101
163
|
priv = server.ip_addresses if Array(pub).empty? && Array(priv).empty?
|
|
@@ -105,6 +167,33 @@ module Kitchen
|
|
|
105
167
|
raise(ActionFailed, "Could not find an IP")
|
|
106
168
|
end
|
|
107
169
|
|
|
170
|
+
# Picks the first usable address off the network named by
|
|
171
|
+
# `:openstack_network_name`.
|
|
172
|
+
#
|
|
173
|
+
# @param server [Fog::OpenStack::Compute::Server] the server
|
|
174
|
+
# @return [String] the address
|
|
175
|
+
# @raise [Kitchen::ActionFailed] if the server is not on that network,
|
|
176
|
+
# or has no address there of the configured IP family
|
|
177
|
+
def ip_from_named_network(server)
|
|
178
|
+
name = config[:openstack_network_name]
|
|
179
|
+
debug "Using configured net: #{name}"
|
|
180
|
+
|
|
181
|
+
addresses = server.addresses[name]
|
|
182
|
+
raise ActionFailed, "Server is not attached to network <#{name}>" if addresses.nil?
|
|
183
|
+
|
|
184
|
+
matching = filter_ips(addresses)
|
|
185
|
+
if matching.empty?
|
|
186
|
+
raise ActionFailed,
|
|
187
|
+
"No #{config[:use_ipv6] ? "IPv6" : "IPv4"} address found on network <#{name}>"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
matching.first["addr"]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Keeps only the addresses matching the configured IP family.
|
|
194
|
+
#
|
|
195
|
+
# @param addresses [Array<Hash>] address hashes, each with an `"addr"` key
|
|
196
|
+
# @return [Array<Hash>] the matching subset
|
|
108
197
|
def filter_ips(addresses)
|
|
109
198
|
if config[:use_ipv6]
|
|
110
199
|
addresses.select { |i| IPAddr.new(i["addr"]).ipv6? }
|
|
@@ -113,15 +202,21 @@ module Kitchen
|
|
|
113
202
|
end
|
|
114
203
|
end
|
|
115
204
|
|
|
205
|
+
# Normalizes and filters public/private address lists to the configured
|
|
206
|
+
# IP family.
|
|
207
|
+
#
|
|
208
|
+
# @param pub [Array<String>, String, nil] public addresses
|
|
209
|
+
# @param priv [Array<String>, String, nil] private addresses
|
|
210
|
+
# @return [Array(Array<String>, Array<String>)] filtered public and
|
|
211
|
+
# private address lists
|
|
116
212
|
def parse_ips(pub, priv)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
213
|
+
# `select`, not `select!`: Array(x) returns x itself when x is already
|
|
214
|
+
# an Array, so filtering in place would edit the caller's list -- and
|
|
215
|
+
# the caller's list here is the Fog server model's own address data.
|
|
216
|
+
wanted = config[:use_ipv6] ? :ipv6? : :ipv4?
|
|
217
|
+
[Array(pub), Array(priv)].map do |addrs|
|
|
218
|
+
addrs.select { |i| IPAddr.new(i).public_send(wanted) }
|
|
123
219
|
end
|
|
124
|
-
[pub, priv]
|
|
125
220
|
end
|
|
126
221
|
end
|
|
127
222
|
end
|
|
@@ -26,8 +26,29 @@ module Kitchen
|
|
|
26
26
|
class Openstack < Kitchen::Driver::Base
|
|
27
27
|
# Server creation and resource finders (image, flavor, network)
|
|
28
28
|
module ServerHelper
|
|
29
|
+
# Config keys copied onto the server definition when set, each routed
|
|
30
|
+
# through {#optional_config}.
|
|
31
|
+
#
|
|
32
|
+
# @return [Array<Symbol>]
|
|
33
|
+
OPTIONAL_SERVER_KEYS = %i{
|
|
34
|
+
security_groups
|
|
35
|
+
key_name
|
|
36
|
+
user_data
|
|
37
|
+
config_drive
|
|
38
|
+
metadata
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
29
41
|
private
|
|
30
42
|
|
|
43
|
+
# Builds the Nova server definition and submits it.
|
|
44
|
+
#
|
|
45
|
+
# Fog's `bootstrap`/`setup` helpers are deliberately not used: they
|
|
46
|
+
# require a public IP address, which is not guaranteed to exist on
|
|
47
|
+
# every OpenStack deployment.
|
|
48
|
+
#
|
|
49
|
+
# @return [Fog::OpenStack::Compute::Server] the newly created server
|
|
50
|
+
# @raise [Kitchen::ActionFailed] on mutually exclusive or unresolvable
|
|
51
|
+
# configuration
|
|
31
52
|
def create_server
|
|
32
53
|
server_def = init_configuration
|
|
33
54
|
raise(ActionFailed, "Cannot specify both network_ref and network_id") if config[:network_id] && config[:network_ref]
|
|
@@ -44,17 +65,7 @@ module Kitchen
|
|
|
44
65
|
end
|
|
45
66
|
end
|
|
46
67
|
|
|
47
|
-
|
|
48
|
-
server_def[:block_device_mapping] = get_bdm(config)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
%i{
|
|
52
|
-
security_groups
|
|
53
|
-
key_name
|
|
54
|
-
user_data
|
|
55
|
-
config_drive
|
|
56
|
-
metadata
|
|
57
|
-
}.each do |c|
|
|
68
|
+
OPTIONAL_SERVER_KEYS.each do |c|
|
|
58
69
|
server_def[c] = optional_config(c) if config[c]
|
|
59
70
|
end
|
|
60
71
|
|
|
@@ -64,12 +75,25 @@ module Kitchen
|
|
|
64
75
|
server_def[:user_data] = YAML.dump(Kitchen::Util.stringified_hash(config[:cloud_config])).gsub(/^---\n/, "#cloud-config\n")
|
|
65
76
|
end
|
|
66
77
|
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
78
|
+
# Last, because this is the only step that creates a resource. Every
|
|
79
|
+
# check above is local config validation, and running them first means
|
|
80
|
+
# a bad security_groups or user_data value cannot strand a Cinder
|
|
81
|
+
# volume whose id exists only in the server_def about to be discarded.
|
|
82
|
+
if config[:block_device_mapping]
|
|
83
|
+
server_def[:block_device_mapping] = get_bdm(config)
|
|
84
|
+
end
|
|
85
|
+
|
|
70
86
|
compute.servers.create(server_def)
|
|
71
87
|
end
|
|
72
88
|
|
|
89
|
+
# Builds the mandatory part of the server definition.
|
|
90
|
+
#
|
|
91
|
+
# `*_id` and `*_ref` are mutually exclusive: an id is used verbatim, a
|
|
92
|
+
# ref is resolved by name, id or regex through {#find_matching}.
|
|
93
|
+
#
|
|
94
|
+
# @return [Hash] name, image, flavor and availability zone
|
|
95
|
+
# @raise [Kitchen::ActionFailed] if both an id and a ref are given for
|
|
96
|
+
# the image or the flavor, or if either cannot be resolved
|
|
73
97
|
def init_configuration
|
|
74
98
|
raise(ActionFailed, "Cannot specify both image_ref and image_id") if config[:image_id] && config[:image_ref]
|
|
75
99
|
raise(ActionFailed, "Cannot specify both flavor_ref and flavor_id") if config[:flavor_id] && config[:flavor_ref]
|
|
@@ -82,17 +106,39 @@ module Kitchen
|
|
|
82
106
|
}
|
|
83
107
|
end
|
|
84
108
|
|
|
109
|
+
# Resolves one optional server setting to the value Nova expects.
|
|
110
|
+
#
|
|
111
|
+
# @param c [Symbol] the config key
|
|
112
|
+
# @return [Object] the resolved value
|
|
113
|
+
# @raise [Kitchen::ActionFailed] if `:security_groups` is not a list, or
|
|
114
|
+
# if the `:user_data` file does not exist
|
|
85
115
|
def optional_config(c)
|
|
86
116
|
case c
|
|
87
117
|
when :security_groups
|
|
88
|
-
|
|
118
|
+
unless config[c].is_a?(Array)
|
|
119
|
+
raise ActionFailed, "The security_groups config must be an array, got #{config[c].class}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
config[c]
|
|
89
123
|
when :user_data
|
|
90
|
-
|
|
124
|
+
# Booting without the user_data the user asked for produces a
|
|
125
|
+
# server that looks fine and behaves wrongly, so a missing file is
|
|
126
|
+
# fatal rather than silently ignored.
|
|
127
|
+
unless File.exist?(config[c])
|
|
128
|
+
raise ActionFailed, "The user_data file <#{config[c]}> does not exist"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
File.read(config[c])
|
|
91
132
|
else
|
|
92
133
|
config[c]
|
|
93
134
|
end
|
|
94
135
|
end
|
|
95
136
|
|
|
137
|
+
# Finds a Glance image by id, name or regex.
|
|
138
|
+
#
|
|
139
|
+
# @param image_ref [String] id, name, or `/regex/`
|
|
140
|
+
# @return [Object] the matching image
|
|
141
|
+
# @raise [Kitchen::ActionFailed] if nothing matches
|
|
96
142
|
def find_image(image_ref)
|
|
97
143
|
image = find_matching(compute.images, image_ref)
|
|
98
144
|
raise(ActionFailed, "Image not found") unless image
|
|
@@ -101,6 +147,11 @@ module Kitchen
|
|
|
101
147
|
image
|
|
102
148
|
end
|
|
103
149
|
|
|
150
|
+
# Finds a Nova flavor by id, name or regex.
|
|
151
|
+
#
|
|
152
|
+
# @param flavor_ref [String] id, name, or `/regex/`
|
|
153
|
+
# @return [Object] the matching flavor
|
|
154
|
+
# @raise [Kitchen::ActionFailed] if nothing matches
|
|
104
155
|
def find_flavor(flavor_ref)
|
|
105
156
|
flavor = find_matching(compute.flavors, flavor_ref)
|
|
106
157
|
raise(ActionFailed, "Flavor not found") unless flavor
|
|
@@ -109,6 +160,11 @@ module Kitchen
|
|
|
109
160
|
flavor
|
|
110
161
|
end
|
|
111
162
|
|
|
163
|
+
# Finds a Neutron network by id, name or regex.
|
|
164
|
+
#
|
|
165
|
+
# @param network_ref [String] id, name, or `/regex/`
|
|
166
|
+
# @return [Object] the matching network
|
|
167
|
+
# @raise [Kitchen::ActionFailed] if nothing matches
|
|
112
168
|
def find_network(network_ref)
|
|
113
169
|
net = find_matching(network.networks.all, network_ref)
|
|
114
170
|
raise(ActionFailed, "Network not found") unless net
|
|
@@ -117,15 +173,31 @@ module Kitchen
|
|
|
117
173
|
net
|
|
118
174
|
end
|
|
119
175
|
|
|
176
|
+
# Picks a resource out of a Fog collection.
|
|
177
|
+
#
|
|
178
|
+
# A ref wrapped in forward slashes is treated as a regular expression
|
|
179
|
+
# matched against the resource name; anything else is compared against
|
|
180
|
+
# the id first and then the name, so an exact id always wins.
|
|
181
|
+
#
|
|
182
|
+
# @example an exact name
|
|
183
|
+
# find_matching(compute.images, "ubuntu-24.04")
|
|
184
|
+
# @example a regular expression
|
|
185
|
+
# find_matching(compute.images, "/^ubuntu-24\\.04/")
|
|
186
|
+
#
|
|
187
|
+
# @param collection [Enumerable] the Fog collection to search
|
|
188
|
+
# @param name [String] id, name, or `/regex/`
|
|
189
|
+
# @return [Object, nil] the first match, or nil
|
|
120
190
|
def find_matching(collection, name)
|
|
121
191
|
name = name.to_s
|
|
122
192
|
if name.start_with?("/") && name.end_with?("/")
|
|
123
193
|
regex = Regexp.new(name[1...-1])
|
|
124
|
-
# check for regex name match
|
|
125
|
-
|
|
194
|
+
# check for regex name match, skipping unnamed resources; Neutron
|
|
195
|
+
# networks in particular are allowed to have no name
|
|
196
|
+
collection.each { |single| return single if single.name && regex.match?(single.name) }
|
|
126
197
|
else
|
|
127
|
-
# check for exact id match
|
|
128
|
-
|
|
198
|
+
# check for exact id match; ids come back as integers on some
|
|
199
|
+
# deployments, so compare as strings
|
|
200
|
+
collection.each { |single| return single if single.id.to_s == name }
|
|
129
201
|
# check for exact name match
|
|
130
202
|
collection.each { |single| return single if single.name == name }
|
|
131
203
|
end
|
|
@@ -26,28 +26,70 @@ module Kitchen
|
|
|
26
26
|
# A class to allow the Kitchen Openstack driver
|
|
27
27
|
# to use Openstack volumes
|
|
28
28
|
#
|
|
29
|
+
# Instances of this class translate a `block_device_mapping` config hash
|
|
30
|
+
# into the shape Nova expects, creating a Cinder volume first when the
|
|
31
|
+
# mapping asks for one.
|
|
32
|
+
#
|
|
29
33
|
# @author Liam Haworth <liam.haworth@bluereef.com.au>
|
|
30
34
|
class Volume
|
|
35
|
+
# Seconds to wait for a newly created volume to become available when
|
|
36
|
+
# the block device mapping does not specify `creation_timeout`.
|
|
37
|
+
#
|
|
38
|
+
# @return [Integer]
|
|
31
39
|
DEFAULT_CREATION_TIMEOUT = 60
|
|
32
40
|
|
|
41
|
+
# Block device mapping keys forwarded verbatim to Cinder's create call.
|
|
42
|
+
#
|
|
43
|
+
# @return [Array<Symbol>]
|
|
44
|
+
VANILLA_VOLUME_OPTIONS = %i{
|
|
45
|
+
snapshot_id
|
|
46
|
+
imageRef
|
|
47
|
+
volume_type
|
|
48
|
+
source_volid
|
|
49
|
+
availability_zone
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
# @param logger [Kitchen::Logger] logger to report volume progress to
|
|
33
53
|
def initialize(logger)
|
|
34
54
|
@logger = logger
|
|
35
55
|
end
|
|
36
56
|
|
|
57
|
+
# Builds a Cinder connection.
|
|
58
|
+
#
|
|
59
|
+
# @param openstack_server [Hash] Fog connection settings
|
|
60
|
+
# @return [Fog::OpenStack::Volume] a Cinder service object
|
|
37
61
|
def volume(openstack_server)
|
|
38
62
|
Fog::OpenStack::Volume.new(openstack_server)
|
|
39
63
|
end
|
|
40
64
|
|
|
65
|
+
# Creates a Cinder volume and blocks until it is available.
|
|
66
|
+
#
|
|
67
|
+
# @param config [Hash] the driver config, read for `:server_name` and
|
|
68
|
+
# `:block_device_mapping`
|
|
69
|
+
# @param os [Hash] Fog connection settings
|
|
70
|
+
# @return [String] the id of the newly created volume
|
|
71
|
+
# @raise [Kitchen::ActionFailed] if a timeout is not a number, if the
|
|
72
|
+
# volume cannot be found after creation, or if it enters an `error`
|
|
73
|
+
# state
|
|
74
|
+
# @raise [Fog::Errors::TimeoutError] if the volume is not available
|
|
75
|
+
# before `:creation_timeout` elapses
|
|
41
76
|
def create_volume(config, os)
|
|
42
|
-
opt = {}
|
|
43
77
|
bdm = config[:block_device_mapping]
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
78
|
+
|
|
79
|
+
# Read both timeouts before anything is created. They are pure config
|
|
80
|
+
# parsing, so a bad value should cost the user an error message, not
|
|
81
|
+
# an orphaned volume that `kitchen destroy` cannot see.
|
|
82
|
+
creation_timeout = timeout_value(bdm, :creation_timeout, DEFAULT_CREATION_TIMEOUT)
|
|
83
|
+
attach_timeout = timeout_value(bdm, :attach_timeout, 0)
|
|
84
|
+
|
|
85
|
+
opt = VANILLA_VOLUME_OPTIONS.select { |o| bdm[o] }.to_h { |key| [key, bdm[key]] }
|
|
86
|
+
|
|
87
|
+
# Build the Cinder connection once and reuse it for the readiness
|
|
88
|
+
# lookup, rather than authenticating to Keystone a second time.
|
|
89
|
+
volume_service = volume(os)
|
|
90
|
+
|
|
49
91
|
@logger.info "Creating Volume..."
|
|
50
|
-
resp =
|
|
92
|
+
resp = volume_service
|
|
51
93
|
.create_volume(
|
|
52
94
|
"#{config[:server_name]}-volume",
|
|
53
95
|
"#{config[:server_name]} volume",
|
|
@@ -56,41 +98,95 @@ module Kitchen
|
|
|
56
98
|
)
|
|
57
99
|
vol_id = resp[:body]["volume"]["id"]
|
|
58
100
|
|
|
59
|
-
|
|
60
|
-
vol_model = volume(os).volumes.first { |x| x.id == vol_id }
|
|
101
|
+
wait_for_volume(volume_service, vol_id, creation_timeout, attach_timeout)
|
|
61
102
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
103
|
+
vol_id
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Resolves the block device mapping to hand to Nova, creating the
|
|
107
|
+
# backing volume first if `:make_volume` is set.
|
|
108
|
+
#
|
|
109
|
+
# The returned hash is a copy: the driver's own config is never
|
|
110
|
+
# mutated, so a retried `create` sees the same input it did the first
|
|
111
|
+
# time.
|
|
112
|
+
#
|
|
113
|
+
# @param config [Hash] the driver config
|
|
114
|
+
# @param os [Hash] Fog connection settings
|
|
115
|
+
# @return [Hash] a block device mapping suitable for Nova
|
|
116
|
+
def get_bdm(config, os)
|
|
117
|
+
bdm = config[:block_device_mapping].dup
|
|
118
|
+
bdm[:volume_id] = create_volume(config, os) if bdm[:make_volume]
|
|
119
|
+
bdm.delete(:make_volume)
|
|
120
|
+
bdm.delete(:snapshot_id)
|
|
121
|
+
bdm
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
# Blocks until the named volume reports ready, then honours any
|
|
127
|
+
# additional `:attach_timeout` grace period.
|
|
128
|
+
#
|
|
129
|
+
# @param volume_service [Fog::OpenStack::Volume] an established Cinder
|
|
130
|
+
# connection
|
|
131
|
+
# @param vol_id [String] id of the volume to wait on
|
|
132
|
+
# @param creation_timeout [Integer] seconds to wait for readiness
|
|
133
|
+
# @param attach_timeout [Integer] extra seconds to sleep once ready
|
|
134
|
+
# @return [void]
|
|
135
|
+
# @raise [Kitchen::ActionFailed] if the volume cannot be fetched back
|
|
136
|
+
# or enters an `error` state
|
|
137
|
+
def wait_for_volume(volume_service, vol_id, creation_timeout, attach_timeout)
|
|
138
|
+
# Fetch the volume by id rather than scanning the collection: a list
|
|
139
|
+
# call returns a single page (Cinder caps it at osapi_max_limit), so
|
|
140
|
+
# in a project with more volumes than that the one just created may
|
|
141
|
+
# not appear on it. `get` is a direct GET and returns nil on 404.
|
|
142
|
+
vol_model = volume_service.volumes.get(vol_id)
|
|
143
|
+
raise(ActionFailed, "Volume #{vol_id} disappeared after creation") if vol_model.nil?
|
|
67
144
|
|
|
68
145
|
@logger.debug "Waiting for volume to be ready for #{creation_timeout} seconds"
|
|
69
146
|
vol_model.wait_for(creation_timeout) do
|
|
70
147
|
sleep(1)
|
|
71
|
-
raise("Failed to make volume") if status.casecmp("error"
|
|
148
|
+
raise(ActionFailed, "Failed to make volume #{vol_id}") if status.casecmp("error") == 0
|
|
72
149
|
|
|
73
150
|
ready?
|
|
74
151
|
end
|
|
75
152
|
|
|
76
|
-
attach_timeout = bdm.key?(:attach_timeout) ? bdm[:attach_timeout] : 0
|
|
77
|
-
|
|
78
153
|
if attach_timeout > 0
|
|
79
154
|
@logger.debug "Sleeping for an additional #{attach_timeout} seconds before attaching volume to wait for Openstack to finish disk creation process.."
|
|
80
155
|
sleep(attach_timeout)
|
|
81
156
|
end
|
|
82
157
|
|
|
83
158
|
@logger.debug "Volume Ready"
|
|
84
|
-
|
|
85
|
-
vol_id
|
|
86
159
|
end
|
|
87
160
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
161
|
+
# Reads a timeout out of the block device mapping as an Integer.
|
|
162
|
+
#
|
|
163
|
+
# YAML happily parses `attach_timeout: 5` as an Integer but
|
|
164
|
+
# `attach_timeout: "5"` as a String, and comparing a String to 0 raises.
|
|
165
|
+
# Coerce so both spellings work.
|
|
166
|
+
#
|
|
167
|
+
# Base 10 is explicit: a bare `Integer("010")` would read the leading
|
|
168
|
+
# zero as octal and quietly wait 8 seconds instead of 10, and
|
|
169
|
+
# `Integer("08")` would raise outright.
|
|
170
|
+
#
|
|
171
|
+
# @param bdm [Hash] the block device mapping
|
|
172
|
+
# @param key [Symbol] the timeout key to read
|
|
173
|
+
# @param default [Integer] value to use when the key is absent or empty
|
|
174
|
+
# @return [Integer] the timeout in seconds
|
|
175
|
+
# @raise [Kitchen::ActionFailed] if the value is present but not a
|
|
176
|
+
# number
|
|
177
|
+
def timeout_value(bdm, key, default)
|
|
178
|
+
value = bdm[key]
|
|
179
|
+
# `attach_timeout:` with nothing after it parses to nil.
|
|
180
|
+
return default if value.nil? || value.to_s.strip.empty?
|
|
181
|
+
|
|
182
|
+
begin
|
|
183
|
+
# The base argument is only legal for a String; passing one
|
|
184
|
+
# alongside an Integer raises "base specified for non string value".
|
|
185
|
+
value.is_a?(String) ? Integer(value, 10) : Integer(value)
|
|
186
|
+
rescue ArgumentError, TypeError
|
|
187
|
+
raise(ActionFailed,
|
|
188
|
+
"The block_device_mapping #{key} must be a number, got #{value.inspect}")
|
|
189
|
+
end
|
|
94
190
|
end
|
|
95
191
|
end
|
|
96
192
|
end
|