kitchen-openstack 7.0.0 → 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.
@@ -34,8 +34,38 @@ require_relative "openstack/volume"
34
34
 
35
35
  module Kitchen
36
36
  module Driver
37
- # This takes from the Base Class and creates the OpenStack driver.
37
+ # Test Kitchen driver for OpenStack Nova.
38
+ #
39
+ # Creates and destroys Nova instances, optionally attaching floating IPs,
40
+ # Cinder volumes and specific Neutron networks. Credentials come from
41
+ # kitchen.yml, `OS_*` environment variables, or a standard
42
+ # `clouds.yaml` -- see {Clouds} for the precedence rules.
38
43
  class Openstack < Kitchen::Driver::Base
44
+ # Settings Fog requires as Strings. Fog re-coerces anything that looks
45
+ # numeric back to an Integer, so these are stringified on the way in.
46
+ #
47
+ # @return [Array<Symbol>]
48
+ FOG_STRING_SETTINGS = %i{
49
+ openstack_username
50
+ openstack_api_key
51
+ openstack_auth_url
52
+ openstack_project_name
53
+ openstack_project_id
54
+ openstack_user_domain
55
+ openstack_user_domain_id
56
+ openstack_project_domain
57
+ openstack_project_domain_id
58
+ openstack_domain_id
59
+ openstack_domain_name
60
+ openstack_region
61
+ openstack_endpoint_type
62
+ openstack_identity_api_version
63
+ openstack_application_credential_id
64
+ openstack_application_credential_secret
65
+ openstack_tenant
66
+ openstack_tenant_id
67
+ }.freeze
68
+
39
69
  include Clouds
40
70
  include Config
41
71
  include Helpers
@@ -49,13 +79,6 @@ module Kitchen
49
79
  default_config :clouds_yaml_path, nil
50
80
  default_config :server_name, nil
51
81
 
52
- # Merge clouds.yaml values into config so they are visible in
53
- # `kitchen diagnose` and available to all driver methods.
54
- def finalize_config!(instance)
55
- super
56
- apply_clouds_config
57
- self
58
- end
59
82
  default_config :server_name_prefix, nil
60
83
  default_config :key_name, nil
61
84
  default_config :port, "22"
@@ -82,6 +105,28 @@ module Kitchen
82
105
  default_config :write_timeout, 60
83
106
  default_config :metadata, nil
84
107
 
108
+ # Merges clouds.yaml and `OS_*` values into the config hash.
109
+ #
110
+ # Done at finalize time rather than lazily so the resolved values show up
111
+ # in `kitchen diagnose` and are available to every driver method.
112
+ #
113
+ # @param instance [Kitchen::Instance] the instance this driver serves
114
+ # @return [self]
115
+ def finalize_config!(instance)
116
+ super
117
+ apply_clouds_config
118
+ self
119
+ end
120
+
121
+ # Creates a Nova instance and waits until it is reachable.
122
+ #
123
+ # Idempotent: returns immediately if `state` already names a server.
124
+ #
125
+ # @param state [Hash] mutable instance state; gains `:server_id` and
126
+ # `:hostname`
127
+ # @return [void]
128
+ # @raise [Kitchen::ActionFailed] on any Fog or Excon failure
129
+ # @raise [Kitchen::InstanceFailure] if the server builds to ERROR state
85
130
  def create(state)
86
131
  config_server_name
87
132
  if state[:server_id]
@@ -117,6 +162,14 @@ module Kitchen
117
162
  raise ActionFailed, e.message
118
163
  end
119
164
 
165
+ # Destroys the Nova instance named by `state`, releasing its floating IP
166
+ # first when this driver allocated one.
167
+ #
168
+ # Safe to call when the server is already gone.
169
+ #
170
+ # @param state [Hash] mutable instance state; loses `:server_id` and
171
+ # `:hostname`
172
+ # @return [void]
120
173
  def destroy(state)
121
174
  return if state[:server_id].nil?
122
175
 
@@ -129,12 +182,7 @@ module Kitchen
129
182
  pub, priv = get_public_private_ips(server)
130
183
  pub, = parse_ips(pub, priv)
131
184
  pub_ip = pub[config[:public_ip_order].to_i] || nil
132
- if pub_ip
133
- info "Retrieve the ID of floating IP <#{pub_ip}>"
134
- floating_ip_id = network.list_floating_ips(floating_ip_address: pub_ip).body["floatingips"][0]["id"]
135
- network.delete_floating_ip(floating_ip_id)
136
- info "OpenStack Floating IP <#{pub_ip}> released."
137
- end
185
+ release_floating_ip(pub_ip) if pub_ip
138
186
  end
139
187
  server.destroy
140
188
  end
@@ -145,45 +193,126 @@ module Kitchen
145
193
 
146
194
  private
147
195
 
196
+ # Releases a floating IP back to its pool.
197
+ #
198
+ # A floating IP that Neutron no longer knows about is not an error worth
199
+ # failing a destroy over, so an unknown address is logged and skipped.
200
+ #
201
+ # @param pub_ip [String] the floating IP to release
202
+ # @return [void]
203
+ def release_floating_ip(pub_ip)
204
+ info "Retrieve the ID of floating IP <#{pub_ip}>"
205
+ net = network
206
+ floating_ips = net.list_floating_ips(floating_ip_address: pub_ip).body["floatingips"]
207
+ if floating_ips.nil? || floating_ips.empty?
208
+ warn "No floating IP found matching <#{pub_ip}>; nothing to release."
209
+ return
210
+ end
211
+
212
+ net.delete_floating_ip(floating_ips[0]["id"])
213
+ info "OpenStack Floating IP <#{pub_ip}> released."
214
+ end
215
+
216
+ # Builds the settings hash handed to every Fog service constructor.
217
+ #
218
+ # @return [Hash] Fog connection settings
148
219
  def openstack_server
