class Kitchen::Driver::Openstack

This takes from the Base Class and creates the OpenStack driver.

Public Instance Methods

config_server_name() click to toggle source

Set the proper server name in the config

# File lib/kitchen/driver/openstack.rb, line 66
def config_server_name
  return if config[:server_name]

  config[:server_name] = if config[:server_name_prefix]
                           server_name_prefix(config[:server_name_prefix])
                         else
                           default_name
                         end
end
create(state) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 76
def create(state)
  config_server_name
  if state[:server_id]
    info "#{config[:server_name]} (#{state[:server_id]}) already exists."
    return
  end
  disable_ssl_validation if config[:disable_ssl_validation]
  server = create_server
  state[:server_id] = server.id

  # this is due to the glance_caching issues. Annoying yes, but necessary.
  debug "Waiting for a max time of:#{config[:glance_cache_wait_timeout]} seconds for OpenStack server to be in ACTIVE state"
  server.wait_for(config[:glance_cache_wait_timeout]) do
    sleep(1)
    raise(Kitchen::InstanceFailure, "OpenStack server ID <#{state[:server_id]}> build failed to ERROR state") if failed?

    ready?
  end
  info "OpenStack server ID <#{state[:server_id]}> created"

  if config[:floating_ip]
    attach_ip(server, config[:floating_ip])
  elsif config[:floating_ip_pool]
    attach_ip_from_pool(server, config[:floating_ip_pool])
  end
  state[:hostname] = get_ip(server)
  wait_for_server(state)
  add_ohai_hint(state)
rescue Fog::Errors::Error, Excon::Errors::Error => ex
  raise ActionFailed, ex.message
end
destroy(state) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 108
def destroy(state)
  return if state[:server_id].nil?

  disable_ssl_validation if config[:disable_ssl_validation]
  server = compute.servers.get(state[:server_id])

  unless server.nil?
    if config[:floating_ip_pool] && config[:allocate_floating_ip]
      info "Retrieve the floating IP"
      pub, priv = get_public_private_ips(server)
      pub, = parse_ips(pub, priv)
      pub_ip = pub[config[:public_ip_order].to_i] || nil
      if pub_ip
        info "Retrieve the ID of floating IP <#{pub_ip}>"
        floating_ip_id = network.list_floating_ips(floating_ip_address: pub_ip).body["floatingips"][0]["id"]
        network.delete_floating_ip(floating_ip_id)
        info "OpenStack Floating IP <#{pub_ip}> released."
      end
    end
    server.destroy
  end
  info "OpenStack instance <#{state[:server_id]}> destroyed."
  state.delete(:server_id)
  state.delete(:hostname)
end

Private Instance Methods

add_ohai_hint(state) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 400
def add_ohai_hint(state)
  if bourne_shell?
    info "Adding OpenStack hint for ohai"
    mkdir_cmd = "sudo mkdir -p #{hints_path}"
    touch_cmd = "sudo bash -c 'echo {} > #{hints_path}/openstack.json'"
    instance.transport.connection(state).execute(
      "#{mkdir_cmd} && #{touch_cmd}"
    )
  elsif windows_os?
    info "Adding OpenStack hint for ohai"
    touch_cmd = "New-Item #{hints_path}\\openstack.json"
    touch_cmd_args = "-Value '{}' -Force -Type file"
    instance.transport.connection(state).execute(
      "#{touch_cmd} #{touch_cmd_args}"
    )
  end
end
attach_ip(server, ip) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 334
def attach_ip(server, ip)
  info "Attaching floating IP <#{ip}>"
  server.associate_address ip
end
attach_ip_from_pool(server, pool) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 308
def attach_ip_from_pool(server, pool)
  @@ip_pool_lock.synchronize do
    info "Attaching floating IP from <#{pool}> pool"
    if config[:allocate_floating_ip]
      network_id = network
        .list_networks(
          name: pool
        ).body["networks"][0]["id"]
      resp = network.create_floating_ip(network_id)
      ip = resp.body["floatingip"]["floating_ip_address"]
      info "Created floating IP <#{ip}> from <#{pool}> pool"
      config[:floating_ip] = ip
    else
      free_addrs = compute.addresses.map do |i|
        i.ip if i.fixed_ip.nil? && i.instance_id.nil? && i.pool == pool
      end.compact
      if free_addrs.empty?
        raise ActionFailed, "No available IPs in pool <#{pool}>"
      end

      config[:floating_ip] = free_addrs[0]
    end
    attach_ip(server, config[:floating_ip])
  end
