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

Returns a new instance of Rackspace.

Parameters:

  • config (Hash)

    the driver configuration



78
79
80
81
# File 'lib/kitchen/driver/rackspace.rb', line 78

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



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

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



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/kitchen/driver/rackspace.rb', line 93

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)

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



241
242
243
244
245
246
247
248
249
250
# File 'lib/kitchen/driver/rackspace.rb', line 241

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
  # see @note on bootstrap def about rackconnect
  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.

Returns:

  • (Array<String>)

    a problem description, or an empty array



202
203
204
205
206
207
208
# File 'lib/kitchen/driver/rackspace.rb', line 202

def credential_problems
  compute.servers.summary
  []
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



129
130
131
# File 'lib/kitchen/driver/rackspace.rb', line 129

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



142
143
144
145
146
147
148
149
# File 'lib/kitchen/driver/rackspace.rb', line 142

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



114
115
116
117
118
119
120
121
122
# File 'lib/kitchen/driver/rackspace.rb', line 114

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)

    mutable instance and driver state

Returns:

  • (Boolean)

    true when a problem was reported



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/kitchen/driver/rackspace.rb', line 177

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



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

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



255
256
257
258
259
260
# File 'lib/kitchen/driver/rackspace.rb', line 255

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



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

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



330
331
332
333
334
335
336
# File 'lib/kitchen/driver/rackspace.rb', line 330

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



292
293
294
295
296
297
# File 'lib/kitchen/driver/rackspace.rb', line 292

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



303
304
305
306
307
# File 'lib/kitchen/driver/rackspace.rb', line 303

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



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/kitchen/driver/rackspace.rb', line 156

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



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

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) ⇒ Object (private)

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



280
281
282
# File 'lib/kitchen/driver/rackspace.rb', line 280

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