Class: Kitchen::Driver::Rackspace

Inherits:
Base
  • Object
show all
Defined in:
lib/kitchen/driver/rackspace.rb

Overview

Test Kitchen driver for Rackspace Cloud Servers.

Creates a Cloud Server for the instance under test, waits for it to become reachable, and destroys it afterwards. Talking to the server is the transport's job; this driver only tells Test Kitchen where it is and who to log in as.

Rackspace can run post-build automation -- RackConnect and Managed Service Level -- that keeps modifying a server after it first reports ready, including changing its public address. Set rackconnect_wait or servicelevel_wait so the driver waits for that to finish before handing the server over, or the transport will connect to an address that is about to change.

Constant Summary collapse

LIVE_STATES =

Server states Rackspace reports for a server that is up and reachable.

%w{ACTIVE}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(config) ⇒ Rackspace

Sets up the driver and applies wait_for as fog's global timeout.

Fog.timeout is process-wide, so the last driver constructed wins if several are in play.

Parameters:

  • config (Hash)

    the driver configuration



83
84
85
86
# File 'lib/kitchen/driver/rackspace.rb', line 83

def initialize(config)
  super
  Fog.timeout = config[:wait_for].to_i
end

Instance Method Details

#computeFog::Compute (private)

Builds the fog compute connection from the configured credentials.

Returns:

  • (Fog::Compute)

    a Rackspace compute connection



235
236
237
238
239
240
241
242
243
# File 'lib/kitchen/driver/rackspace.rb', line 235

def compute
  server_def = { provider: "Rackspace" }
  opts = %i{version rackspace_username rackspace_api_key
            rackspace_region}
  opts.each do |opt|
    server_def[opt] = config[opt]
  end
  Fog::Compute.new(server_def)
end

#create(state) ⇒ void

This method returns an undefined value.

Creates the Cloud Server and waits until it can be logged into.

Waits for the server to report ready, then optionally for RackConnect and Managed Service Level automation, then for the transport to accept a connection.

Parameters:

  • state (Hash)

    mutable instance state; gains server_id, hostname, username, and port

Raises:

  • (Kitchen::ActionFailed)

    if the Rackspace API rejects the build



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/kitchen/driver/rackspace.rb', line 98

def create(state)
  server = create_server
  state[:server_id] = server.id
  info("Rackspace instance <#{state[:server_id]}> created.")
  server.wait_for { ready? }
  puts "(server ready)"
  rackconnect_check(server) if config[:rackconnect_wait]
  servicelevel_check(server) if config[:servicelevel_wait]
  state[:hostname] = hostname(server)
  state[:username] = config[:username]
  state[:port] = config[:port] if config[:port]
  tcp_check(state)
rescue Fog::Errors::Error, Excon::Errors::Error => ex
  raise ActionFailed, ex.message
end

#create_serverFog::Compute::RackspaceV2::Server (private)

Note:

fog's bootstrap calls Server#setup, which rescues Errno::ECONNREFUSED and retries forever, one second apart. A server that never opens port 22 hangs here rather than failing, and wait_for does not bound it.

Bootstraps the Cloud Server from the configured options.

RackConnect and Managed Service Level both need the root password left unlocked to do their work, so no_passwd_lock is forced on whenever either wait is requested, regardless of how it was configured.

Returns:

  • (Fog::Compute::RackspaceV2::Server)

    the newly built server



257
258
259
260
261
262
263
264
265
266
267
# File 'lib/kitchen/driver/rackspace.rb', line 257

def create_server
  server_def = { name: config[:server_name], networks: }
  %i{image_id flavor_id public_key_path no_passwd_lock user_data config_drive}.each do |opt|
    server_def[opt] = config[opt]
  end
  # RackConnect and Managed Service Level need the root password left
  # unlocked; see the note above.
  no_passwd_lock = config[:rackconnect_wait] || config[:servicelevel_wait]
  server_def[:no_passwd_lock] = no_passwd_lock if no_passwd_lock
  compute.servers.bootstrap(server_def)
end

#credential_problemsArray<String> (private)

Confirms the configured credentials can actually talk to Rackspace, which is cheaper to learn here than half way through a converge.