end
compute() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 164
def compute
  Fog::OpenStack::Compute.new(openstack_server)
end
connection_options() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 156
def connection_options
  %i{read_timeout write_timeout connect_timeout}
end
countdown(seconds) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 440
def countdown(seconds)
  date1 = Time.now + seconds
  while Time.now < date1
    Kernel.print "."
    sleep 10
  end
end
create_server() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 176
def create_server
  server_def = init_configuration
  raise(ActionFailed, "Cannot specify both network_ref and network_id") if config[:network_id] && config[:network_ref]

  if config[:network_id]
    networks = [].concat([config[:network_id]])
    server_def[:nics] = networks.flatten.map do |net_id|
      { "net_id" => net_id }
    end
  elsif config[:network_ref]
    networks = [].concat([config[:network_ref]])
    server_def[:nics] = networks.flatten.map do |net|
      { "net_id" => find_network(net).id }
    end
  end

  if config[:block_device_mapping]
    server_def[:block_device_mapping] = get_bdm(config)
  end

  %i{
    security_groups
    key_name
    user_data
    config_drive
    metadata
  }.each do |c|
    server_def[c] = optional_config(c) if config[c]
  end

  if config[:cloud_config]
    raise(ActionFailed, "Cannot specify both cloud_config and user_data") if config[:user_data]

    server_def[:user_data] = Kitchen::Util.stringified_hash(config[:cloud_config]).to_yaml.gsub(/^---\n/, "#cloud-config\n")
  end

  # Can't use the Fog bootstrap and/or setup methods here; they require a
  # public IP address that can't be guaranteed to exist across all
  # OpenStack deployments (e.g. TryStack ARM only has private IPs).
  compute.servers.create(server_def)
end
default_name() click to toggle source

Generate what should be a unique server name up to 63 total chars Base name: 15 Username: 15 Hostname: 23 Random string: 7 Separators: 3

Total: 63

# File lib/kitchen/driver/openstack.rb, line 273
def default_name
  [
    instance.name.gsub(/\W/, "")[0..14],
    ((Etc.getpwuid ? Etc.getpwuid.name : Etc.getlogin) || "nologin").gsub(/\W/, "")[0..14],
    Socket.gethostname.gsub(/\W/, "")[0..22],
    Array.new(7) { rand(36).to_s(36) }.join,
  ].join("-")
end
disable_ssl_validation() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 422
def disable_ssl_validation
  require "excon" unless defined?(Excon)
  Excon.defaults[:ssl_verify_peer] = false
end
filter_ips(addresses) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 381
def filter_ips(addresses)
  if config[:use_ipv6]
    addresses.select { |i| IPAddr.new(i["addr"]).ipv6? }
  else
    addresses.select { |i| IPAddr.new(i["addr"]).ipv4? }
  end
end
find_flavor(flavor_ref) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 249
def find_flavor(flavor_ref)
  flavor = find_matching(compute.flavors, flavor_ref)
  raise(ActionFailed, "Flavor not found") unless flavor

  debug "Selected flavor: #{flavor.id} #{flavor.name}"
  flavor
end
find_image(image_ref) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 241
def find_image(image_ref)
  image = find_matching(compute.images, image_ref)
  raise(ActionFailed, "Image not found") unless image

  debug "Selected image: #{image.id} #{image.name}"
  image
end
find_matching(collection, name) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 448
def find_matching(collection, name)
  name = name.to_s
  if name.start_with?("/") && name.end_with?("/")
    regex = Regexp.new(name[1...-1])
    # check for regex name match
    collection.each { |single| return single if regex&.match?(single.name) }
  else
    # check for exact id match
    collection.each { |single| return single if single.id == name }
    # check for exact name match
    collection.each { |single| return single if single.name == name }
  end
  nil
end
find_network(network_ref) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 257
def find_network(network_ref)
  net = find_matching(network.networks.all, network_ref)
  raise(ActionFailed, "Network not found") unless net

  debug "Selected net: #{net.id} #{net.name}"
  net
end
get_bdm(config) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 172
def get_bdm(config)
  volume.get_bdm(config, openstack_server)