149
220
  server_def = {
150
221
  connection_options: {},
151
222
  }
152
- required_server_settings.each { |s| server_def[s] = config[s] }
153
- optional_server_settings.each { |s| server_def[s] = config[s] if config[s] }
223
+ required_server_settings.each { |s| server_def[s] = normalize_fog_setting(s, config[s]) }
224
+ optional_server_settings.each { |s| server_def[s] = normalize_fog_setting(s, config[s]) if config[s] }
154
225
  connection_options.each { |s| server_def[:connection_options][s] = config[s] if config[s] }
155
226
  server_def
156
227
  end
157
228
 
229
+ # Settings always sent to Fog, even when nil.
230
+ #
231
+ # @return [Array<Symbol>]
158
232
  def required_server_settings
159
233
  %i{openstack_username openstack_api_key openstack_auth_url openstack_domain_id}
160
234
  end
161
235
 
236
+ # Every other `openstack_*` setting Fog recognizes, sent only when set.
237
+ #
238
+ # @return [Array<Symbol>]
162
239
  def optional_server_settings
163
240
  Fog::OpenStack::Compute.recognized.select do |k|
164
241
  k.to_s.start_with?("openstack")
165
242
  end - required_server_settings
166
243
  end
167
244
 
245
+ # Settings passed through to Excon rather than to Fog itself.
246
+ #
247
+ # `ssl_ca_file` belongs here, not in the Fog settings: Fog does not
248
+ # recognize it, so a CA bundle from `OS_CACERT` or a clouds.yaml
249
+ # `cacert` entry would otherwise be parsed and then silently dropped.
250
+ #
251
+ # @return [Array<Symbol>]
168
252
  def connection_options
169
- %i{read_timeout write_timeout connect_timeout}
253
+ %i{read_timeout write_timeout connect_timeout ssl_ca_file}
170
254
  end
171
255
 
256
+ # @return [Fog::OpenStack::Network] a Neutron connection
172
257
  def network
173
258
  Fog::OpenStack::Network.new(openstack_server)
174
259
  end
175
260
 
261
+ # @return [Fog::OpenStack::Compute] a Nova connection
176
262
  def compute
177
263
  Fog::OpenStack::Compute.new(openstack_server)
178
264
  end
179
265
 
266
+ # @return [Kitchen::Driver::Openstack::Volume] a Cinder helper
180
267
  def volume
181
268
  Volume.new(logger)
182
269
  end
183
270
 
271
+ # Resolves the block device mapping to send to Nova.
272
+ #
273
+ # @param config [Hash] the driver config
274
+ # @return [Hash] a Nova block device mapping
184
275
  def get_bdm(config)
185
276
  volume.get_bdm(config, openstack_server)
186
277
  end
278
+
279
+ # Coerces one setting to the type Fog expects.
280
+ #
281
+ # @param setting [Symbol] the Fog config key
282
+ # @param value [Object] the configured value
283
+ # @return [Object] the coerced value
284
+ def normalize_fog_setting(setting, value)
285
+ return value if value.nil?
286
+ return normalize_identity_api_version(value) if setting == :openstack_identity_api_version
287
+ return value unless FOG_STRING_SETTINGS.include?(setting)
288
+
289
+ value.to_s
290
+ end
291
+
292
+ # Normalizes the identity API version into a form Fog will not mangle.
293
+ #
294
+ # Fog::Service#coerce_options re-coerces any value where
295
+ # `value.to_s.to_i.to_s == value.to_s` back to an Integer, which
296
+ # then breaks Fog::OpenStack::Auth::Token.build (it calls `=~`
297
+ # on the value). Prefixing with "v" keeps Fog from coercing and
298
+ # still satisfies Token.build's `/(v)*2(\.0)*/i` regex check.
299
+ #
300
+ # @example
301
+ # normalize_identity_api_version(3) #=> "v3"
302
+ # normalize_identity_api_version("2.0") #=> "v2.0"
303
+ #
304
+ # @param value [String, Integer] the configured identity API version
305
+ # @return [String] a version string prefixed with "v"
306
+ def normalize_identity_api_version(value)
307
+ str = value.to_s.strip
308
+ return str if str.empty?
309
+ return str if str.start_with?("v", "V")
310
+
311
+ case str
312
+ when "2", "2.0" then "v2.0"
313
+ else "v#{str}"
314
+ end
315
+ end
187
316
  end
188
317
  end
189
318
  end
@@ -23,6 +23,12 @@ module Kitchen
23
23
  #
24
24
  # @author Jonathan Hartman <j@p4nt5.com>
25
25
  module Driver
26
- OPENSTACK_VERSION = "7.0.0"
26
+ # The kitchen-openstack gem version.
27
+ #
28
+ # Read by the gemspec and bumped by Release Please, so it must stay a
29
+ # plain string literal on a single line.
30
+ #
31
+ # @return [String]
32
+ OPENSTACK_VERSION = "8.0.0"
27
33
  end
28
34
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-openstack
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.0
4
+ version: 8.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Hartman
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-04-07 00:00:00.000000000 Z
12
+ date: 2026-08-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: test-kitchen