Listing servers is the cheapest authenticated call fog-rackspace offers: GET /servers returns only an ID and a name per server, and it exercises the compute endpoint for the configured region rather than just the identity endpoint.

Returns:

  • (Array<String>)

    a problem description, or an empty array



213
214
215
216
217
218
219
# File 'lib/kitchen/driver/rackspace.rb', line 213

def credential_problems
  compute.servers.all
  []
rescue Fog::Errors::Error, Excon::Errors::Error => e
  ["Rackspace rejected the configured credentials for region " \
   "#{config[:rackspace_region]}: #{e.message}"]
end

#default_imageString?

Looks up the base image for the platform under test.

Returns:

  • (String, nil)

    the Rackspace image ID, or nil when the platform is not one the bundled image list knows about, in which case image_id must be set explicitly



134
135
136
# File 'lib/kitchen/driver/rackspace.rb', line 134

def default_image
  images[instance.platform.name]
end

#default_nameString

Generates a server name that is unique per run and fits Rackspace's 63-character limit.

The budget is spent as base name 15, username 15, hostname 23, random suffix 7, and three separators, for 63 exactly. Each part is stripped of non-word characters and truncated to its share, so a long login or hostname cannot push the result over.

Returns:

  • (String)

    e.g. default-alice-buildbox01-x7f2p9q



147
148
149
150
151
152
153
154
# File 'lib/kitchen/driver/rackspace.rb', line 147

def default_name
  [
    instance.name.gsub(/\W/, "")[0..14],
    (Etc.getlogin || "nologin").gsub(/\W/, "")[0..14],
    Socket.gethostname.gsub(/\W/, "")[0..22],
    Array.new(7) { rand(36).to_s(36) }.join,
  ].join("-")
end

#destroy(state) ⇒ void

This method returns an undefined value.

Destroys the Cloud Server, if one was created.

Parameters:

  • state (Hash)

    mutable instance state; server_id and hostname are removed



119
120
121
122
123
124
125
126
127
# File 'lib/kitchen/driver/rackspace.rb', line 119

def destroy(state)
  return if state[:server_id].nil?

  server = compute.servers.get(state[:server_id])
  server.destroy unless server.nil?
  info("Rackspace instance <#{state[:server_id]}> destroyed.")
  state.delete(:server_id)
  state.delete(:hostname)
end

#doctor(state) ⇒ Boolean

Checks the configuration for the mistakes that only show up as a confusing failure part way through create.

Parameters:

  • state (Hash)

    instance state; accepted for the Test Kitchen hook signature and not read

Returns:

  • (Boolean)

    true when a problem was reported



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/kitchen/driver/rackspace.rb', line 183

def doctor(state) # rubocop:disable Lint/UnusedMethodArgument
  problems = []

  if config[:image_id].nil?
    problems << "No image_id is set and #{instance.platform.name} is not " \
                "in the bundled image list. Set image_id explicitly."
  end

  if config[:public_key_path].nil?
    problems << "No public key was found in ~/.ssh. Set public_key_path " \
                "to the key Rackspace should install on the server."
  end

  problems.concat(credential_problems)

  problems.each { |problem| warn(problem) }
  !problems.empty?
end

#hostname(server) ⇒ String (private)

Picks the address Test Kitchen should connect to.

Parameters:

  • server (Fog::Compute::RackspaceV2::Server)

    the built server

Returns:

  • (String)

    the private address when servicenet is set, otherwise the public one



336
337
338
339
340
341
342
# File 'lib/kitchen/driver/rackspace.rb', line 336

def hostname(server)
  if config[:servicenet] == false
    server.public_ip_address
  else
    server.private_ip_address
  end
end

#imagesHash{String => String} (private)

The bundled platform-name to image-ID map.

Returns:

  • (Hash{String => String})

    parsed from data/images.json



272
273
274
275
276
277
# File 'lib/kitchen/driver/rackspace.rb', line 272

def images
  @images ||= begin
    json_file = File.expand_path("../../../data/images.json", __dir__)
    JSON.parse(IO.read(json_file))
  end
end

#lookup_server(server_id) ⇒ Fog::Compute::RackspaceV2::Server? (private)

Looks a server up without turning a missing one into a failure.