end
get_ip(server) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 352
def get_ip(server)
  if config[:floating_ip]
    debug "Using floating ip: #{config[:floating_ip]}"
    return config[:floating_ip]
  end

  # make sure we have the latest info
  info "Waiting for network information to be available..."
  begin
    w = server.wait_for { !addresses.empty? }
    debug "Waited #{w[:duration]} seconds for network information."
  rescue Fog::Errors::TimeoutError
    raise ActionFailed, "Could not get network information (timed out)"
  end

  # should also work for private networks
  if config[:openstack_network_name]
    debug "Using configured net: #{config[:openstack_network_name]}"
    return filter_ips(server.addresses[config[:openstack_network_name]]).first["addr"]
  end

  pub, priv = get_public_private_ips(server)
  priv = server.ip_addresses if Array(pub).empty? && Array(priv).empty?
  pub, priv = parse_ips(pub, priv)
  pub[config[:public_ip_order].to_i] ||
    priv[config[:private_ip_order].to_i] ||
    raise(ActionFailed, "Could not find an IP")
end
get_public_private_ips(server) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 339
def get_public_private_ips(server)
  begin
    pub = server.public_ip_addresses
    priv = server.private_ip_addresses
  rescue Fog::OpenStack::Compute::NotFound, Excon::Errors::Forbidden
    # See Fog issue: https://github.com/fog/fog/issues/2160
    addrs = server.addresses
    addrs["public"] && pub = addrs["public"].map { |i| i["addr"] }
    addrs["private"] && priv = addrs["private"].map { |i| i["addr"] }
  end
  [pub, priv]
end
hints_path() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 418
def hints_path
  Ohai.config[:hints_path][0]
end
init_configuration() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 218
def init_configuration
  raise(ActionFailed, "Cannot specify both image_ref and image_id") if config[:image_id] && config[:image_ref]
  raise(ActionFailed, "Cannot specify both flavor_ref and flavor_id") if config[:flavor_id] && config[:flavor_ref]

  {
    name: config[:server_name],
    image_ref: config[:image_id] || find_image(config[:image_ref]).id,
    flavor_ref: config[:flavor_id] || find_flavor(config[:flavor_ref]).id,
    availability_zone: config[:availability_zone],
  }
end
network() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 160
def network
  Fog::OpenStack::Network.new(openstack_server)
end
openstack_server() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 136
def openstack_server
  server_def = {
    connection_options: {},
  }
  required_server_settings.each { |s| server_def[s] = config[s] }
  optional_server_settings.each { |s| server_def[s] = config[s] if config[s] }
  connection_options.each { |s| server_def[:connection_options][s] = config[s] if config[s] }
  server_def
end
optional_config(c) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 230
def optional_config(c)
  case c
  when :security_groups
    config[c] if config[c].is_a?(Array)
  when :user_data
    File.open(config[c], &:read) if File.exist?(config[c])
  else
    config[c]
  end
end
optional_server_settings() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 150
def optional_server_settings
  Fog::OpenStack::Compute.recognized.select do |k|
    k.to_s.start_with?("openstack")
  end - required_server_settings
end
parse_ips(pub, priv) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 389
def parse_ips(pub, priv)
  pub = Array(pub)
  priv = Array(priv)
  if config[:use_ipv6]
    [pub, priv].each { |n| n.select! { |i| IPAddr.new(i).ipv6? } }
  else
    [pub, priv].each { |n| n.select! { |i| IPAddr.new(i).ipv4? } }
  end
  [pub, priv]
end
required_server_settings() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 146
def required_server_settings
  %i{openstack_username openstack_api_key openstack_auth_url openstack_domain_id}
end
server_name_prefix(server_name_prefix) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 282
def server_name_prefix(server_name_prefix)
  # Generate what should be a unique server name with given prefix
  # of up to 63 total chars
  #
  # Provided prefix:  variable, max 54
  # Separator:        1
  # Random string:    8
  # ===================
  # Max:              63
  #
  if server_name_prefix.length > 54
    warn "Server name prefix too long, truncated to 54 characters"
    server_name_prefix = server_name_prefix[0..53]
  end

  server_name_prefix.gsub!(/\W/, "")

  if server_name_prefix.empty?
    warn "Server name prefix empty or invalid; using fully generated name"
    default_name
  else
    random_suffix = ("a".."z").to_a.sample(8).join
    server_name_prefix + "-" + random_suffix
  end
end
volume() click to toggle source
# File lib/kitchen/driver/openstack.rb, line 168
def volume
  Volume.new(logger)
end
wait_for_server(state) click to toggle source
# File lib/kitchen/driver/openstack.rb, line 427
def wait_for_server(state)
  if config[:server_wait]
    info "Sleeping for #{config[:server_wait]} seconds to let your server start up..."
    countdown(config[:server_wait])
  end
  info "Waiting for server to be ready..."
  instance.transport.connection(state).wait_until_ready
rescue
  error "Server #{state[:hostname]} (#{state[:server_id]}) not reachable. Destroying server..."
  destroy(state)
  raise
end