Parameters:

  • server_id (String)

    the Rackspace server ID

Returns:

  • (Fog::Compute::RackspaceV2::Server, nil)

    the server, or nil when Rackspace does not know it or cannot be reached



226
227
228
229
230
# File 'lib/kitchen/driver/rackspace.rb', line 226

def lookup_server(server_id)
  compute.servers.get(server_id)
rescue Fog::Errors::Error, Excon::Errors::Error
  nil
end

#networksArray<String>? (private)

Builds the network list for the new server.

Rackspace's PublicNet and ServiceNet have fixed, well-known IDs. Any configured networks are added to those two rather than replacing them, since dropping PublicNet would leave the server unreachable.

Returns:

  • (Array<String>, nil)

    network IDs, or nil to let Rackspace apply its own defaults



352
353
354
355
356
357
358
# File 'lib/kitchen/driver/rackspace.rb', line 352

def networks
  base_nets = %w{
    00000000-0000-0000-0000-000000000000
    11111111-1111-1111-1111-111111111111
  }
  config[:networks] ? base_nets + config[:networks] : nil
end

#rackconnect_check(server) ⇒ void (private)

This method returns an undefined value.

Waits for RackConnect automation to finish.

The server is refreshed afterwards, because RackConnect assigns a new public address as part of its work and the stale one would otherwise be handed to the transport.

Parameters:

  • server (Fog::Compute::RackspaceV2::Server)

    the server to poll



314
315
316
317
318
319
# File 'lib/kitchen/driver/rackspace.rb', line 314

def rackconnect_check(server)
  server.wait_for \
    { .all["rackconnect_automation_status"] == "DEPLOYED" }
  puts "(rackconnect automation complete)"
  server.update # refresh accessIPv4 with new IP
end

#servicelevel_check(server) ⇒ void (private)

This method returns an undefined value.

Waits for Managed Service Level automation to finish.

Parameters:

  • server (Fog::Compute::RackspaceV2::Server)

    the server to poll



325
326
327
328
329
# File 'lib/kitchen/driver/rackspace.rb', line 325

def servicelevel_check(server)
  server.wait_for \
    { .all["rax_service_level_automation"] == "Complete" }
  puts "(service level automation complete)"
end

#status(state) ⇒ Hash

Reports what Rackspace currently thinks of the server.

Parameters:

  • state (Hash)

    instance state naming the server

Returns:

  • (Hash)

    a Test Kitchen status hash, or the base implementation's answer when there is no server or Rackspace does not know it



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/kitchen/driver/rackspace.rb', line 161

def status(state)
  return super unless state[:server_id]

  server = lookup_server(state[:server_id])
  return super unless server

  {
    live: LIVE_STATES.include?(server.state),
    state: server.state,
    source: "driver",
    resource_id: state[:server_id],
    message: "Rackspace reports the server as #{server.state}",
    checked_at: Time.now.utc.iso8601,
  }
end

#tcp_check(state) ⇒ void (private)

This method returns an undefined value.

Waits until the server will accept a login.

The TCP check does not honour ssh_config, which some setups need, so no_ssh_tcp_check swaps the check for a fixed sleep of no_ssh_tcp_check_sleep seconds instead.

Parameters:

  • state (Hash)

    instance state describing how to connect



287
288
289
290
291
292
293
# File 'lib/kitchen/driver/rackspace.rb', line 287

def tcp_check(state)
  # allow driver config to bypass SSH tcp check -- because
  # it doesn't respect ssh_config values that might be required
  wait_for_sshd(state) unless config[:no_ssh_tcp_check]
  sleep(config[:no_ssh_tcp_check_sleep]) if config[:no_ssh_tcp_check]
  puts "(ssh ready)"
end

#wait_for_sshd(state) ⇒ void (private)

This method returns an undefined value.

Blocks until the configured transport can connect to the instance.

Kitchen::Driver::SSHBase used to supply this; the configured transport knows how to wait for the instance to accept connections.

Parameters:

  • state (Hash)

    instance state describing how to connect



302
303
304
# File 'lib/kitchen/driver/rackspace.rb', line 302

def wait_for_sshd(state)
  instance.transport.connection(state, &:wait_until_ready)
end